Goを勉強している中で、「Composition over inheritance」という概念が出てきました。ちゃんと理解していなかったので、ここで改めて掘り下げます。
特に、普段 Ruby や Rails を書いている初中級者の方は、Go を勉強する前におさらいしておくことをオススメします。なぜなら、Go では、Rubyで馴染みのある Inheritance(継承)ではなく、Composition(合成)のみが使われるからです。
「Composition over inheritance」とは
「Composition over inheritance」は、日本語だと「継承より合成」と表現されます。
これは、オブジェクト思考プログラミングにおいて、親クラスやベースクラスを「継承」するよりも、「合成」によってコードを共通化・再利用する方が望ましい、という考え方です。
つまり、継承より合成の方が良いということです。
それを理由に、Go では合成のみが採用されており、型の継承はできません。
Composition と Inheritance の比較
Composition と Inheritance、それぞれについておさらいします。
両者の対比は、次のようにまとめられます。
英語 | 日本語 | 型の関係 | 考え方の例 |
---|---|---|---|
Composition | 合成 | has-a | ソファには綿が入っている |
Inheritance | 継承 | is-a | ソファは家具である |
具体的なコード例
文字だけでは抽象的なので、具体的なコード例で考えてみましょう。
ここでは、挨拶ができる「英語圏の人」と「日本人」をコードで表してみます。
Composition と Inheritance を対比させたいので、どちらも表現しやすい Ruby を用います(独断と偏見)。
Inheritance の例
Inheritance で設計する場合、まず「挨拶ができる」スーパークラスとして Person
クラスを定義し、greet
メソッドを持たせます。
class Person
def greet
print word
end
end
その Person
クラスを継承する English
クラスを定義します。
class English < Person
def word
'Hello!'
end
end
同様に Japanese
クラスも定義します。
class Japanese < Person
def word
'こんにちは!'
end
end
English
・Japanese
、どちらでも挨拶できます。
e = English.new
e.greet # 'Hello!'
j = Japanese.new
j.greet # 'こんにちは!'
Inheritance は、普段 Ruby・Railsを書いてる人にとって、ごく一般的で馴染みのある設計だと思います。
Composition の例
Composition で設計する場合、「挨拶ができる」ことに着目し、そのビジネスロジックを Greeting
クラスとして切り出します。(モジュールを include
するやり方は、クラス継承ツリーに含まれてしまうので、Composition の純粋な例としては使えません。)
class Greeting
def greet(word)
print word
end
end
その Greeting
クラスを English
クラスに持たせます(合成します)。
class English
attr_reader :greeting
def initialize
@greeting = Greeting.new
end
def greet
greeting.greet(word)
end
def word
'Hello!'
end
end
同様に Japanese
クラスも定義します。
class Japanese
attr_reader :greeting
def initialize
@greeting = Greeting.new
end
def greet
greeting.greet(word)
end
def word
'こんにちは!'
end
end
これで先ほどと同様に、English
・Japanese
どちらでも挨拶できます。
e = English.new
e.greet # 'Hello!'
j = Japanese.new
j.greet # 'こんにちは!'
Composition の例 (Go ver.)
参考として、Go で上の Composition を表すとこのようになります → こちら (Go Playground)
なぜ、Compositionが良いのか?
ここからが本題です。
なぜ「Composition over inheritance」、つまり、継承より合成の方が良いのでしょうか?
Composition のメリット
一般的に考えられているメリットは次の2つです。
1. 考えやすい
Composition の場合、その共通化させるビジネスロジック(上の例では、挨拶できること)に着目してコードを設計します。そのため、Inheritance の場合と違って、抽象化させるためのスーパークラス名や、継承ツリーの構成などに頭を悩まされることはありません。
2. 変更しやすい
コードに変更を加える場合、Composition では、そのビジネスロジックを持っている当事者のみが影響を受けます。また、あるクラスにビジネスロジックを追加する場合も、Composition として共通化していれば簡単に対応できます。
一方 Inheritance では、スーパークラスのコードを変更すると、その継承ツリーの下位クラス全体に影響があります。そのため、スーパークラスに新しい振る舞いを追加することは、意図しないエラーを生むリスクがあります。
例えば、挨拶できるロボットがいた場合...
Composition だと、新たな Robot
クラスに Greeting
クラスを持たすだけでOKです。
一方 Inheritance では、Robot
は Person
ではないので、スーパークラス名や継承ツリーの構成を再検討しないといけません。また、スーパークラスに変更を加えると、その下位クラスで意図しないエラーが起きるリスクがあります。
Composition のデメリット
一般的に考えられているデメリットは1つで、合成したメソッドを呼ぶためのメソッドを書かないといけないことです。上の Ruby の例で言う、English#greet
や Japanese#greet
ですね。
一方の Inheritance では、継承したスーパークラスのメソッドをそのまま呼び出せます。したがって、スーパークラスに基本的なビジネスロジックを持たせておけば、Composition よりも少ないコードで同じ振る舞いを実装することが可能です。(ただし、何でもかんでも詰め込むと「神クラス」となって収集がつかなくなるので注意!)
デメリットの回避
この Composition のデメリットを回避するために、Go では Embedding types が用いられます。
先ほどあげた Go Playground のコード例でも Greeting
タイプを English
・Japanese
タイプに組み込むことで、それぞれのタイプで greet
メソッドを定義することを回避しています。
// 一部抜粋
type Greeting struct{}
type English struct {
Greeting
}
type Japanese struct {
Greeting
}
まとめ
継承は、抽象化したコードで効率的な実装ができるので、変更の可能性が少ない部分などにはとても有効な設計手法です。そして、Ruby を使っている人にとっては、とても馴染みがあります。
しかし、Go では、合成のみが採用されており、型の継承はできません。
この言語仕様の違いを、背景にある「Composition over inheritance」と一緒にしっかり理解しておけば、より早く Go の世界に馴染むことができるでしょう。
Sources
- Composition over inheritance - Wikipedia
- Why Go’s structs are superior to class-based inheritance - Ian Macalinao
- オブジェクト指向と10年戦ってわかったこと - @tutinoco
- 継承より合成ってなに? - Mastering Python
- Ruby : Composition over Inheritance because The Force is Strong with Composition - Kartik Jagdale
- I would love to see some composition examples - Ruby Chat