Go言語のinterface:効果的な機能設計とユースケース

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は便利だなぁと思った