お題
表題の通り。
Goに限らずだけど、アプリがDB接続、外部APIとの通信といったロジックを含む場合、実際に接続可能な環境で実行するする分にはよいけど、ローカル環境やテストコードにおいてはネックになる。
こうした外部依存部分を必要に応じてモック等に切り替えられるようにする仕組みを考えたい。
まあ、考えたいといっても、こうした考え自体は「依存性注入(DependencyInjection)」という形で解決するものだと既に前例がある。
Javaのようなエンタープライズな世界で動くものについては、フレームワークがよしなに提供してくれるのだけど、
Goではデファクトスタンダードなやり方がはっきりと決まっているとは言えない(と思う)。
ので、ちょっと考えてみた。
開発環境
# OS
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="17.10 (Artful Aardvark)"
# Golang
$ go version
go version go1.11.2 linux/amd64
バージョンの切り替えはgoenvで行っている。
実践
ソースの全量は↓
https://github.com/sky0621/go-di/tree/c7311d818fe8c7cd9ef26d14fdf862336124dee5
ツリー
$ tree
.
├── README.md
├── cmd
│ └── main.go
├── container.go
├── infrastructure
│ ├── cloudpubsub.go
│ └── cloudsql.go
└── otherpackage
└── logic.go
DIコンテナ
外部依存するロジック(Factoryという名前を付けた関数型で定義)をDIコンテナという箱に詰めて管理する。
type DIContainer struct {
// 要素追加はmain関数のみで、あとは要素取得のみのため Mutex は使わない
container map[StoreKey]Factory
}
コンテナに格納するキーは、元は int 型の独自タイプ。
今回は事例として「CloudSQL」や「CloudPubSub」へのアクセス部分を外部依存ロジックと捉えている。
// StoreKey ... コンテナ格納時のキー
type StoreKey int
const (
CloudSQLAccessor StoreKey = iota
CloudPubSubAccessor
)
Factoryは、DIコンテナを受け取って、そこから(外部依存)アクセッサを抜き出す関数。
type Factory func(c *DIContainer) Accessor
func CloudSQLAccessorFactory(c *DIContainer) Accessor {
// CloudSQLへのアクセッサならではのロジックがあれば、ここで。
// AccessorをAbstractFactory形式にして、階層化された依存構造を生成して返すロジックにしてもよいかも。
return &infrastructure.CloudSQLAccessor{}
}
func CloudPubSubAccessorFactory(c *DIContainer) Accessor {
return &infrastructure.CloudPubSubAccessor{}
}
アクセッサには単に外部依存アクセッサを束ねるためだけのインタフェースを定義。
Javaならマーカーインタフェースでよかったけど、Goはダックタイプなので(たぶん)振る舞いを定義しないとダメ。(たぶん)
type Accessor interface {
Duck() // マーカーインタフェース用途のため本来は関数不要だが、ダックタイプ形式のため適当な関数を定義
}
さて、DIコンテナには格納用と取得用それぞれのメソッドを持たせておく。
格納用はmain関数(厳密にはinit関数内)で使う。
// RegistFactory ... main関数でのみ呼ばれることを想定しているが、それを強制しているわけではないので堅牢とは言えない
func (c *DIContainer) RegistFactory(k StoreKey, f Factory) {
c.container[k] = f
}
func (c *DIContainer) GetAccessor(k StoreKey) Accessor {
f := c.container[k]
return f(c)
}
main関数
DIコンテナのセットアップをして、(とりあえず適当に定義した)Logic
関数の引数として渡す。
// プログラム内のどこからでもアクセスできるDIコンテナとしてもよかったが、呼び出し関係の都合上、cyclicインポートを防ぐため引数での引き回しを採用
// mainパッケージに置くべきかも再考
var dicon *app.DIContainer
func init() {
// テストコードでは、ここをモックに差し替えることで冪等性を担保したコードにする
dicon = app.NewDIContainer()
dicon.RegistFactory(app.CloudSQLAccessor, app.CloudSQLAccessorFactory)
dicon.RegistFactory(app.CloudPubSubAccessor, app.CloudPubSubAccessorFactory)
}
func main() {
otherpackage.Logic(dicon)
}
Logic関数
func Logic(dicon *app.DIContainer) {
cloudSQLAccessor := dicon.GetAccessor(app.CloudSQLAccessor)
cloudSQLAccessor.Duck()
cloudPubSubAccessor := dicon.GetAccessor(app.CloudPubSubAccessor)
cloudPubSubAccessor.Duck()
}
各アクセッサ実装
type CloudSQLAccessor struct{}
func (c *CloudSQLAccessor) Duck() {
fmt.Println("CloudSQL!")
}
type CloudPubSubAccessor struct{}
func (p *CloudPubSubAccessor) Duck() {
fmt.Println("PubSub!")
}
実行結果
$ go run main.go
CloudSQL!
PubSub!
まとめ
挙動としてはCloudSQLアクセッサ、CloudPubSubアクセッサそれぞれのDuck()
実行をもって標準出力されたので期待通り。
ただ、テストコード書いて、実際にモック差し替えがうまく機能するかは未確認。
また、今回くらいのコード量ならともかく、レイヤード、ヘキサゴナル、クリーンアーキテクチャといったものと組み合わせても適合するかも不明。
やはり、ある程度の規模のアプリを作ってみないと真価(及び不備)は見つからなそう。。。