Go言語のinterfaceが便利という話
intarfaceについて
Go言語のインターフェースは、メソッドの集合を定義する抽象的な型。具体的なデータ型や実装の詳細に依存せず、メソッドのシグネチャ(メソッドの名前、引数、戻り値の型)のみを定義できる。 インターフェースは、異なる型の値に共通の振る舞いを持たせたい場合や、多様な型のコレクションを扱いたい場合に役立つ。
httpリクエストを行うクライアントを例にして考えてみる
Getのみが行えるOperatorとGetとPostが行えるAdminクライアントを作ることを例として考えてみる
機能設計として記載するなら以下のような感じ
- operatorはGetを行う権限を持つ
- adminはGet、Postを行う権限を持つ
とりあえずの実装
通常はそのままhttp.Clientを使用する形になる
package main import ( "bytes" "net/http" ) func main() { operator := http.Client{} operator.Get("http://example.com/data") admin := http.Client{} admin.Get("http://example.com/data") admin.Post("http://example.com/data", "application/json", bytes.NewBufferString(`{"name":"sample"}`)) }
変数名でoperatorとadminを分けられてはいるが、ぱっと見で機能設計と突合ができないものとなっている。また、各クライアントで機能を制限していないためプログラマが気を付けてコーディングする必要が出てくる。
改善Version
機能を制限したstructを用意して改善する
package client import ( "fmt" "io" "net/http" ) // operator クライアント type OperatorClient struct { baseUrl string client *http.Client } // operator クライアントはGetを可能にする func (c *OperatorClient) Get() (resp *http.Response, err error) { fmt.Printf("get to %s", c.baseUrl) return c.client.Get(c.baseUrl) } // operator クライアントを返却 func NewOperater(baseUrl string) *OperatorClient { return &OperatorClient{baseUrl, &http.Client{}} } // admin クライアント type AdminClient struct { baseUrl string client *http.Client } // admin クライアントはPostを可能にする func (c *AdminClient) Post(body io.Reader) (resp *http.Response, err error) { fmt.Printf("post to %s", c.baseUrl) return c.client.Post(c.baseUrl, "application/json", body) } // admin クライアントはGetを可能にする func (c *AdminClient) Get() (resp *http.Response, err error) { fmt.Printf("get to %s", c.baseUrl) return c.client.Get(c.baseUrl) } // admin クライアントを返却 func NewAdmin(baseUrl string) *AdminClient { return &AdminClient{baseUrl, &http.Client{}} }
package main import ( "bytes" "my-rest-api/cmd/client" ) func main() { operator := client.NewOperater("test") operator.Get() admin := client.NewAdmin("test") admin.Get() admin.Post(bytes.NewBufferString(`{"data":"test"}`)) }
一気に長くなったが、機能設計と突合しやすくなり、拡張性も増え、プログラマ依存にならないでoperatorとadminの操作権限を付与ができるコードとなった。
しかし問題点はまだある。
現状では各クライアントがhttp.Clientを直接持っているため単体テストする際に対向のサーバーが必要になってしまう。
そこでintarfaceですよ
以下のようにintarfaceを用いると単体テストする際にintarfaceのmockが設定可能となり対向のサーバーが不要なコードとなる。
package client import ( "fmt" "io" "net/http" ) // clientパッケージ内で利用するhttp.Clientの機能をintarfaceとして定義する type httpClient interface { Get(url string) (resp *http.Response, err error) Post(url string, contentType string, body io.Reader) (resp *http.Response, err error) } // Operator クライアント // clientの型をhttpClientに変更する type OperatorClient struct { baseUrl string client httpClient } func (c *OperatorClient) Get() (resp *http.Response, err error) { fmt.Printf("get to %s", c.baseUrl) return c.client.Get(c.baseUrl) } func NewOperater(baseUrl string) *OperatorClient { return &OperatorClient{baseUrl, &http.Client{}} } // Admin クライアント // clientの型をhttpClientに変更する type AdminClient struct { baseUrl string client httpClient } func (c *AdminClient) Post(body io.Reader) (resp *http.Response, err error) { fmt.Printf("post to %s", c.baseUrl) return c.client.Post(c.baseUrl, "application/json", body) } func (c *AdminClient) Get() (resp *http.Response, err error) { fmt.Printf("get to %s", c.baseUrl) return c.client.Get(c.baseUrl) } func NewAdmin(baseUrl string) *AdminClient { return &AdminClient{baseUrl, &http.Client{}} }
package main import ( "bytes" "my-rest-api/cmd/client" ) func main() { operator := client.NewOperater("test") operator.Get() admin := client.NewAdmin("test") admin.Get() admin.Post(bytes.NewBufferString(`{"data":"test"}`)) }
というわけでintarfaceは便利だなぁと思った