goでflexibleな実装の選択のためのちょっとしたパターンのメモ

前回の記事でxormのコードを読んでいたときに、flexibleに実装を選択するためのパターンがあったことを思い出したのでそのメモ。

flexibleな実装の選択?

実装の選択と言っても、DI的な話ではない。デフォルト実装と拡張の実装の間を自由に行き来できるような実装のパターンのこと。もう少し具体的に言うと、デフォルト実装であるBaseとその拡張の実装であるXの間で以下のような関係が成り立つもののこと。

  • 拡張実装(X)からデフォルト実装(Base)からの任意の処理が呼べる
  • デフォルト実装(Base)から拡張実装(X)の任意の処理が呼べる
  • 拡張実装(X)はデフォルト実装(Base)の定義を上書きできる

もう少しわかりやすい例で言えば、ORM的なライブラリを作っていたとして、mysqlやpostgresのような方言部分が拡張実装。そしてインターフェイスとしてDialectを持っているというような感じ。そしてbase側から拡張側で実装した処理も呼べるようにするにはどうしたら良いか?というような話。

要件

ここからは、どのような挙動のものが欲しいかを説明するもう少し具体的な例を紹介することにする

まず、どのような機能を提供するかのインターフェイスを定義する。Managerという名前を付けてみる1。Managerは以下のようなもの。このようなインターフェイスがあったとして、

type Manager interface {
    CreateTable(name string)
    DropTable(name string)
    RefreshTable(name string) // Drop + Create

    Prefix() string

    TypeOf(ob interface{}) string
}

これのデフォルト実装部分を担ったBaseというstructがあり2、その拡張としてXというstructが存在するということにする。

最終的には、以下のmain部分のように、RefrshTable()とTypeOf()が使えるようになることを目指す。ここでRefreshTable()はCreateTable()とDropTable()を呼び出す実装。

func main() {
    x := NewXManager("foo")
    x.RefreshTable("Target")    // Manager.RefreshTable()

    // xで拡張されたTypeOfが呼べる
    fmt.Println("----------------------------------------")
    fmt.Println("10 is", x.TypeOf(10))
    fmt.Println("foo is", x.TypeOf("foo"))
    fmt.Println("0.1234 is", x.TypeOf(0.1234))
}

このような振る舞いをするコードを例として実装していく。

実装例

ここからは具体的な実装例の話。以下の3つの対応ができれば良いという話だった。

  • 拡張実装(X)からデフォルト実装(Base)からの任意の処理が呼べる
  • デフォルト実装(Base)から拡張実装(X)の任意の処理が呼べる
  • 拡張実装(X)はデフォルト実装(Base)の定義を上書きできる

拡張実装(X)からデフォルト実装(Base)からの任意の処理が呼べる

これは埋め込みを使えば完成する。したがってXの定義が以下の様になっていれば良い。 ここで、CreateTable()はデフォルト実装で定義したもの。埋め込みなので直接呼べる。

type X struct {
    Base
}

func (x *X) RefreshTable(name string) {
    fmt.Println("refresh..")
    x.DropTable(name)   // こちらはXで定義した実装
    x.CreateTable(name) // こちらはBaseで定義したdefault実装
}

func (x *X) DropTable(name string) {
    fmt.Printf("%-5s\t%-10s\t%s.%s\n", "in X", "drop table", x.Prefix(), name)
}

デフォルト実装(Base)から拡張実装(X)の任意の処理が呼べる

ここがパターンと呼んで良い肝となる部分かもしれない。利用する機能をインターフェイス3として定義し、Baseがこのインターフェイスをフィールドとして保持するような構造にしてあげる。そして拡張実装のstructがこのインターフェイスを実装する。

このフィールドを通して呼ぶことで、デフォルト実装側から拡張実装側の処理が呼べるようになる。

type Base struct {
    manager Manager
}

func (b *Base) CreateTable(name string) {
    // BaseからXのPrefix()が呼べる
    fmt.Printf("%-5s\t%-10s\t%s.%s\n", "in Base", "create table", b.manager.Prefix(), name)
}

type X struct {
    Base
    prefix string // prefixを追加した
}

func (x *X) Prefix() string {
    return x.prefix
}

実はこの種の実装パターンを一度記事にした事があった。

リンク先の記事はフィールドの型が特定のstructだったがこちらの定義ではインターフェイスになっている。そういう意味ではリンク先の記事の応用と言えるかもしれない。

拡張実装(X)はデフォルト実装(Base)の定義を上書きできる

こちら素直。埋め込んだ型(Base)と同名のメソッドを定義すれば良いだけ。

func (b *Base) TypeOf(ob interface{}) string {
    return "Unknown"
}

func (x *X) TypeOf(ob interface{}) string {
    // Baseで定義したTypeOfを上書きできる(分岐を増やせる)
    switch reflect.TypeOf(ob).Kind() {
    case reflect.Int:
        return "Integer"
    case reflect.String:
        return "String"
    default:
        return x.Base.TypeOf(ob)
    }
}

はい。

実装例

全部つなげたコード例は以下の様になる。

package main

import (
    "fmt"
    "reflect"
)

type Manager interface {
    CreateTable(name string)
    DropTable(name string)
    RefreshTable(name string)

    Prefix() string

    TypeOf(ob interface{}) string
}

type Base struct {
    manager Manager
}

func (b *Base) CreateTable(name string) {
    // BaseからXのPrefix()が呼べる
    fmt.Printf("%-5s\t%-10s\t%s.%s\n", "in Base", "create table", b.manager.Prefix(), name)
}

func (b *Base) TypeOf(ob interface{}) string {
    return "Unknown"
}

type X struct {
    Base
    prefix string
}

func (x *X) TypeOf(ob interface{}) string {
    // Baseで定義したTypeOfを上書きできる(分岐を増やせる)
    switch reflect.TypeOf(ob).Kind() {
    case reflect.Int:
        return "Integer"
    case reflect.String:
        return "String"
    default:
        return x.Base.TypeOf(ob)
    }
}

func (x *X) RefreshTable(name string) {
    fmt.Println("refresh..")
    x.DropTable(name)   // こちらはXで定義した実装
    x.CreateTable(name) // こちらはBaseで定義したdefault実装
}

func (x *X) DropTable(name string) {
    fmt.Printf("%-5s\t%-10s\t%s.%s\n", "in X", "drop table", x.Prefix(), name)
}
func (x *X) Prefix() string {
    return x.prefix
}

func NewXManager(prefix string) Manager {
    x := &X{prefix: prefix}
    x.Base.manager = x
    return x
}

func main() {
    x := NewXManager("foo")
    x.RefreshTable("Target") // Manager.RefreshTable()

    // xで拡張されたTypeOfが呼べる
    fmt.Println("----------------------------------------")
    fmt.Println("10 is", x.TypeOf(10))
    fmt.Println("foo is", x.TypeOf("foo"))
    fmt.Println("0.1234 is", x.TypeOf(0.1234))
}

実行結果

refresh..
in X    drop table  foo.Target
in Base create table    foo.Target
----------------------------------------
10 is Integer
foo is String
0.1234 is Unknown

実際の利用例

xormは以下の様な感じでの実際に利用している。

gist


  1. この名前は良い名前ではない。ひどく微妙な名前。

  2. privateなhelperと見做しても良いかもしれない。

  3. この記事においてはManager