at kaneshin

Free space for me.

golangでABテストの振る舞いを実装する

golangに限らないんですが、ABテストの振る舞いをちゃんとした設計のもと実装するのって難しいと思っています。

よくあるパターンとしては、Userという構造体があって、それに対してABテストによる振る舞いを変えるように実装をすることかと思います。

User

type User struct {
    ID   int
    Name string
}

関数を生やす

あまり考えずに関数を作っていく方針

func (u User) IsFoo() bool {
    return u.ID%10 == 0
}

func (u User) IsBar() bool {
    return u.ID%10 == 1
}

func main() {
    u := User{ID: 1, Name: "kaneshin"}

    fmt.Println(u.IsFoo())
    fmt.Println(u.IsBar())
}

コード:https://play.golang.org/p/bSw-EfBJFm

これは、実装自体は簡単で、やりがちなんですが、User構造体が肥大化するのでこの実装は好きじゃないです。

型を新規作成

新しく構造体を定義して、それらがABテストで有効かどうかの振る舞いを実装します。

type (
    FooTester User
    BarTester User
)

func (u FooTester) IsEnabled() bool {
    return u.ID%10 == 0
}

func (u BarTester) IsEnabled() bool {
    return u.ID%10 == 1
}

func main() {
    u := User{ID: 1, Name: "kaneshin"}
 
    fmt.Println(FooTester(u).IsEnabled())
    fmt.Println(BarTester(u).IsEnabled())
}

コード:https://play.golang.org/p/H_FZKYHX17

これが今のところわかりやすいですかね。必要に応じて下記のような interface を定義することもできます。

type ABTester interface {
    IsEnabled() bool
}

これを書いたキッカケ

go1.8から型の変換がより使える幅が広がったので、何かいい方法無いかなーと考えていたのですが、結果としていい方法は思いつきませんでした。

func example() {
    type T1 struct {
        X int `json:"foo"`
    }
    type T2 struct {
        X int `json:"bar"`
    }
    var v1 T1
    var v2 T2
    v1 = T1(v2) // now legal
}

タグの変換ができるので、Unmarshal/Marshalやタグを定義してなどのうまい方法は思いきましたが、暗にリフレクションを多用する実装はしたくないな、と。

おわりに

結論はないんですが、ABテストの知見って無いですかね。トレイトとかの案も出ると思いますが、使用する元の情報(e.g.: User.ID)をトレイト先が欲するのは違う。日付判定しかしないトレイトならOKなんですがね。