2010年9月23日木曜日

Object Oriented Go

GoはObject Orientedなこともできる言語だけれど、JavaとかC++にはあるアクセス制限のための仕組みが無い。あのpublicとかprivateとかいうヤツね。正確に言えば、パッケージ間であればアクセス制限のための仕組みはある(大文字始まりの変数・関数のみが外部に公開される)が、パッケージ内だとそういう仕組みは無い。つまりGoでstructを作るとパッケージ内では全てがpublicに公開されてしまう。

Go的OO

そんなGoでObject Orientedなコードを書く場合どうするかを試してみた。

package main

import (
    "log"
    "os"
)

var logger *log.Logger = log.New(os.Stdout, nil , "", log.Lok)

type Talker interface {
    Talk() string
}

type Cat struct {
    name string
}

func (c Cat) Talk() string {
    return c.name+": Meow meow"
}

type Dog struct {
    name string
}

func (d Dog) Talk() string {
    return d.name+": Bow wow"
}

func DoTalk(t Talker) {
    logger.Log(t.Talk())
}

func main() {
    cat := Cat{
        name:"Tama",
    }

    dog := Dog{
        name:"Pochi",
    }

    DoTalk(cat)
    DoTalk(dog)
}

上記プログラムの出力

$ ./oo-test
Tama: Meow meow
Pochi: Bow wow

nameというstring変数を持ったDogとCatというクラスと、それらのクラスのインターフェースを抽出したTalkerインターフェース。OOの教科書では良く見る例のGo版である。

ここでもしnameを名字と名前の二つの要素から構成しようと変更しようとした場合、色々と書き換えが必要になる。なので、(他のOO言語でも一緒だけど)Go的にはnameを生のstringとするのでなく、専用の型を作っておく方が安全。


type Name string

type Cat struct {
    name Name
}

func (n Name) String() string {
    return string(n)
}

func (c Cat) Talk() string {
    return c.name.String()+": Meow meow"
}

こうしておくと、先のように名字と名前にわけたくなったときでもコードの変更量が少ない。


type Name struct {
    first string
    last string
}

type Talker interface {
    Talk() string
}

type Cat struct {
    name Name
}

func (n Name) String() string {
    return n.last + n.first
}

func (c Cat) Talk() string {
    return c.name.String()+": Meow meow"
}

type Dog struct {
    name Name
}

func (d Dog) Talk() string {
    return d.name.String()+": Bow wow"
}

func DoTalk(t Talker) {
    logger.Log(t.Talk())
}

func main() {
    cat := Cat{
        name: Name{
            first:"たま",
            last:"斎藤",
        },
    }

    dog := Dog{
        name: Name{
            first:"ポチ",
            last:"五十嵐",
        },
    }

    DoTalk(cat)
    DoTalk(dog)
}

出力

$ ./oo-test
斎藤たま: Meow meow
五十嵐ポチ: Bow wow

更に言うと、上記のようにString()という関数でNameの文字列を返すようにしておくと、fmt.PrintfでもそもままNameが渡せるようになるので二度おいしい。これが実現可能なのは、fmt側で専用のStringerインタフェースを定義しているから。


    dog := Dog{
        name: Name{
            first:"ポチ",
            last:"五十嵐",
        },
    }

    fmt.Printf("name: %s\n",dog.name)

出力

name: 五十嵐ポチ

まとめ

そんなわけでObject OrientedなGoを書いてみた。基本はアクセス制限の効かない普通のOO言語と言ったところ。一つ違うのがGoのもつインタフェースの仕組みで、標準ライブラリが受けいれるインタフェースに沿えば自作したクラスであっても標準ライブラリにそのまま渡せるというのは便利。

  • 型はパッケージ内・パッケージ間に関わらず積極的に定義する。型チェックの厳しさがGoの強みなので、それを活かすのが重要。ついでに変更にも強くなる。
  • 標準ライブラリがサポートしているインタフェースにあわせて関数を定義すると色々便利。

0 件のコメント:

コメントを投稿