Goにおける依存性の分離
Goの例に移る前に、依存性の分離に必要な3要素をもう一度思い出してください。多態性、依存性の注入、モック実装の自動生成でしたね。これらはGoにおいてどのように実現されるでしょうか?
まず多態性ですが、Goにおいて多態性を実現できる要素は、インターフェースと関数だけです。インターフェースとは、メソッドのシグネチャの集合です。指定されたシグネチャのメソッドを実装していれば、どのような構造体でも同じインターフェースの型として扱うことができます。また、Goでは関数を値として扱うことができ、関数の型は、その引数と返り値の型によって決まります。
次にモック実装の生成ですが、関数のモックを自動生成するツールは、私の知る限り現時点ではありません。インターフェースのモックであれば、mockgenというツールを使用して、静的に生成することができます。つまり、mockgenはインターフェース定義を読み取って、そのインターフェースを実装したモックのソースファイルを出力します。テスト時には出力されたソースファイルをコンパイルに含めることになります。
最後に注入の方法ですが、これは比較的自由です。構造体のメンバーに持たせてオブジェクト指向の真似事をすることもできるし、関数の引数に渡してもいいでしょう。しかし、関数の引数に渡す場合は、依存性が多くなってくると呼び出しの記述が冗長になってきます。パッケージや構造体の全ての関数・メソッドに依存性を渡さなければならないのは、大規模なアプリケーションでは避けたいことです。それよりも、構造体の生成時に一度だけ依存性を渡したほうが楽でしょう。
では、Goの例に移ります。Goの例でも、Javaの例と同じ構成のユニットの単体テストを書きます。こちらのサンプルコードも、GitHub上で公開されています。
まずLogicユニットのコードを以下に示します。このコードでは、Logicユニットの処理である最新のレポジトリを取得するメソッドを持つインターフェースLogicと、その実装であるlogicImplという構造体を定義しています。この構造体はその依存性であるGithubApiインターフェースをメンバーとして保持しています。
Logicインターフェースを用意したのは、Logicユニットに依存するユニットの存在を考慮してのことです。実体しか定義されていないと、多態性を持たせることができないので、Logicユニットに依存しているユニットのテストの際に不便です。名前からも分かるとおり、Logicインターフェースの実装であるlogicImplはエクスポートされていません。なぜエクスポートしないのでしょうか? 構造体をエクスポートしてしまったら、どのパッケージにあるユニットからもその構造体を自由に生成することができます。構造体のメンバーとして定義されている依存性をゼロ値のまま放っておいて、メソッドを呼び出すことすらできてしまいます。つまり、正しい初期化が行われる保証がないのです。そのような可能性をできるだけ減らすために、構造体自体はエクスポートしないで、その代わりNewLogicというファクトリ関数を用意しています。
この関数の他に、テスト時に使うDI用の関数newLogicDIを定義しています。テストをこのユニットと同じパッケージに書けば、logicImplを直接newできるため、この関数はなくても構いませんが、あれば設計意図がより明確になるでしょう。
type Logic interface {
LatestRepository(string) (*Repository, error)
}
type logicImpl struct {
githubApi GithubApi
}
func NewLogic() Logic {
return &logicImpl{
&GithubApiImpl{},
}
}
func newLogicDI(githubApi GithubApi) Logic {
return &logicImpl{
githubApi,
}
}
このような泥臭い工夫をしたとしても、Javaの時ほどスッキリとしません。なぜなら、Goのアクセス制御の単位はパッケージ単位だからです。同じパッケージ内からの誤った使用を防ぐことができないのです。Javaの場合でもリフレクションを使われてしまえばお終いですが、逆にいえば、リフレクションの使用というハードルを乗り越えなければ、設計者の意図に背くことはできません。しかしGoの場合、構造体を直接生成するのはファクトリ関数を呼び出すのと同じくらい簡単です。パッケージ内からの誤った使用を防ぐ障壁がないのです。それに、メンバーの書き換えもあまりにも簡単にできてしまいます。だからといって、構造体一つに対して対応するパッケージを作成したりしたいですか? ありえないですよね。パッケージの数が膨大になり、名前の衝突も多くなるでしょう。細かいアクセス制御はGoが犠牲にしたものの一つです。
続いてテストコードを見てみましょう。このテストコードでは、Go標準のテストパッケージtestingと、モック生成にmockgenを使っています。NewMockGithubApiという、モック実装を生成する関数を呼び出し、返り値をnewLogicDI関数に渡して、モック実装の注入を実現しています。モック実装の振る舞いは、モック実装のEXPECT()メソッドを使うことで設定できます。このとき、設定した振る舞いが呼び出されなかった場合はエラーとなってテストが失敗します。振る舞い設定と検証が同時に行われてしまうのです。
個人的に、これはかなり不適切な設計だと思っています。振る舞い設定と検証は別の目的を持った処理なので、インターフェースは別れるべきですし、テストコードを見ただけではモックとの相互作用が検証されているのかどうかも分かりづらいです。これはGoの問題というよりライブラリの問題なので、今後の改善に期待したいです。それ以外の点は、簡潔さというGoの利点が活かされているようなコードになっていますね。
func TestLogicImpl_LatestRepositoryOfOrganization(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockGithubApi := NewMockGithubApi(ctrl)
tested := newLogicDI(mockGithubApi)
...
mockGithubApi.EXPECT()
.GetRepositoriesOfOrganization(organizationName)
.Return(returned)
...
}
このGoの例では、インターフェースを使えば多態性を実現でき、モックの自動生成も行えること、パッケージ内からの誤った初期化を防ぐことができないことを示しました。これはまさにGoのコインの表と裏を如実に語っています。パッケージ単位でのアクセス制御や、エクスポートする/しないの二元論は確かに簡潔で理解しやすいのですが、細かい制御ができないということは、言語機能による設計意図の強制ができないということでもあるのです。つまり「やれるけれどやってはいけない」ことがあるのです。Goを用いて開発する場合には、テスト可能な設計という方法論への理解と、言語機能で強制しきれない箇所をカバーする意志力が求められると思います。
最後にこの記事のまとめをしましょう。
- 単体テストを行うにあたっては、各ユニットの依存性を分離しなければなりません。
- 依存性を分離するためには、多態性、依存性の注入、モック実装の自動生成の3つの要素を確保する必要があります。
- Goでは、インターフェースを使えば多態性とモック実装の自動生成が確保できます。依存性の注入は、構造体のメンバーを経由する方法と、関数の引数を経由する方法がありますが、個人的には構造体のメンバーを経由する方法を推奨します。
- Goでは細かいアクセス制御ができないので、依存性をメンバーに持つ構造体の初期化方法が設計意図通りかどうかを常に注視する必要があります。
Goで依存性の分離をもっと美しく効率的に行える方法をご存知でしたら、ぜひ共有してください。
