Goのインターフェイス構成

私にとって最も楽しい囲conceptsの概念の1つは、インターフェイスを構成する機能です。 この記事では、この言語の機能を使用した小さな例を分析します。 これを行うには、2つの構造がユーザーデータを処理し、http要求を実行する仮想シナリオを想像してください。



type ( //      Sync struct { client HTTPClient } ) //    Sync func NewSync(hc HTTPClient) *Sync { return &Sync{hc} } //        func (s *Sync) Sync(user *User) error { res, err := s.client.Post(syncURL, "application/json", body) //   res  err return err }
      
      





 type ( //       Store struct { client HTTPClient } ) //    Store func NewStore(hc HTTPClient) *Store { return &Store{hc} } //       func (s *Store) Store(user *User) error { res, err := s.client.Get(userResource) //   res  err res, err = s.client.Post(usersURL, "application/json", body) //   res  err return err }
      
      





SyncおよびStore構造は、システム内のユーザーとの操作を担当します。 HTTP要求を実行するには、 HTTPClientインターフェイスに準拠した構造を渡す必要があります。 彼は次のとおりです。



 type ( //   http    HTTPClient interface { //  POST- Post(url, contentType string, body io.Reader) (*http.Response, error) //  GET- Get(url string) (*http.Response, error) } )
      
      





したがって、2つの構造があり、それぞれが1つのことを行い、それをうまく行います。両方とも1つの引数インターフェイスのみに依存します。 HTTPClientインターフェースのスタブを作成するだけなので、テストしやすいコードのように見えます。 同期の単体テストは、次のように実装できます。



 func TestUserSync(t *testing.T) { client := new(HTTPClientMock) client.PostFunc = func(url, contentType string, body io.Reader) (*http.Response, error) { // check if args are the expected return &http.Response{StatusCode: http.StatusOK}, nil } syncer := NewSync(client) u := NewUser("foo@mail.com", "de") if err := syncer.Sync(u); err != nil { t.Fatalf("failed to sync user: %v", err) } if !client.PostInvoked { t.Fatal("expected client.Post() to be invoked") } } type ( HTTPClientMock struct { PostInvoked bool PostFunc func(url, contentType string, body io.Reader) (*http.Response, error) GetInvoked bool GetFunc func(url string) (*http.Response, error) } ) func (m *HTTPClientMock) Post(url, contentType string, body io.Reader) (*http.Response, error) { m.PostInvoked = true return m.PostFunc(url, contentType, body) } func (m *HTTPClientMock) Get(url string) (*http.Response, error) { return nil, nil}
      
      





このテストはうまく機能しますが、 SyncHTTPClientインターフェースの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 //   //   //   // ... } ) func NewAMQPHandler(r Repository) *AMQPHandler { return &AMQPHandler{r} } func (h *AMQPHandler) Handle(body []byte) error { //   if err := h.repository.Add(user); err != nil { return err } return nil }
      
      





ユーザーデータを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 ( //      Sync struct { client HTTPPoster } ) //    Sync func NewSync(hc HTTPPoster) *Sync { return &Sync{hc} } //        func (s *Sync) Sync(user *User) error { res, err := s.client.Post(syncURL, "application/json", body) //   res  err return err }
      
      





 func TestUserSync(t *testing.T) { client := new(HTTPPosterMock) client.PostFunc = func(url, contentType string, body io.Reader) (*http.Response, error) { // assert the arguments are the expected return &http.Response{StatusCode: http.StatusOK}, nil } syncer := NewSync(client) u := NewUser("foo@mail.com", "de") if err := syncer.Sync(u); err != nil { t.Fatalf("failed to sync user: %v", err) } if !client.PostInvoked { t.Fatal("expected client.Post() to be invoked") } } type ( HTTPPosterMock struct { PostInvoked bool PostFunc func(url, contentType string, body io.Reader) (*http.Response, error) } ) func (m *HTTPPosterMock) Post(url, contentType string, body io.Reader) (*http.Response, error) { m.PostInvoked = true return m.PostFunc(url, contentType, body) }
      
      





冗長なHTTPClientインターフェースを扱う必要がなくなりました。このアプローチにより、テストが簡素化され、不要な依存関係が回避されます。 またNewSyncコンストラクターの引数の目的がより明確になりました。



HTTPClientの両方のメソッドを使用して、 Storeのテストがどのようになるかを見てみましょう。



 func TestUserStore(t *testing.T) { client := new(HTTPClientMock) client.PostFunc = func(url, contentType string, body io.Reader) (*http.Response, error) { // assertion omitted return &http.Response{StatusCode: http.StatusOK}, nil } client.GetFunc = func(url string) (*http.Response, error) { // assertion omitted return &http.Response{StatusCode: http.StatusOK}, nil } storer := NewStore(client) u := NewUser("foo@mail.com", "de") if err := storer.Store(u); err != nil { t.Fatalf("failed to store user: %v", err) } if !client.PostInvoked { t.Fatal("expected client.Post() to be invoked") } if !client.GetInvoked { t.Fatal("expected client.Get() to be invoked") } } type ( HTTPClientMock struct { HTTPPosterMock HTTPGetterMock } HTTPPosterMock struct { PostInvoked bool PostFunc func(url, contentType string, body io.Reader) (*http.Response, error) } HTTPGetterMock struct { GetInvoked bool GetFunc func(url string) (*http.Response, error) } ) func (m *HTTPPosterMock) Post(url, contentType string, body io.Reader) (*http.Response, error) { m.PostInvoked = true return m.PostFunc(url, contentType, body) } func (m *HTTPGetterMock) Get(url string) (*http.Response, error) { m.GetInvoked = true return m.GetFunc(url) }
      
      





正直なところ、私はそのようなアプローチを発明しませんでした。 これはGo標準ライブラリで見ることができます。io.ReadWriterはインターフェイス構成の原理をよく示しています。



 type ReadWriter interface { Reader Writer }
      
      





インターフェイスをこのように編成すると、コード内の依存関係がより明確になります。



賢明な読者は、おそらく私の例でTDDのヒントを見つけたでしょう。 実際、単体テストなしでは、最初の試行でそのような設計を達成することは困難です。 また、テストに外部依存関係がないことにも注目する価値があります。このアプローチは、 Ben Johnsonが調査したものです。



おそらく、 HTTPClientの実装がどのように見えるか興味がありますか?



 type ( //   http- HTTPClient struct { req *Request } //    http- Request struct{} ) //   HTTPClient func New(r *Request) *HTTPClient { return &HTTPClient{r} } //  Get- func (c *HTTPClient) Get(url string) (*http.Response, error) { return c.req.Do(http.MethodGet, url, "application/json", nil) } //  Post- func (c *HTTPClient) Post(url, contentType string, body io.Reader) (*http.Response, error) { return c.req.Do(http.MethodPost, url, contentType, body) } //  http- func (r *Request) Do(method, url, contentType string, body io.Reader) (*http.Response, error) { req, err := http.NewRequest(method, url, body) if err != nil { return nil, fmt.Errorf("failed to create request %v: ", err) } req.Header.Set("Content-Type", contentType) return http.DefaultClient.Do(req) }
      
      





シンプルでシンプル-PostGetのメソッドを実装するだけです 。 コンストラクターはインターフェイスと特定の型を返さないことに注意してください;このアプローチはGoで推奨されます。 また、 HTTPClientを使用するコンシューマパッケージでインターフェイスを宣言する必要があります。 この場合、 ユーザーパッケージを呼び出すことができます。



 type ( //      User struct { Email string `json:"email"` Country string `json:"country"` } //   HTTPClient interface { HTTPGetter HTTPPoster } //   Post- HTTPPoster interface { Post(url, contentType string, body io.Reader) (*http.Response, error) } //   Get- HTTPGetter interface { Get(url string) (*http.Response, error) } )
      
      





そして最後に、 main.goにすべてをまとめます



 func main() { req := new(httpclient.Request) client := httpclient.New(req) _ = user.NewSync(client) _ = user.NewStore(client) //   Sync  Store }
      
      





この例が、インターフェイス分離原則を使用して、テストしやすく明示的な依存関係を持つより慣用的なGoコードを作成するのに役立つことを願っています。 次の記事では、 フェイルオーバーロジックと再送信HTTPClientに追加し、接続を維持します。



サンプル実装するための完全なソースコード。



この記事をレビューしてくれた友人のバスティアンフェリペに感謝します。



All Articles