はじめに
Go言語を学習しはじめて、他言語の継承のようなことをしたい場合にどのように書くべきかわからなかったため調査しました。
継承の目的
継承とは、既存クラスやオブジェクトであるスーパークラス(親クラス)から、サブクラス(子クラス)に、クラスの属性や振る舞いといった特性を受け継ぐをことです。
継承は下記2点の目的を達するための仕組みであると考えられます。
- コードの再利用
- 多態性の実現(柔軟に拡張可能な設計)
コードの再利用は、共通の機能を持つクラスが必要な場合に、親クラスを継承した子クラスを作成することで、既存のコードを 再利用 できます。また、複数のクラスで共通の特徴がある場合に、抽出して親クラスにまとめること(汎化)でも、コードの再利用に繋がり、効率的な開発ができます。
多態性とは同じ操作をしたときに、異なるオブジェクトで異なる振る舞いをすることです。これにより、柔軟に拡張可能なシステムにすることができます。
Go言語に継承がない理由は?
Go言語では言語仕様として継承はありません。
設計段階で継承による複雑さを回避するために採用しなかったようです。
言語に関わらず、継承の問題点はよく議論に上がっていると思います。「初めてのGo言語」でも、「クラス継承よりオブジェクト合成の方がよい」と主張されていました。
継承では基底クラスと派生クラスの結合度が高くなってしまいますし、派生クラスは基底クラスに依存することでカプセル化を破ることにもなります。そのため、派生クラスは基底クラスの実装を正しく理解した上で実装しないとバグを埋め込んでしまうことになります。
こうした継承の問題点を回避するために、Goでは言語仕様で継承をサポートせずに、継承で実現したかった 「再利用」は埋め込み(embedded field)による委譲 、「多態性」はinterface 、で実現します。
Go言語でのコードの再利用
Go言語では継承がないが、合成(composition)や昇格(promotion)が組み込まれており、これらを使うコードの再利用が推奨されています。
埋め込みフィールドとして埋め込むことで、フィールドやメソッドは埋め込んでいる上位の構造体に「昇格」し、その構造体から直接呼び出しが可能になります。構造体に埋め込めるのは構造体だけでなく、どのような型でも可能です。
ただし、埋め込みフィールドと同名のフィールドがある場合は、埋め込まれる側が隠れてしまうのでアクセス時に型を明示する必要があります。
継承と埋め込みの違い
埋め込みにより継承と似たようなことができるだろうと考えていたためにハマりました。
継承ではスーパークラスを期待する関数にサブクラスを渡すことができるが、埋め込みで同じフィールドを持つ構造体でも、同様のことを行うことはできません。またGoの具象型には動的ディスパッチがないため、埋め込まれた構造体が埋め込まれていることを知ることができないです。
例えば、埋め込む側(Outer)と、埋め込まれる側(Inner)があり同名のメソッドがあったとします。このときにInner側のメソッドから同名メソッドを呼び出しても上位(Outer)のメソッドを呼び出すことができません。
検証コード
func main() {
outerA := &OuterA{
Inner: Inner{name: "Hoge"},
}
fmt.Printf("%s \n", outerA.getName()) // OuterA
fmt.Printf("%s \n", outerA.greet()) // Hello : Inner
}
type Inner struct {
name string
}
func (i *Inner) greet() string {
return "Hello : " + i.getName()
}
func (i *Inner) getName() string {
return "Inner"
}
type OuterA struct {
Inner
}
func (o *OuterA) getName() string {
return "OuterA"
}
type OuterB struct {
Inner
}
継承の感覚で、7行目で「outerA.greet()」で呼び出したときに、Outer側でオーバーライドしているgetName()が呼ばれて「Hello : OuterA」と出力されるつもりでいました。
この挙動はGo言語の埋め込み(embedded field)は継承ではなく委譲を実現する仕組みのため、このような挙動になります。他言語の継承のようなこと(再利用ではなく多態性)を実現するためには埋め込みではなく、インターフェースで実現する必要があります。
今まで継承(+オーバーライド)で実現していたようにOuter側のgreet()を呼び出したときにOuter(上位)のメソッドを呼ぶには下記のようにインターフェースを利用すれば実現は可能です(「これで本当に良いのか?」という気持ちでいます…もっと良い方法があれば知りたい)
type Namer interface {
getName() string
}
func main() {
outerA := &OuterA{
Inner: Inner{
name: "Hoge",
},
}
fmt.Printf("%s \n", outerA.greet(outerA)) // Hello : OuterA
}
まとめ
他言語で継承によって実現していた目的はGo言語では下記の機能で実現します。
- 実装の再利用 → 埋め込み(embedded field)
- 多態性 → Interface
Go言語では設計段階で両者を明確に分けることで、設計上の問題が起きないように工夫されていることがわかりました。実現したい目的をしっかりと考えながら適切な方法を選択してシステム設計していくことで、より品質の高いシステムに繋がると思いました。
参考文献