いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-60 (インターフェースをちゃんと実装しているか検証するTips)

概要

Qiitaに以下のとても勉強になる記事がありました。

qiita.com

なるほど、、。こんなやり方があるんですねー。勉強になりました。m( )m

IDEを使っていない環境でコード書いてる時とかに便利ですね。忘れないように自分用にメモメモ。。

上記の記事の中で参照として、uber の go style guide がリンクされていました。

github.com

Effective Goの方にも同じような記述がありますね。

golang.org

インターフェースと構造体を定義してみる

まず、インターフェースとそれを実装する構造体を定義します。サンプル用なので中身は適当です。

package command

import (
    "fmt"
    "path/filepath"
)

type (
    // Command は、サンプルの動作確認のためのインターフェースです.
    Command interface {
        Run() error
    }

    // ListFileCommand は、指定されたパターンのファイルを出力します.
    ListFileCommand struct {
        Dir     string
        Pattern string
    }
)

// Run は、パターンにマッチしたファイル名を出力します.
func (c *ListFileCommand) Run() error {
    matches, err := filepath.Glob(fmt.Sprintf("%s/%s", c.Dir, c.Pattern))
    if err != nil {
        return err
    }

    for _, v := range matches {
        fmt.Println(v)
    }

    return nil
}

今後、このインターフェースが拡張されたり、他の実装も増えてきたりしたときに

ちゃんと実装できているかを検証したい場合は、以下のようなダミーフィールドを

追加します。

package command

import (
    "fmt"
    "path/filepath"
)

type (
    // Command は、サンプルの動作確認のためのインターフェースです.
    Command interface {
        Run() error
    }

    // ListFileCommand は、指定されたパターンのファイルを出力します.
    ListFileCommand struct {
        Dir     string
        Pattern string
    }
)

// インターフェースを実装できていることを検証するためのダミーフィールド
//
// 構造体が特定のインターフェースを実装できているかを検証し続けたい場合は
// 以下のようにEmptyなフィールドを作ってインターフェースの型で設定するようにしておく
// ポインタレシーバーの場合は, ゼロ値が nil なので以下のようになる.
// ポインタレシーバー出ない場合は、 listFileCommand{} のように指定する
var _ Command = (*ListFileCommand)(nil)

// Run は、パターンにマッチしたファイル名を出力します.
func (c *ListFileCommand) Run() error {
    matches, err := filepath.Glob(fmt.Sprintf("%s/%s", c.Dir, c.Pattern))
    if err != nil {
        return err
    }

    for _, v := range matches {
        fmt.Println(v)
    }

    return nil
}

意図的にインターフェース型に代入するようにしておいて、キャスト出来なかったら実装できていないって分かるってことですね。

インターフェースを拡張してみる

実際にコンパイルエラーになるのかを試してみます。上のインターフェースに振る舞いを追加。

package command

import (
    "fmt"
    "path/filepath"
)

type (
    // Command は、サンプルの動作確認のためのインターフェースです.
    Command interface {
        Version() string
        Run() error
    }

    // ListFileCommand は、指定されたパターンのファイルを出力します.
    ListFileCommand struct {
        Dir     string
        Pattern string
    }
)

// インターフェースを実装できていることを検証するためのダミーフィールド
//
// 構造体が特定のインターフェースを実装できているかを検証し続けたい場合は
// 以下のようにEmptyなフィールドを作ってインターフェースの型で設定するようにしておく
// ポインタレシーバーの場合は, ゼロ値が nil なので以下のようになる.
// ポインタレシーバー出ない場合は、 ListFileCommand{} のように指定する
var _ Command = (*ListFileCommand)(nil)

// Run は、パターンにマッチしたファイル名を出力します.
func (c *ListFileCommand) Run() error {
    matches, err := filepath.Glob(fmt.Sprintf("%s/%s", c.Dir, c.Pattern))
    if err != nil {
        return err
    }

    for _, v := range matches {
        fmt.Println(v)
    }

    return nil
}

ビルドしてみると

$ make build
go build -o ./trygolang github.com/devlights/try-golang/cmd/trygolang
# github.com/devlights/try-golang/basic/interface_/command
basic/interface_/command/command.go:28:5: cannot use (*ListFileCommand)(nil) (type *ListFileCommand) as type Command in assignment:
        *ListFileCommand does not implement Command (missing Version method)
make: *** [Makefile:35: build] Error 2

ちゃんと実装が足りていないってメッセージが出ました。

てことで、実装を追加。

package command

import (
    "fmt"
    "path/filepath"
)

type (
    // Command は、サンプルの動作確認のためのインターフェースです.
    Command interface {
        Version() string
        Run() error
    }

    // ListFileCommand は、指定されたパターンのファイルを出力します.
    ListFileCommand struct {
        Dir     string
        Pattern string
    }
)

// インターフェースを実装できていることを検証するためのダミーフィールド
//
// 構造体が特定のインターフェースを実装できているかを検証し続けたい場合は
// 以下のようにEmptyなフィールドを作ってインターフェースの型で設定するようにしておく
// ポインタレシーバーの場合は, ゼロ値が nil なので以下のようになる.
// ポインタレシーバー出ない場合は、 ListFileCommand{} のように指定する
var _ Command = (*ListFileCommand)(nil)

// Run は、パターンにマッチしたファイル名を出力します.
func (c *ListFileCommand) Run() error {
    matches, err := filepath.Glob(fmt.Sprintf("%s/%s", c.Dir, c.Pattern))
    if err != nil {
        return err
    }

    for _, v := range matches {
        fmt.Println(v)
    }

    return nil
}

// Version は、本コマンドのバージョンを返します.
func (c *ListFileCommand) Version() string {
    return "v0.0.1"
}

再度ビルドしてみます。

$ make build
go build -o ./trygolang github.com/devlights/try-golang/cmd/trygolang

ちゃんと通りました。

構造体のコンストラクタでインタフェース型で返す

今回のサンプルみたいに、一つのインターフェースに対して複数の実装が存在するようになることが考えられる場合は、他の言語でもやるようにFactoryな関数を作って返すようにするのも有効です。ユーザ側に実体型を直接触らせずにインターフェース経由で操作してもらうためにですね。

func NewListFileCommand(dir, pattern string) Command {
    c := new(ListFileCommand)

    c.Dir = dir
    c.Pattern = pattern

    return c
}

このようなコンストラクタを作っている場合は、ダミーフィールドを使う必要はないです。ここで検証されるので、同じようにコンパイルエラーになります。

利用する側のコードも

package interface_

import (
    "github.com/devlights/try-golang/basic/interface_/command"
)

// VerifyInterfaceCompliance は、インターフェースの実装を検証するやり方のサンプルです.
//
// REFERENCES::
//   - https://qiita.com/kskumgk63/items/423df2e5245da4b16c25
//   - https://github.com/uber-go/guide/blob/master/style.md#verify-interface-compliance
func VerifyInterfaceCompliance() error {

    cmd := &command.ListFileCommand{
        Dir:     ".",
        Pattern: "go.*",
    }

    err := cmd.Run()
    if err != nil {
        return err
    }

    return nil
}

という風に、実体を直接むき出しで利用しているコードから

package interface_

import (
    "github.com/devlights/try-golang/basic/interface_/command"
)

// VerifyInterfaceCompliance は、インターフェースの実装を検証するやり方のサンプルです.
//
// REFERENCES::
//   - https://qiita.com/kskumgk63/items/423df2e5245da4b16c25
//   - https://github.com/uber-go/guide/blob/master/style.md#verify-interface-compliance
func VerifyInterfaceCompliance() error {

    cmd := command.NewListFileCommand(".", "go.*")
    err := cmd.Run()
    if err != nil {
        return err
    }

    return nil
}

という風にインターフェース経由で操作してもらえるようになります。ちょいと書くことが増えますが、個人的には可能であればこのようなコンストラクタを書く派です。コンストラクタでインターフェースを返すようにする場合は、上記の実体型 ListFileCommand はパッケージプライベートなスコープでいいので、公開型にしなくてよくて listFileCommand に出来ますね。

実行すると以下のようになります。

$ make run
ENTER EXAMPLE NAME: interface_verify_compliance
[Name] "interface_verify_compliance"
go.mod
go.sum

複数のインターフェースを実装している場合

複数のインターフェースを実装している場合は、インターフェースごとにダミーフィールド作ればいい感じですね。

package main

import (
    "fmt"
    "os"
)

type (
    command interface {
        Run()
    }

    cmdA struct {
        data string
    }
)

var _ command = (*cmdA)(nil)
var _ fmt.Stringer = (*cmdA)(nil)

func newCmdA() command {
    c := new(cmdA)
    c.data = "helloworld"
    return c
}

func (c *cmdA) Run() {
    c.data = "worldhello"
}

func (c *cmdA) String() string {
    return c.data
}

func main() {
    os.Exit(run())
}

func run() int {
    c := newCmdA()
    c.Run()
    fmt.Println(c)
    return 0
}

過去の記事については、以下のページからご参照下さい。

  • いろいろ備忘録日記まとめ

devlights.github.io

サンプルコードは、以下の場所で公開しています。

  • いろいろ備忘録日記サンプルソース置き場

github.com

github.com

github.com