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"
)
type OperatorClient struct {
baseUrl string
client *http.Client
}
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{}}
}
type AdminClient struct {
baseUrl string
client *http.Client
}
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"}`))
}
一気に長くなったが、機能設計と突合しやすくなり、拡張性も増え、プログラマ依存にならないでoperatorとadminの操作権限を付与ができるコードとなった。
しかし問題点はまだある。
現状では各クライアントがhttp.Clientを直接持っているため単体テストする際に対向のサーバーが必要になってしまう。
そこでintarfaceですよ
以下のようにintarfaceを用いると単体テストする際にintarfaceのmockが設定可能となり対向のサーバーが不要なコードとなる。
package client
import (
"fmt"
"io"
"net/http"
)
type httpClient interface {
Get(url string) (resp *http.Response, err error)
Post(url string, contentType string, body io.Reader) (resp *http.Response, err error)
}
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{}}
}
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は便利だなぁと思った