オブジェクト指向言語としてGolangをやろうとするとハマる点を整理してみる

  • 35
    いいね
  • 0
    コメント

オブジェクト指向言語としてGolangをやろうとするとハマること

この状態でペガサスを走らせたらどうなるでしょうか?Javaなどのオブジェクト指向言語に慣れた方は「Walkは子クラスでオーバーライドされたのだから、走れと言われたら3回飛ぶに違いない」と思われるでしょう。でも違います。3回歩きます。

何故上記のようになるのか?何故埋め込みが必要だったのか?ということを性能面ではなくオブジェクト指向の観点で整理してみます。

1. 実装継承と委譲

実は、上記のような挙動になるのはGolang独特でもなくJavaやRubyなどでも同様です。

まずは先ほどのGolangのコードから埋め込みを使わないで同様のことを実現してみます。
オリジナルソースコード
埋め込み除去版ソースコード
Javaソースコード

実行結果
Earth Pony walk
歩くよ
Earth Pony Sprint
歩くよ
歩くよ
歩くよ
Pegasus Walk
走るよ
Pegasus Sprint
歩くよ
歩くよ
歩くよ

こうしてみるとこのような実行結果になることが自然だとわかると思います。

つまりどういうことかというとGolangの埋め込みは継承ではなく委譲だということです。

2. 埋め込みとは何なのか?

委譲は従来の実装継承と比較して以下のような冗長なコードが増えてしまいます。

  1. 委譲元のクラスに対して委譲先のインスタンスを渡す
  2. 委譲元のクラスでは委譲先クラスのインスタンスを受け取れるようにメンバを宣言
  3. 委譲元のクラスに委譲先の関数を実行する処理を記述。関数の数だけ必要。

Golang以外の言語を書いているときは諦めて素直に上記のような冗長なコードを書いていました・・・が!
Golangではこの冗長なコードを埋め込み(embedded)を使うことで解決しています。

では、改めて比較してみましょう。

埋め込み未使用の委譲
func main() {
    var ep EarthPony
    fmt.Println("Earth Pony walk")
    ep.Walk() 
    fmt.Println("Earth Pony Sprint")
    ep.Sprint()

    p:= Pegasus{ep} // 冗長ポイント1
    fmt.Println("Pegasus Walk")
    p.Walk() 
    fmt.Println("Pegasus Sprint")
    p.Sprint()
}

type Pegasus struct {
    ep EarthPony // 冗長ポイント2
}
// 省略
// 冗長ポイント3
func (u *Pegasus) Sprint() {
    u.ep.Sprint()
}
埋め込みを使用した委譲
func main() {
    var ep EarthPony
    fmt.Println("Earth Pony walk")
    ep.Walk() 
    fmt.Println("Earth Pony Sprint")
    ep.Sprint()

    var p Pegasus // 改善ポイント1
    fmt.Println("Pegasus Walk")
    p.Walk() 
    fmt.Println("Pegasus Sprint")
    p.Sprint()
}

type Pegasus struct {
    EarthPony // 改善ポイント2
}

// 改善ポイント3
// EarthPonyをEmbedしているのでPegasusでSprintを実行すると
// EarthPonyのSprintに処理が委譲される。実装継承ではない。

見事に冗長なポイントが改善されており、構造体に委譲先の型を埋め込むだけで良いため、継承のときと同様に数文字追加するだけで済むようになりました。

3. そもそも何故Golangは継承を廃止して委譲を推奨しているのか?

Golangに限らずオブジェクト指向の世界では継承より委譲を使うべきだということは、過去に議論されてきました。

具体的な継承の問題点は、子クラスが親クラスの実装に依存してカプセル化を破ることです(継承の悪い例)。つまり子クラスの実装者は親クラスのインターフェースの入出力の仕様だけを理解して実装してしまうとバグを埋め込む可能性があるため、親クラスの実装を正しく理解しなければいけません。
委譲ではこのようなことはありません。

また継承がなくともインターフェースが機能として提供されていれば、すべてのオブジェクト指向設計の原則を守ることができます。

もちろん継承も正しく理解して使えば害は少ないかもしれませんが、継承はどうしてもis-a関係を破壊した実装をしがち(車を作るのにタイヤやエンジンを継承したりはしていませんか?車 is a タイヤではありません。)で、is-a関係とhas-a関係を正しく区別して継承と委譲を使い分けるには一定のスキルが必要になります。そのため不要なら継承を削除してしまおうという考えたのではないでしょうか。
# Golangでは埋め込みで対応したのに対して、Rubyはmixinを採用しています。解決方法は1つではありません。”多重継承は悪だ”というのは嘘だ by まつもとゆきひろ

まとめ

このようにオブジェクト指向の概念とGolangの文法を理解することで、Golangの埋め込みでハマる点を整理してみました。

また継承と委譲について考えたことがなかった人は、実装継承と委譲の違いや、実装継承や型継承の違いなどを調べて消化してみると良いと思います。
正解があるわけではありませんが、自分の中で消化できると継承と委譲の使い分けの基準が明確になるはずです。

またGolangは継承がないからオブジェクト指向言語ではないと言われることがありますが、私の中では「継承がない」「インターフェースがある」「委譲が簡単」という特徴を兼ね備えたGolangこそが求めていた理想的なオブジェクト指向言語です。

ワード

  • inheritance vs composition
  • 継承 VS 委譲
  • is-a関係、has-a関係
  • 実装継承、型継承

参考

蛇足

自分なりにGolangで走るということは3回歩くことだという概念を共通化したコードも書いてみました。
共通化版

このような共通化はデザインパターンではTemplate Methodパターンとして登場します。GoFでは実装継承を使っていますが今回は委譲で実現してみます。

委譲で共通化するためにmattnさんテクニックを採用させて頂きました。

また設計としては不要になるためPegasusからEarthPonyへの委譲をやめています。

jpeg.jpg

共通化版実行結果
Earth Pony walk
歩くよ
Earth Pony Sprint
歩くよ
歩くよ
歩くよ
Pegasus Walk
飛ぶよ
Pegasus Sprint
飛ぶよ
飛ぶよ
飛ぶよ

シンプルに書くならコンパウンド使う必要ありませんでした。
共通化版2