kitoko552.memo

kitoko552のメモ

Goでオブジェクト指向っぽく書く

※この記事はQiitaにも投稿されています。

Goでオブジェクト指向っぽく書く - Qiita

はじめに

Goはオブジェクト指向の言語ですか?という問いが公式ドキュメントのFAQに載っています。

Is Go an object-oriented language?

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. ...

つまり、従来のオブジェクト指向言語とはちょっと違う(型の階層はない)けど、オブジェクト指向っぽく書くことはできるということです(私はそう理解しました)。 そこで、Goでオブジェクト指向っぽく書いてみました。

カプセル化

Goでは、構造体やそのフィールド、関数、メソッドのスコープは、名前の先頭が大文字か小文字かで決まります。 大文字ならpublic、小文字なら同じパッケージ内に閉じたスコープとなります。 したがって、以下のように書くことでカプセル化を実現できます。

package class

type Human struct {
    // フィールド名を小文字からはじめてスコープを同じパッケージ内に閉じる
    name string
}

// コンストラクタ
// 構造体がpublicでも、そのフィールドが閉じたスコープの場合、
// パッケージ外から{}を使った初期化はできない。
func NewHuman(name string) *Human {
    return &Human{ name: name }
}

// カプセル化
// メソッド名を大文字からはじめることで、publicなスコープにする。
func (human Human) GetName() string {
    return human.name
}

// セッターはレシーバをポインタにしないと値が変更されない
func (human *Human) SetName(name string) {
    human.name = name
}
package main

import (
    "./class"
    "fmt"
)

func main() {
    human := class.NewHuman("alice")
    fmt.Println(human.GetName())
    human.SetName("bob")
    fmt.Println(human.GetName())
}
$ go run main.go
alice
bob

注意

  • human.goでコンストラクタと書きましたが、Goにはコンストラクタにあたる構文がありません。代わりに、Newから始まる関数を定義して、その内部で構造体を生成するのが通例です。
  • 同じパッケージ内からはアクセッサを経由しないでアクセスできてしまいます。

Embed(埋め込み)

Goに継承はありませんが、代わりに他の型を埋め込む(Embed)方式で振る舞いを拡張できます。 埋め込まれた型(以下ではHuman)のフィールドやメソッドは、埋め込んだ型(以下ではMan)が実装しているかのように振る舞います。

/* 略 */

// Humanのメソッド
func (human Human) Check() {
    fmt.Printf("%s is a Human.\n", human.name)
}

type Man struct {
    *Human // Humanを埋め込む
}

func NewMan(name string) *Man {
    return &Man{ Human: NewHuman(name) }
}
/* 略 */

func main() {
    man := class.NewMan("bob")
    man.Check() // Humanのメソッドを自分が実装しているかのように呼び出すことができる。
}
$ go run main.go
bob is a Human.

オーバーライド

同名のメソッドを自分の型をレシーバにして再定義することでメソッドをオーバーライドすることができます。

/* 略 */

func (man Man) Check( ) {
    fmt.Printf("%s is a Man.\n", man.name)
}

先ほどと同じmain.goを実行すると、

$ go run main.go
bob is a Man.

注意

埋め込んだ型(Man)を埋め込まれた型(Human)として扱うことはできません。例えば、 var man *class.Human = class.NewMan("bob") とするとコンパイルエラーになります。

これは最初に載せた公式ドキュメントにもある通り、Goには型の階層がなくHumanとManは完全に別の型扱いになるからです。 Embedはあくまで埋め込みであって継承ではありません。

ポリモフィズム

Goでは継承を使ったポリモフィズムは実現できませんが、interfaceを実装することでポリモフィズムを実現することができます。

/* 略 */

type Speaker interface {
    Speak()
}

/* 略 */

// HumanにSpeakerを実装
func (human Human) Speak() {
    fmt.Println("I am Humman.")
}

/* 略 */

// Humanは埋め込んでいない
type Woman struct {
    name string
}

func NewWoman(name string) *Woman {
    return &Woman{ name: name }
}

// WomanにSpeakerを実装
func (woman Woman) Speak() {
    fmt.Println("I am Woman.")
}
/* 略 */

func main() {
    human := class.NewHuman("alice")
    woman := class.NewWoman("emily")
    
    // HumanもWomanもSpeakerインターフェースとして扱うことができる。
    speak(human)
    speak(woman)
}

func speak(speaker class.Speaker) {
    speaker.Speak()
}
$ go run main.go
I am a Humman.
I am a Woman.

interfaceを実装した型を埋め込む

あるinterfaceを実装した型を埋め込んだ構造体もまた、そのinterfaceとして扱うことができます。 上の例では、ManはHumanを埋め込んでいるので、Man自体はSpeakerを実装していなくてもSpeakerとして扱うことができます。

/* 略 */

func main() {
    human := class.NewHuman("alice")
    man := class.NewMan("bob")
    woman := class.NewWoman("emily")
    speak(human)
    speak(man)
    speak(woman)
}

func speak(speaker class.Speaker) {
    speaker.Speak()
}
$ go run main.go
I am a Humman.
I am a Humman.
I am a Woman.

もちろんManがSpeakメソッドをオーバーライドすることも可能です。

まとめ

以上のように、Goでもある程度オブジェクト指向っぽく書くことができました。

※この記事では以下の本を参考にしています。

WEB+DB PRESS Vol.82

WEB+DB PRESS Vol.82

  • 作者: 山口徹,Jxck,佐々木大輔,横路隆,加来純一,山本伶,大平武志,米川健一,坂本登史文,若原祥正,和久田龍,平栗遵宜,伊藤直也,佐藤太一,高橋俊幸,海野弘成,五嶋壮晃,佐藤歩,吉村総一郎,橋本翔,舘野祐一,中島聡,渡邊恵太,はまちや2,竹原,河合宜文,WEB+DB PRESS編集部
  • 出版社/メーカー: 技術評論社
  • 発売日: 2014/08/23
  • メディア: 大型本
  • この商品を含むブログ (1件) を見る