継承のことは忘れよう
ご存知の通り、オブジェクト指向の三大要素であるはずの継承においてはたくさんの問題点が報告されている(こことか、こことか)。確かに継承よりもコンポジション(委譲、合成)のほうが優れている点が多い。下記はその一例だ。
いま、Windows.FormsのButtonクラスに機能を足して押された回数をカウントして表示するボタンを作りたいのでButtonクラスを継承して新しいクラスを作るとする。
class CountingButton : Button { private int count = 0; protected override void InitLayout() { base.InitLayout(); this.Text = this.count.ToString(); } protected override void OnClick(EventArgs e) { base.OnClick(e); this.count++; this.Text = this.count.ToString(); } }
元のButtonクラスのコードはすべて引き継がれているので変更が必要な初期化とクリックの処理だけをオーバーライドすればOKだ。とても便利!…と思いきや、IDEで入力補完を見るとすぐに問題があることが分かる。
これが継承の弱点だ。Textプロパティの操作まで公開されてしまっている。Publicなメソッドやプロパティを継承先のクラスで隠すことができないのでどうしようもない。これでは意図しない操作によりオブジェクトが破壊される可能性があるので非常に不安になる。使わないように気を付けたら良いという問題ではない。例えば親クラス型のコレクションに入れられたときにどうなるか。
List<Button> buttons = new List<Button>(); buttons.Add(new Button()); buttons.Add(new Button()); buttons.Add(new CountingButton()); foreach (Button button in buttons) { button.Text = "アウト!"; }
このコレクションに私の作ったオリジナルボタンが入っていないことを気にしなければならなくなる。これは中身を気にしなくてよいというカプセル化の原則に違反している。
コンポジションならこの問題は発生しない。
class CountingButton { readonly Button origin; int count = 0; public event EventHandler Click; public CountingButton(Button origin) { this.origin = origin; this.origin.Click += origin_Click; this.origin.Text = this.count.ToString(); } private void origin_Click(object sender, EventArgs e) { count++; this.origin.Text = this.count.ToString(); this.Click(sender, e); } }
問題だらけの継承よりコンポジションが優れている。だから継承は悪。
…本当だろうか。
継承の使い方がまちがっている
継承が悪かどうかという話においては上記のような実用上の問題点にフォーカスが当てられがちだが、その多くは継承を不正に使っていることが原因だ。上記の例はまさに典型的な継承の誤用である。Buttonクラスを継承してコードを再利用しようとするのは良いが、Textプロパティを操作できないような仕様にするのはまずい。Windows.Formsの世界において、ButtonはTextプロパティを操作できるものと決められているからだ。もはやこの世界において、CountingButtonはButtonとは認められないのでButtonクラス型の変数に入れられるようになっていてはいけない。したがって最初からコンポジションを使うのが正解だ*1。
そもそも継承の仕組みがオブジェクト指向の世界にそぐわない
上記の例のように、実装の再利用という観点で継承を理解すると無意識のうちに悪用してしまう。どうしてJavaやC#、その他オブジェクト指向言語にはこのような危険な仕組みが用意されているのだろうか。便利だから?こんな仕組みはオブジェクト指向の世界に全くそぐわない。元祖オブジェクト指向言語Smalltalkのことを思い出してほしい。継承のキーワードはinherit(継承)ではなくderive(派生)だ。これは分類学的な意味でサブクラスを作るという意味(例えば人間は哺乳類のサブクラス)であり実装をコピーして引き継ぐこととは何の関係もない。そしてそれはインターフェースで間に合っている。
interface Animal { void Breathe(); } interface Human : Animal { void Talk(); } interface Dog : Animal { void Run(); } interface Fish : Animal { void Swim(); }
分類学的な意味でのサブクラスはスーパークラスの性質を完全に引き継ぐ必要がある。スーパークラスの性質(メソッド)をオーバーライドしたり差し引いたりしたサブクラスはもはやスーパークラスと同列に扱ってはいけない存在だ。にもかかわらず継承の仕組みはそれを許してしまっている。それこそが本当の問題ではないだろうか。
不正なサブクラスが存在しているとき世界は歪んだ状態になっている。歪んだ世界において我々はスーパークラスのオブジェクトを信用できないので中身を調べたり(is演算子[C#] / instanceof演算子[Java])、無理やり認知(ダウンキャスト)したりしなければならなくなる。そのようなプログラムはオブジェクト指向とは言い難い。
つまるところ、継承はオブジェクト指向とは何の関係もない仕組みなので忘れるべきだろう。それが世界を守るための唯一の方法だ*2。もちろんすべてのクラスをsealed(Javaではfinal)にして第三者による破壊からも守らなければならない。
参考文献
- Object Thinking 第9章
- Composition vs. Inheritance: How to Choose? | ThoughtWorks