体はドクペで出来ている

インフラ、Goの割合が多い技術ブログ

Golangでインターフェースを使いコードを疎結合にする

はじめに

Golangにはインターフェース ( interface ) という型があり、これを使ってコードの抽象化・実装の隠蔽を行うことができます。上手く使うと単体テストや実装の分離でとても便利なのですが、私もそうだったように初学者にとっては中々理解しづらい概念だと思うので自分の再学習も兼ねてここに記したいと思います。

ちなみに筆者はGolang以外の言語はあまりちゃんと勉強したことがないので、他の言語との比較についてはここでは一切触れずGolangのインターフェースではという視点でのみ書きます。

インターフェースとは何なのかを簡潔に

インターフェースは何か特別な機能というわけでなく、あくまで intstruct 等と同じ仲間である「型」です。そして中身はメソッドの定義で、以下のような書き方をします。

type Taiyaki interface {
    GetNakami() string       // GetNakami は鯛焼きの中身を調べる
    SetNakami(nakami string) // SetNakami は鯛焼きの中身をセットする
}

見てわかるとおり struct の宣言とあまり変わりありませんが、中身がフィールドではなくメソッドのリストになっています。

インターフェースの使い方

ここでは上記で宣言した Taiyaki 型を例に実装と使い方の例を見ていきます。

インターフェースには実体が必要

まず使い方の第一歩として、インターフェースには実体が必要です。インターフェースはあくまでメソッドを定義するだけでその中身が何なのか、どのような実装なのかについては一切関知しません。下記のように実体が無いインターフェースを使おうとするとpanicが発生します。

type Taiyaki interface {
    GetNakami() string
    SetNakami(nakami string)
}

func main() {
    var t Taiyaki
    t.SetNakami("あんこ")
    fmt.Println(t.GetNakami())
}

https://play.golang.org/p/gIsLZspZIMM

インターフェースに実体を作る

インターフェースの実体は、典型的な形では下記のように構造体のレシーバーとメソッドを定義することで実装することができます(他にもインターフェース型、プリミティブ型以外であれば実装することができますが今回は扱いません)。

func (t *normalTaiyaki) GetNakami() string {
    return t.Nakami
}

func (t *normalTaiyaki) SetNakami(nakami string) {
    t.Nakami = nakami
}

func main() {
    var t normalTaiyaki
    t.SetNakami("あんこ")
    fmt.Println(t.GetNakami())
}

https://play.golang.org/p/0ip9e__LDdq

なお本筋から逸れますが SetNakami(nakami string) のような所謂破壊的メソッドを実装する場合はレシーバー( t )の型をポインター型にしておかないと意図しない動作になりますのでご注意下さい。参考資料 一つでも破壊的メソッドがある場合は全てのレシーバーをポインター型にしてしまうのが間違いが無くて良いと思います。

メリット1: 異なる実体を同じように扱える

ここまでの紹介だとインターフェースを使うメリットは全くありませんので、これからメリットがわかる例を紹介していきたいと思います。

Golangではインターフェースで定義されたメソッドを全て実装すると、特段宣言をせずともそのインターフェースを満たしたものと見做されます(ダックタイピング)。これを利用しインターフェースを経由することで異なる実体の型を全く同じように扱うことができます。引き続き鯛焼き Taiyaki を例にすると以下のようなコードで実証することができます。

type Taiyaki interface {
    GetNakami() string
    SetNakami(nakami string)
}

type normalTaiyaki struct {
    Nakami string
}

func NewNormalTaiyaki() Taiyaki {
    return &normalTaiyaki{}
}

func (t *normalTaiyaki) GetNakami() string {
    return t.Nakami
}

func (t *normalTaiyaki) SetNakami(nakami string) {
    t.Nakami = nakami
}

type whiteTaiyaki struct {
    Nakami string
}

func NewWhiteTaiyaki() Taiyaki {
    return &whiteTaiyaki{}
}

func (t *whiteTaiyaki) GetNakami() string {
    return t.Nakami
}

func (t *whiteTaiyaki) SetNakami(nakami string) {
    t.Nakami = nakami
}

func main() {
    t1 := NewNormalTaiyaki()
    t2 := NewWhiteTaiyaki()

    makeTaiyaki(t1, "あんこ")
    makeTaiyaki(t2, "クリーム")

    fmt.Println(t1.GetNakami())
    fmt.Println(t2.GetNakami())
}

func makeTaiyaki(t Taiyaki, nakami string) Taiyaki {
    t.SetNakami(nakami)
    return t
}

https://play.golang.org/p/YP2n0b-l6IP

少々長く見づらくてて申し訳ないのですが、重要なところは makeTaiyaki(t Taiyaki) 関数で鯛焼きインターフェース( Taiyaki 型)を受け取っていることにより、普通の鯛焼きnormalTaiyaki 型)であろうと白い鯛焼きwhiteTaiyaki 型)であろうと同じく中身をセットできており、つまり同じコードで2つの実体を扱うことができている点です。もしこれがインターフェースではなく具体的な構造体(つまり normalTaiyaki 構造体や whiteTaiyaki 構造体そのもの)を受け取っていると、それぞれの型で2つの makeTaiyaki() の定義が必要になってしまいます。

確認のため敢えて二度同じことを書きますが、なぜ makeTaiyaki(t Taiyaki)normalTaiyaki 型と whiteTaiyaki 型の両方が受け取れるのかというと、両方の構造体で Taiyaki 型の実装( GetNakami() stringSetNakami(nakami string) )を満たしているからです。今後例えば黒い鯛焼きblackTaiyaki 型)を追加したとしても Taiyaki 型のインターフェースで必要なメソッドを全て実装している限り、やはり同じように一切の修正無く makeTaiyaki(t Taiyaki) に放り込むことができます。

メリット2: 実装を隠蔽する

これまでのコードでは単一のパッケージで実装していたため、やろうと思えば特定の型に依存したコードが書けてしまいます(例えば whiteTaiyaki.Nakami = "クリーム" というような書き方)。これではコードが密結合になってしまい、単体テストが書きづらくなったり、実装の変更時に影響範囲の把握が困難になったりして後々非常に苦労することになってしまいます。

ところで、Golangではパッケージを境界として関数や構造体の公開・非公開を制御することができます(Golangの基本:頭文字が大文字であればパッケージ外へ公開、小文字であれば非公開)。これを利用しインターフェースの実装部分のみを外部公開することで中身の構造体や内部処理に使う関数を隠蔽し特定の型・パッケージに強く依存したコードを書けなくすることができます。

ここからはインラインのコードではパッケージ構成が理解しづらくなってきますのでこちらのリポジトリを参照しながら確認して下さい。

main

package main

import (
    "fmt"

    "github.com/ryo-yamaoka/go-taiyaki/model"
    "github.com/ryo-yamaoka/go-taiyaki/normal"
    "github.com/ryo-yamaoka/go-taiyaki/white"
)

func main() {
    t1 := normal.NewNormalTaiyaki()
    t2 := white.NewWhiteTaiyaki()

    makeTaiyaki(t1, "あんこ")
    makeTaiyaki(t2, "クリーム")

    fmt.Println(t1.GetNakami())
    fmt.Println(t2.GetNakami())
}

func makeTaiyaki(t model.Taiyaki, nakami string) model.Taiyaki {
    t.SetNakami(nakami)
    return t
}

model

package model

type Taiyaki interface {
    GetNakami() string
    SetNakami(nakami string)
}

normal

package normal

import "github.com/ryo-yamaoka/go-taiyaki/model"

type normalTaiyaki struct {
    Nakami string
}

func NewNormalTaiyaki() model.Taiyaki {
    return &normalTaiyaki{}
}

func (t *normalTaiyaki) GetNakami() string {
    return t.Nakami
}

func (t *normalTaiyaki) SetNakami(nakami string) {
    t.Nakami = nakami
}

white

package white

import "github.com/ryo-yamaoka/go-taiyaki/model"

type whiteTaiyaki struct {
    Nakami string
}

func NewWhiteTaiyaki() model.Taiyaki {
    return &whiteTaiyaki{}
}

func (t *whiteTaiyaki) GetNakami() string {
    return t.Nakami
}

func (t *whiteTaiyaki) SetNakami(nakami string) {
    t.Nakami = nakami
}

ここでのポイントはパッケージ normal 及び white では中の構造体は公開せず、構造体のメソッドとコンストラクターNew*Taiyaki() 関数)のみをパッケージ外に公開している点です。この形を取ることによりmainパッケージ側からはインターフェースを経由した操作しかすることができないため特定の実装に依存したコードを排除することができるようになっています。また今回のコードにはありませんが、ヘルパー関数等も同様です。

メリット3: モックを作ることができる

更に応用として単体テスト時にインターフェースを利用したモック動作を行う方法も紹介します。これを実務で使いそうなケースを挙げると、実動作時にはDBへ問い合わせを行うがテスト時はDB無しで固定の値を返却する、というモックの振る舞いをして欲しい場合が典型的でしょう。

main_test.go

package main

import "testing"

type testTaiyaki struct {
    Nakami string
}

func (t *testTaiyaki) GetNakami() string {
    return t.Nakami
}

func (t *testTaiyaki) SetNakami(nakami string) {
    t.Nakami = nakami
}

func TestMain(t *testing.T) {
    patterns := []struct {
        nakami string
    }{
        {nakami: "あんこ"},
        {nakami: "クリーム"},
        {nakami: "いちごジャム"},
        {nakami: "謎ジャム"},
    }

    for i, pattern := range patterns {
        taiyaki := &testTaiyaki{}
        makeTaiyaki(taiyaki, pattern.nakami)
        if taiyaki.GetNakami() != pattern.nakami {
            t.Errorf("unexpected nakami(%d): %s != %s", i, taiyaki.GetNakami(), pattern.nakami)
        }
    }
}

ポイントはテストファイル内で新たに testTaiyaki 型を作成し Taiyaki インターフェースのメソッドを実装している点です。こういう形を取ることによりテスト時だけ挙動が異なる実装を用意することができるため単体テストを非常にやりやすくすることができます。

以下はコードを実行した結果とテストを実行した時の様子です。

$ go run main.go 
あんこ
クリーム
$ 
$ 
$ go test -v ./...
=== RUN   TestMain
--- PASS: TestMain (0.00s)
PASS
ok      github.com/ryo-yamaoka/go-taiyaki   (cached)
?       github.com/ryo-yamaoka/go-taiyaki/model [no test files]
?       github.com/ryo-yamaoka/go-taiyaki/normal    [no test files]
?       github.com/ryo-yamaoka/go-taiyaki/white [no test files]

まとめ

  • インターフェースを使うとコードの共通化ができるよ!
  • インターフェースを使うと実装を隠蔽して疎結合なコードを強制することができるよ!
  • インターフェースを使うと単体テストでモックを作れるよ!

インターフェースの使い方は勿論これだけではなく他にも色々便利なことができますので、追々気が向いたら記事にしたいと思います。

また最後にとても重要なことですが、鯛焼きを購入した際にうっかり所持金が足りなかった場合でも走って逃げてはいけません。身分を明かし事情を説明した上で誠意を持った謝罪と可及的速やかな代金の支払いをしましょう。