Help us understand the problem. What is going on with this article?

インタフェースの実装パターン #golang

More than 1 year has passed since last update.

はじめに

この記事は,以下の過去にQiitaに投稿したインタフェースの実装パターンの記事に,typeやメソッド,インタフェースの基本的な説明を追加してわかりやすくしたものです.

まずtypeとメソッド,基本的なインタフェースの実装方法についておさらいすることで,さまざまなインタフェースの実装パターンを扱う準備をしましょう.

typeで型を宣言する

まずはじめに,Go言語における型の宣言方法をおさらいします.Go言語をはじめたばかりの方の中に,typeの使い方を限定的にしか理解していない方をよく見かけます.ご存知のとおり,typeは型を宣言するために使うキーワードです.以下のように,構造体型やインタフェース型の宣言の際に,使用することが多いでしょう.

// 構造体型の宣言
type Hoge struct {
    // フィールドリスト
}

// インタフェース型の宣言
type Fuga interface {
    // メソッドリスト
}

golang.orgのThe Go Programming Language Specificationを見ると,typeを使った型宣言の文法は以下のように定義されています.

TypeDecl  = "type" ( TypeSpec | "(" { TypeSpec ";" } ")" ) .
TypeSpec  = identifier Type .
Type      = TypeName | TypeLit | "(" Type ")" .
TypeName  = identifier | QualifiedIdent .
TypeLit   = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
            SliceType | MapType | ChannelType .

一番シンプルな例をもう少しわかりやすく書くと以下のようになります.

type 識別子 型

「識別子」は,ここで宣言する型の名前です.「型」は,型名または型リテラル,()で囲まれた型となります.

型名は,intstringなどの組込み型の型名だけではなく,各パッケージでtypeを使って宣言された既存の独自型の型名も含みます.

つまり,typeを使うと,既存の型に新しい名前をつけることができます.以下のように宣言した場合,基本的にはintと同じように振る舞います.しかし,intHexは別の型であるため,演算したり,int型の値をHex型の変数に入れたりする場合は型のキャストが必要になります.

type Hex int

型リテラルとは,以下のような型をリテラルで書いたものです.

// 配列型
[10]int

// 構造体型
struct {
    // フィールドリスト
}

// ポインタ型
*int

// 関数型
func(s string) int

// インタフェース型
interface {
    // メソッドリスト
}

// スライス型
[]int

// マップ型
map[string]int

// チャネル型
chan bool

このような型リテラルに,「識別子」で指定した型名を新たにつけることができます.つまり,構造体型やインタフェース型の他にも,関数型やスライス型やマップ型,チャネル型の型リテラルにも型名をつけることができます.実際,httpパッケージのhttp.HandlerFuncは,以下のように定義された関数型です.なんでも構造体型として宣言する方を見かけますが,typeを使った型の宣言はもっと柔軟な使い方ができるので,ぜひいろいろ試してみてください.

type HandlerFunc func(w http.ResponseWriter, r *http.Request)

メソッド

メソッドは,以下のようにレシーバとメソッド名,関数本文を指定して定義します.この場合pがレシーバとなっています.

func (p *Person) String() string {
    return fmt.Sprintf("%s %s (%d)", p.FirstName, p.LastName, p.Age)
}

上記のメソッドは以下のような構造体のポインタ型のメソッドとして定義されているとします.

type Person struct {
    FirstName string
    LastName  string
    Age       int
}

このように,構造体型(または構造体のポインタ型)にメソッドを設けることが可能です.しかし,メソッドは構造体型や構造体のポインタ型だけにしか定義できないわけではありません.インタフェース型でなければ,どんな型にでもメソッドを定義することができます.たとえば,上述したHex型にメソッドを定義してみましょう.

type Hex int
func (h Hex) String() string {
    return fmt.Sprintf("0x%x", int(h))
}

int型を新たにHex型として宣言し,Stringというメソッドを設けることができました.
もちろん,同様に関数にもメソッドを設けることができます.実際に,http.HandlerFuncは以下のようなメソッドを定義しています.

type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

http.HandlerFuncServeHTTPメソッドはレシーバ自身を関数呼び出しするメソッドですが,なぜこのようなメソッドを宣言する必要があるのかは後で説明します.

基本的なインタフェースの実装

前述したとおり,インタフェース型は以下のように宣言されます.

type TypeName interface {
    // メソッドリスト
    Method1()
    Method2()
}

インタフェース型の宣言時に指定したメソッドリストのメソッドをすべて実装することで,インタフェースを実装することができます.Javaなどのように,implementsなどを使って明示的に実装する必要はありません.

ここまで何度か出てきている各型のStringメソッドは,fmt.Stringerインタフェースを実装していることになります.なお,fmt.Stringerは以下のように宣言されています.

type Stringer interface {
    String() string
}

たとえば,上述したHex型はStringメソッドを持たせたので,fmt.Stringerインタフェースを実装したことになります.そのため,Hex型はfmt.Stringerインタフェースとして振る舞うことができます.fmt.Stringerインタフェースとして振る舞うとは,fmt.Stringer型の変数に代入したり,引数として関数に渡すことができることを指します.

var stringer fmt.Stringer
// int型の100をHex型にキャストし,fmt.Stringer型の変数に代入している
stringer = Hex(100)

ちなみに,よく見かけるinterface{}型は,メソッドリストがないインタフェース型の型リテラルです.
メソッドリストがないということは,メソッドを一つも実装しなくても,interface{}インタフェースを実装したことになるため,interface{}型の変数や引数には,どんな型の値でも代入したり,渡したりすることができます.

さて,ここまではtype,メソッド,インタフェースの基本的な実装方法について説明してきました.
ここまでの説明を踏まえて,より応用的な方法でインタフェースを実装してみましょう.

関数にインタフェース実装させる

上述のとおり,関数にもメソッドを持たせることができます.
たとえば,http.HandlerFuncは以下のようにServeHTTPメソッドを持っていました.

type HandlerFunc func(w ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

httpパッケージでは,http.Handlerというインタフェースを定義しています.

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

そして,実はhttp.HandlerFunc型は同じ引数と戻り値の関数に,http.Handlerインタフェースを実装するために宣言されている型だったのです.そのため,メソッドの中でレシーバ自身の関数呼び出しを行っていたのでした.

関数を以下のようにキャストすることで,インタフェース型として変数に代入できます.
このとき,fhttp.Handlerを実装しているため,http.Handleの第2引数として渡すことができます.

f := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hello, world")
})

// func Handle(pattern string, handler Handler)
http.Handle("/", f)

なお,httpパッケージには,この処理をもっと簡単にできるhttp.HandleFuncという関数が用意されています.通常はこの関数を用いることが多いでしょう.

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

構造体に埋め込んでインタフェースを実装する

構造体には,匿名フィールドとして指定した型の値を埋め込むことができます.
埋め込んだ型の値のフィールドやメソッドは,あたかも埋め込み先の構造体のフィールドやメソッドのように呼び出すことができます.

たとえば,以下のようにNameのポインタ型にStringメソッドを持たせて,Person型に埋め込んでみます.Person型はStringメソッドを持っていませんが,埋め込んだ*Name型がStringメソッドを持っているため,p.String()のように呼び出すことができます.

type Name struct {
    FirstName string
    LastName  string
}

func (n *Name) String() string {
    return fmt.Sprintf("%s %s", n.FirstName, n.LastName)
}

type Person struct {
    // *Name型の値を埋め込む
    *Name
    Age int
}

func main() {
    n := &Name{
        FirstName: "Taro",
        LastName:  "Yamada",
    }

    p := &Person{
        // *Name型のnを埋め込む
        Name: n,
        Age: 20,
    }
    fmt.Println(p.String())
}

このとき,*Name型は,Stringメソッドを持っているため,fmt.Stringerインタフェースを実装していることになりますが,実は*Name型をPerson型に埋め込んだことにより,Person型と*Person型fmt.Stringerインタフェースを実装したことになります.つまり,上記の変数pは以下のようにfmt.Stringer型の変数に代入することができます.

var stringer fmt.Stringer = p
fmt.Println(stringer.String())

埋め込みを使ったインタフェースの部分実装

インタフェースを定義する際に実装を義務付けるメソッドを複数指定することができます.
たとえば,以下のようなインタフェースを定義したとします.

type Person interface {
    // 敬称
    Title() string
    // 名前
    Name()  string
}

上述の埋め込みによるインタフェースの実装を応用して,インタフェースで指定したメソッドリストの一部を構造体型に埋め込んだ型に実装させることが可能です.

以下の*person型は,Nameメソッドを持っています.しかし,Titleメソッドは持っていないため,Personインタフェースを実装していることにはなりません.

type person struct {
    firstName string
    lastName  string
}

func (p *person) Name() string {
    return fmt.Sprintf("%s %s", p.firstName, p.lastName)
}

しかし,*person型をTitleメソッドを実装した構造体に埋め込むことで,Personインタフェースを実装することができます.以下の例では,*female型と*male型にTitleメソッドを実装させています.それぞれの型には*person型が埋め込まれているため,Nameメソッドも実装していることになります.そのため,*female型と*male型はPersonインタフェースを実装していることになります.

type Gender int

const (
    Female = iota
    Male
)

type female struct {
    *person
}

func (f *female) Title() string {
    return "Ms."
}

type male struct {
    *person
}

func (m *male) Title() string {
    return "Mr."
}

Go言語では,小文字で始まる型はそのパッケージ内からしか使用することができません.そのため,female型とmale型は他のパッケージから隠蔽されています.他のパッケージからは,これらの型を意識せずPersonインタフェースとして使用できることが好ましいでしょう.

Personインタフェースを実装した値を返す,NewPerson関数を考えてみましょう.引数に性別(gender)を指定することで,内部で生成する構造体の型を切り替えています.このように呼び出し元では,Personインタフェースを実装しているのが*female型なのか,*male型のなのか意識せず,Titleメソッドの実装を切り替えることができています.

func NewPerson(gender Gender, firstName, lastName string) Person {
    p := &person{firstName, lastName}

    if gender == Female {
        return &female{p}
    }

    return &male{p}
}

埋め込みを使ったインタフェースの動的実装

構造体型に埋め込める型は,型名が付いた型であればどんな型でも埋め込むことができます.ただし,型リテラルは埋め込むことができません.たとえば,chan int[]intなどは型リテラルであるため,構造体型に埋め込むことはできません.一方で,fmt.Stringerインタフェースなどは,型名が付いているインタフェース型であるため,構造体型に埋め込むことが可能です.

type Hoge struct {
    chan int        // ダメ
    []int           // ダメ
    fmt.Stringer    // OK
}

埋め込む型が構造体型やそのポインタであった場合,その型の値しか埋め込むことはできません.しかし,埋め込んでいる型がインタフェース型であれば,そのインタフェースを実装した値であればなんでも埋め込むことができます.

type Hoge struct {
    *Fuga          // *Fuga型の値しか埋め込むことができない
    fmt.Stringer   // fmt.Stringerインタフェースを実装していれば埋め込むことができる
}

上述のとおり,埋め込んだ型は匿名フィールドとして宣言されているため,値を動的に変えることが可能です.

type Person struct {
    fmt.Stringer
    FirstName string
    LastName  string
    Age       int
}

func main() {
    p := Person{
        Stringer:  nil,
        FirstName: "Taro",
        LastName:  "Yamada",
        Age:       20,
    }
    fmt.Println(p.Stringer) // nil
    p.Stringer = ???        // fmt.Stringerインタフェースを実装していれば代入できる
}

上記のPerson型にfmt.Stringer型を埋め込むことができたとしても,Person型の値を使ってStringメソッドを実装した値でなければあまり意味がありません.この場合であれば,FirstNameLastNameを使って文字列を組み立てることが望まれています.

そこで以下のような,fmt.Stringerインタフェースを実装する関数型を考えてみましょう.StringerFuncは文字列を返す関数にfmt.Stringerインタフェースを実装させるための型です.

type StringerFunc func() string

func (sf StringerFunc) String() string {
    return sf()
}

さらに,Person型のポインタを引数にとるようなfunc(p *Person) string型の関数をStringerFunc型にキャスト可能なfunc() string型に変換するような関数を考えてみます.引数を取らずに,特定の変数(この場合だとp)にアクセスするにはクロージャを用いるとよいでしょう.

func BindStringer(p *Person, f func(p *Person) string) fmt.Stringer {
    return StringerFunc(func() string {
        return f(p)  
    })
}

BindStringer関数の戻り値の型はfmt.Stringer型ですが,実際に返される値はStringerFunc型です.関数内部では,第1引数のpと第2引数のfを参照できるようなfunc() string型のクロージャを作成し,StringerFunc型にキャストしています.ここで生成しているクロージャ内部では,引数pfが参照できるため,pfの引数として関数呼びだしを行っています.

このようにfunc(p *Person) string型をfunc() string型に変換し,さらにStringerFunc型にキャストすることで,*Person型を実装にしようしたfmt.Stringerインタフェースを満たす値を生成することができます.

それでは,上記の関数を*Person型を初期化するNewPerson関数に組み込んでみましょう.そして,fmt.Stringerインタフェースの実装を動的に変えることができるようにSetStringerメソッドを用意してみます.

func NewPerson(firstName, lastName string, age int) (p *Person) {
    p = &Person{
        nil,
        firstName,
        lastName,
        age,
    }

    p.Stringer = StringerFunc(func() string {
        return fmt.Sprintf("%s %s (%d)", p.FirstName, p.LastName, p.Age)
    })

    return
}

func (p *Person) SetStringer(sf func(p *Person) string) {
    p.Stringer = StringerFunc(func() string {
        return sf(p)
    })
}

NewPerson関数では,fmt.Stringerのデフォルト値として,Taro Yamada (20)のような文字列を返すStringerFuncを埋め込んでいます.SetStringerメソッドは,引数に*Person型をとり,stringを返す関数を引数sfにとっています.sfは上述のBindStringer関数と同様の手順で,StringerFuncにキャストされ,Person型の匿名フィールドであるStringerに設定されます.SetStringerを使えば,埋め込まれているfmt.Stringerインタフェースの実装を実行時に動的に変更することが可能となります.

ここでひとつ注意しなければならないことがあります.Person型に埋め込んだfmt.Stringer型はfmtパッケージで外部に公開されている型です.そのため,Person型に埋め込んだ値をPerson型を宣言したパッケージ外からも変えることができます.そこで,以下のようにPerson型に埋め込む型はfmt.Stringer型に新たに名前をつけたパッケージ外に公開されない型にすることで,パッケージ外から埋め込んだ値を変更されることを防ぐことができます.そして,stringerインタフェースを実装している型はfmt.Stringerインタフェースも実装していることになるため,本質的な動作は変わりません.

// パッケージ外に公開されない型として宣言する
type stringer fmt.Stringer

type Person struct {
    stringer            // パッケージ外から代入ができない匿名フィールド
    FirstName string
    LastName  string
    Age       int
}

おわりに

この記事では,インタフェースに関係する基礎的な知識をおさらいし,さらに応用的なインタフェースの実装方法を説明しました.私自身,ここで挙げた応用的なインタフェースの実装方法が現実のGo言語のプログラミングにどのように活かせるのかは模索中です.この記事を読んだ方で,こういう使い方ができるという実装例があれば,ぜひ@tenntennまで教えて下さい.

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした