golangでダックタイピングをしてみよう

f:id:AdwaysEngineerBlog:20170711181010j:plain:w500

こんにちは、久保田です。
最近はインフラdivに移動し、専らgolangで日々の業務を行っております。

そんな僕は以前までRubyばかりを書いている日々でした。

なのでgoを書いていると、いけないと思いながらもこんな思いが生まれてしまうわけです。
「Rubyみたくカジュアルに書きたい。文字列からメソッドの実行がしたい。」と。

何事もダメだダメだと言われるとやりたくなるのが人間の性。「禁断の」ほど熱く燃え上がるものですね。

ということでRubyのメインコンセプトであるダックタイピングをgolangで実践してみようと思います。一緒に熱く燃え上がりましょう。

ダックタイピング

「もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルである」

Rubyやったことがある人ならば一度は聞いたことがある言葉ですね。
これを説明するとオブジェクト指向の話とかが難しいので、ものすごく簡単に言いますが、

「大切なのは振る舞い」

ってことですね。

オブジェクトがどんな形をしているかではなく、何ができるかにフォーカスを当てた考え方です。
アヒルのように扱えるならアヒルとして扱えるし、猫のように扱えるなら猫なんです。猫だと思っていたらそれは虎でも別にいいんです。

Rubyではこの考え方があるので、型を意識せず色々と自由な書き方ができるんですね。いいか悪いかは別として。

golangでダックタイピング

さて、では本題のgolangでダックタイピングをやってみようかなと思います。

ここで登場する大切なgolangのキーワードはinterfaceです。
interfaceを使うとダックタイピングを実装できます。

golangでは、
interfaceを実装している型はそのinterfaceの型として扱うことができます。

なんだか聞いたことのあるの響きですね。

例えば、こんなことができます。

package main

import "fmt"

type animal interface {
    bark() string
}

type dog struct {
}

type cat struct {
}

func (d dog) bark() string {
    return "bow"
}

func (c cat) bark() string {
    return "mew"
}

func main() {
    animals := []animal{dog{}, cat{}}

    for _, animal := range animals {
        fmt.Println(animal.bark())
    }
}

mainの中に、dog型とcat型を同じanimal型のsliceに入れています。
本来、異なる型のsliceには要素を入れることができないのですが、
dog型cat型animal型を実装しているので、こんなことができてしまいます。
interfaceが持っているメソッドが重要なのです。

複数の動作をするコマンドを実装してみる。

さて、基本的なところがわかったところで実践的な実装をしてみます。
golangといえばコマンドが簡単に作れるのが魅力だと思うので、
引数によって複数の動作をするコマンドをダックタイピングを用いて実装してみます。

動物の名前を渡したら鳴き声を出させましょうか。

例えばRubyであれば、こんなインターフェイスを持つコマンドは簡単に、簡潔に書けますよね。

$ ruby animal.rb dog
=> bow
$ ruby animal.rb cat
=> mew
$ ruby animal.rb bird
=> you can use [dog|cat]

animal.rbdogcatメソッドを実装し、sendで実行してあげれば文字列をそのままメソッドとして使えるので、タイプ数が減りますね。
ないものはmethod_missingなんかでhelpを出せるようにする感じですかね。

しかしgolangでは文字列実行などない。(と思います)

これをgolangで実装しようと思うと、こんな感じになってしまっていました。

package main

import (
    "fmt"
    "os"
)

func main() {
    commandName := os.Args[1]

    var r string

    switch commandName {
    case "dog":
        r = dog()
    case "cat":
        r = cat()
    default:
        r = help()
    }

    fmt.Println(r)
}

func dog() string {
    return "bow"
}

func cat() string {
    return "mew"
}

func help() string {
    return "you can use [dog|cat]"
}

嫌ですね。。。

これだと動物を増やすたびにswitchの中を増やさなければいけませんね。
それに、golangではできればmain packageの中に書きまくりたくないので、できればそれぞれの動物はpackage化してしまいたいです。

こういうのはできればコマンド引数の文字列から実行かつpackage化をしたい。。。

というわけで以下のようにしてみました。

.
├── cli
│   └── cli.go
├── animal
│   ├── animal.go
│   ├── cat
│   │    └── bark.go
│   └── dog
│        └── bark.go
└── main.go
  • main.go
package main

import (
    "fmt"

    _ "./animal/cat"
    _ "./animal/dog"

    "./cli"
)

func main() {
    fmt.Println(cli.Run())
}
  • cli/cli.go
package cli

import (
    a "../animal"
    "os"
)

var animals = map[string]a.Animal{}

// Register sets command to commands
func Register(key string, animal a.Animal) {
    animals[key] = animal
}

// Run calls commands
func Run() string {
    animal, ok := animals[os.Args[1]]
    if ok {
        return animal.Bark()
    }
    return help()
}

func help() string {
    return "you can use [dog|cat]"
}
  • animal/animal.go
package animal

// Animal is interface that has Bark function.
type Animal interface {
    Bark() string
}
  • animal/dog/bark.go
package dog

import (
    "../../cli"
)

// Dog implements cli.Animal
type Dog struct {
}

func init() {
    cli.Register("dog", Dog{})
}

// Bark returns string
func (d Dog) Bark() string {
    return "bow"
}
  • animal/cat/bark.go
package cat

import (
    "../../cli"
)

// Cat implements cli.Animal
type Cat struct {
}

func init() {
    cli.Register("cat", Cat{})
}

// Bark returns string
func (b Cat) Bark() string {
    return "mew"
}

main.goが動くと、animal以下のdog, cat がimportされます。
_ で importされるので、init関数が実行されるだけです。
それぞれの動物packageのinit関数が実行されると、cli.Registerが動き、cliのcommands変数に構造体が初期化されて入ります。

そしてその構造体をコマンド引数をmapのkeyとして、取り出し、animalインターフェイスで宣言されたそれぞれのBark関数を実行しているわけです。

animalインターフェイスはcli.goでimportしているので、DogとCatはanimalインターフェイスをimplementedだと判断されます。

コマンドが増えた時も、animal以下に動物を増やし、animalインターフェイスを実装しmain.goでimportすれば簡単に増やせます。

Bark関数が重要となり、ダックな世界が完成しましたね。

今回は以上です。

animal以下のpackageがcliを読み込んでいるところが若干複雑になっていますが、、まぁ仕方ないとしましょう。

このパターンは、以下のライブラリを真似していますので、皆さんもぜひ実装してください。

https://github.com/vmware/govmomi