2020年1月6日月曜日

golang で関数を扱うテクニック

概要

golang の関数を扱う Tips を紹介します
リファクタリングやテストを書く際に使えるテクニックです

環境

  • macOS 10.15.2
  • golang 1.12.9

オーバーライド的なことがしたい

構造体を新たに定義してそのフィールドにオーバーライドしたい構造体を 1 つ持たせます

package main

import (
    "fmt"
)

type Animal struct {

}

func (a *Animal) Cry() string {
    return "..."
}

type Dog struct {
    Animal
}

func (d *Dog) Cry() string {
    return "bowwow"
}

func main() {
    animal := Animal{}
    fmt.Println(animal.Cry())
    dog := Dog{animal}
    fmt.Println(dog.Cry())
}

Animal の Cry を Dog がオーバーライドしています
それぞれの構造体に合わせた Cry がコールされます
これだけだとあまり有り難みがわかりませんが interface と組み合わせることで柔軟な実装ができるようになります

package main

import (
    "fmt"
)

// Animal
type AnimalInterface interface {
    Cry() string
}

type Animal struct {

}

func (a *Animal) Cry() string {
    return "..."
}

type Dog struct {
    Animal
}

func (d *Dog) Cry() string {
    return "bowwow"
}

// Human
type Human struct {
    Pet AnimalInterface
}

func (h *Human) CryMyPet() string {
    return h.Pet.Cry()
}

func main() {
    animal := Animal{}
    dog := Dog{animal}
    // AnimalInterface を実装しているペットは何でも飼える
    player1 := Human{}
    player1.Pet = &dog
    // 飼っているペットに合わせて鳴き声が変わる
    fmt.Println(player1.CryMyPet())
}

AnimalInterface を抽出し Animal と Dog はそれらを実装します
こうすることで AnimalInterface を持つ Human は AnimalInterface を実装している好きなペットを飼うことができるようになります
また Human からの呼び出しを変更することなくペットの鳴き声を返ることができます

この手法はテスト時の mock の差し替えなどに使われます
本番では使う Pet は Dog だがテスト時には Animal という名前の mock を使わせることができます

オーバーライドが使えないケース

ある関数 (Cry) 内でコールされている関数 (GetName) だけをオーバーライドしようとしてもそれは動作しません
具体的には以下の通りで Dog は GetName だけをオーバーライドしたいのですが Cry がないため Animal の Cry -> GetName とコールされてしまいます
一見オーバーライドした Dog の GetName がコールされそうですがされないのです
しかも以下のサンプルはコンパイルエラーにはならないので更に注意が必要です

package main

import (
    "fmt"
)

// Animal
type AnimalInterface interface {
    Cry() string
    GetName() string
}

type Animal struct {

}

func (a *Animal) Cry() string {
    return a.GetName() + " < " + "..."
}

func (a *Animal) GetName() string {
    return "no name"
}

type Dog struct {
    Animal
}

// func (d *Dog) Cry() string {
//  return d.GetName() + " < " + "bowwow"
// }

func (d *Dog) GetName() string {
    return "Pochi"
}

// Human
type Human struct {
    Pet AnimalInterface
}

func (h *Human) CryMyPet() string {
    return h.Pet.Cry()
}

func main() {
    animal := Animal{}
    dog := Dog{animal}
    // AnimalInterface を実装しているペットは何でも飼える
    player1 := Human{}
    player1.Pet = &dog
    // 飼っているペットに合わせて鳴き声が変わる
    fmt.Println(player1.CryMyPet())
}

もし GetName をオーバーライドしたならそれを呼び出している Cry のオーバーライドも必要です
こんな感じのことをしたい場合は以下の方法を使うのもありだと思います

構造体のフィールドとして関数を持つことで DI 的なことができる

もうひとつ方法として関数自体を構造対のフィールドに設定することで関数の挙動をあとから変更する手法です

package main

import (
    "fmt"
)

type Animal struct {
    Cry func() string
}

func main() {
    animal := Animal{}
    // 構造体を生成後にフィールドに関数を設定する
    animal.Cry = func() string {
        return "..."
    }
    fmt.Println(animal.Cry())
    // 構造体を生成時に関数を定義することも可能
    animal2 := Animal{
        Cry: func() string {
            return "bowwow"
        },
    }
    fmt.Println(animal2.Cry())
}

これでも前者と呼び出しは変えずに関数自体の挙動を変更することができます
こちらのほうが柔軟でかつ挙動を変更したいときに変更できるので便利なのですが 1 つの構造体がいろいろな挙動を持つ可能性があるので複雑になることもあります

正直使い分けかなと思いますが前者のほうが抽象化という意味ではしっかりできているかなと思います

最後に

golang の関数を定義し扱うテクニックを少し紹介しました
これ以外にもいろいろとあると思いますが個人的にこういったテクニックは主にイベントハンドリングやテスト時にかなり有効な手法だと思います

例えば使いこなしているコードとそうでないコードではテストの書き方や見やすさ、書きやすさなどが大きく変わってくると思います

0 件のコメント:

コメントを投稿