私にとって最も楽しい囲conceptsの概念の1つは、インターフェイスを構成する機能です。 この記事では、この言語の機能を使用した小さな例を分析します。 これを行うには、2つの構造がユーザーデータを処理し、http要求を実行する仮想シナリオを想像してください。
type (
type (
Syncおよび
Store構造は、システム内のユーザーとの操作を担当します。 HTTP要求を実行するには、
HTTPClientインターフェイスに準拠した構造を渡す必要があります。 彼は次のとおりです。
type (
したがって、2つの構造があり、それぞれが1つのことを行い、それをうまく行います。両方とも1つの引数インターフェイスのみに依存します。
HTTPClientインターフェースのスタブを作成するだけなので、テストしやすいコードのように見えます。
同期の単体テストは、次のように実装できます。
func TestUserSync(t *testing.T) { client := new(HTTPClientMock) client.PostFunc = func(url, contentType string, body io.Reader) (*http.Response, error) {
このテストはうまく機能しますが、
Syncは
HTTPClientインターフェースの
Getメソッドを使用しないという事実に注意する必要があります
。クライアントは、使用しないメソッドに依存しないでください。 ロバート・マーティン
また、新しいメソッドを
HTTPClientに追加する場合は、そのメソッドを
HTTPClientMockスタブに追加する必要があります。これにより、コードの可読性が低下し、テストが複雑になります。
Getメソッドのシグネチャを変更するだけでも、このメソッドが使用されていなくても、
Sync構造のテストに影響を及ぼします。 このような依存関係は排除する必要があります。
この例では、
HTTPClientインターフェースをスタブするために2つのメソッドのみを実装する必要があります。 しかし、仮想ハンドラーがキューからメッセージを受信し、データベースに保存する必要があると想像してください。
type ( AMQPHandler struct { repository Repository } Repository interface { Add(user *User) error FindByID(ID string) (*User, error) FindByEmail(email string) (*User, error) FindByCountry(country string) (*User, error) FindByEmailAndCountry(country string) (*User, error) Search(...CriteriaOption) ([]*User, error) Remove(ID string) error
ユーザーデータを
AMQPHandlerデータベースに保存するには、
Addメソッドのみが必要
ですが、おそらくご
想像のとおり 、テスト用の
リポジトリインターフェイスのスタブは脅威に見えます。
type ( RepositoryMock struct { AddInvoked bool } ) func (r *Repository) Add(u *User) error { r.AddInvoked = true return nil } func (r *Repository) FindByID(ID string) (*User, error) { return nil } func (r *Repository) FindByEmail(email string) (*User, error) { return nil } func (r *Repository) FindByCountry(country string) (*User, error) { return nil } func (r *Repository) FindByEmailAndCountry(email, country string) (*User, error) { return nil } func (r *Repository) Search(...CriteriaOption) ([]*User, error) { return nil, nil } func (r *Repository) Remove(ID string) error { return nil }
アプリケーション設計における同様のエラーのため、毎回
Repositoryインターフェースのすべてのメソッドを実装する方法は他にありません。 しかし、Goの哲学によれば、インターフェイスは原則として小さく、1つまたは2つのメソッドで構成する必要があります。 この観点から、
リポジトリの実装は完全に冗長なようです。
インターフェイスが大きいほど、抽象化は弱くなります。 ロブ・パイク
ユーザー管理コードに戻りましょう
。Postメソッドと
Getメソッドはどちらもデータの保存(
Store )にのみ必要であり、同期には
Postメソッドだけで十分です。 これを念頭に置いて、
同期の実装を修正しましょう。
type (
func TestUserSync(t *testing.T) { client := new(HTTPPosterMock) client.PostFunc = func(url, contentType string, body io.Reader) (*http.Response, error) {
冗長な
HTTPClientインターフェースを扱う必要がなくなりました。このアプローチにより、テストが簡素化され、不要な依存関係が回避されます。
また 、
NewSyncコンストラクターの引数の目的がより明確になりました。
HTTPClientの両方のメソッドを使用して、
Storeのテストがどのようになるかを見てみましょう。
func TestUserStore(t *testing.T) { client := new(HTTPClientMock) client.PostFunc = func(url, contentType string, body io.Reader) (*http.Response, error) {
正直なところ、私はそのようなアプローチを発明しませんでした。 これはGo標準ライブラリで見ることができます
。io.ReadWriterはインターフェイス構成の原理をよく示しています。
type ReadWriter interface { Reader Writer }
インターフェイスをこのように編成すると、コード内の依存関係がより明確になります。
賢明な読者は、おそらく私の例でTDDのヒントを見つけたでしょう。 実際、単体テストなしでは、最初の試行でそのような設計を達成することは困難です。 また、テストに外部依存関係がないことにも注目する価値があります。このアプローチは、
Ben Johnsonが調査したものです。
おそらく、
HTTPClientの実装がどのように見えるか興味がありますか?
type (
シンプルでシンプル
-Postと
Getのメソッドを実装するだけ
です 。 コンストラクターはインターフェイスと特定の型を返さないことに注意してください;このアプローチはGoで
推奨されます。 また、
HTTPClientを使用するコンシューマパッケージでインターフェイスを宣言する必要があります。 この場合、
ユーザーパッケージを呼び出すことができます。
type (
そして最後に、
main.goにすべてをまとめ
ます func main() { req := new(httpclient.Request) client := httpclient.New(req) _ = user.NewSync(client) _ = user.NewStore(client)
この例が
、インターフェイス分離の
原則を使用して、テストしやすく明示的な依存関係を持つより慣用的なGoコードを作成するのに役立つことを願っています。 次の記事では、
フェイルオーバーロジックと
再送信を
HTTPClientに追加し、接続を維持します。
サンプルを
実装するための完全なソースコード。
この記事をレビューしてくれた友人の
バスティアンと
フェリペに感謝します。