「継承より合成(移譲)」について

2020-09-27
Rustでゲームを作ってみた 際に、Rustには `class` という機能はないが、

struct で独自のデータ型を定義できて、 そして impl でその型に関連するメソッドを定義することでオブジェクト指向のようにコードを分割できるということでそのように組んでみた。

しかしどうもC++など一般的なクラスベースのOOP的な考え方で作ろうと思ってもうまくいかないように感じた。 というのも 継承 という機能がないからだ。

構造体、impl、トレイト

例えばいろんな種類の敵が登場する場合、従来だとベースクラスを作成してそれを継承した個別のクラスを作成する、というのが常套手段だったと思う。 共通する処理はベースクラスにメソッドを持たせて、特殊化したい機能を仮想関数でオーバーライドする、などとしていた。 しかしRustでは継承ができないので、結局すべての敵を同じ構造体で扱うという、なんともひどい方法で対処した。

敵の種類ごとに構造体を分けるには trait でデータ型が実装すべきメソッドを規定して、各構造体で個別に実装してやればよい。 管理する側ではそれらのメソッドを介して扱うことができる。

ただそうした場合、敵のある機能(例えば描画など)が共通化できる・したい、という場合にもコードを共有することができない。 trait はJavaでいう interface のようにメソッドは規定できるけどフィールドを持てないので、 位置や表示パターンなどの情報を参照できないからだ (それらも trait のメソッド経由で取得できるようにして、trait のデフォルト実装とすればできなくはないが)。

継承より合成 (Composition over inheritance)

他にどういうふうに組むのがいいんだろうかと思いながらうろついていたら、 そもそもクラスベースのOOPでも昨今では継承より合成 が望ましい、ということになっているということを知った(今さら…)。 継承では「親クラス」を指定してその機能を利用・拡張するのに対して、 合成は構造体のフィールドとして別の構造体を保持して利用する。

よくいわれる、継承はis-a、合成はhas-aを表現するが、自然言語では言い方によってはどちらにも表現できるので間違って使ってしまうとおかしなことになる、 ということだと思ってたんだけど、is-aでも合成のほうが望ましいということなんだろうか? どちらにせよ、Rustでは継承が使えないから常に合成を使うしかない。

合成を使用する際の問題点と利点

問題点

is-aを表現する場合にも合成を使用する際の面倒な点として、移譲するコードを書く必要がある。 継承であれば親クラスのメソッドが自動的に呼び出されるのに対して、合成の場合には明示的に記述する必要がある。

また実装上の問題点として継承では処理の大部分は共通で一部だけ特殊化したいといったときにメソッドをオーバーライドすることで実現できるが、合成ではできない (別のカプセルになっているので)。 コールバックを渡すとかすればある程度できなくはないが、Rustだとコールバック内で書き換えができないので制限がある。 とすると特殊化するには処理をうまく切り分けて個別に呼び分けるか、コードを重複(コピペで一部変更)させる必要がある。

利点

継承の場合に発生する以下の問題が、合成では回避できる:

  • 親クラスを(たいてい)1つしか指定できないので複数の機能を使いたくてもクラス階層構造の構成上できなかったりする
  • 親子のクラスが密結合して複雑化する
  • あらゆる機能を持つ親クラスができ上がる

ということで継承を使えば実装上楽できていたところが多少面倒になるけど、それを補って設計上の問題が回避できる、ということになるかもしれない。 継承は便利だけど強力すぎて間違った利用を引き起こす事が多く、合成では機能は制限されるけどその分各クラスが1つの役割に責任を持つ設計になる、のかもしれない。

リンク