継承には2つの意味がある
Javaのような静的型のオブジェクト指向言語の変数には,変数を介して呼び出されるメソッドを制限する働きがありました。ただし,制限がかかるのは「どのようなメソッドを持っているか」であって,「どのように実装されているか」ではありません。
今まで一まとめにして継承と呼んできましたが,実は継承には2つの異なる概念が含まれています。一つは,「どのようなメソッドを持っているか」あるいは「どのように振る舞うか」ということに着目した「仕様の継承」です。
もう一つは「どのようなデータ構造を使い,どのようなアルゴリズムで処理するか」ということに着目した「実装の継承」です。
静的型言語では両者の区別が重要になります*4。Javaでもこの2つを明確に区別しており,実装の継承はスーパークラスとして「extends」で指定します。仕様の継承の方はインタフェースと呼ばれるものを「implements」で指定します。
クラスがオブジェクトの実装を指定するものであるのに対して,インタフェースはオブジェクトの外見(どのようなメソッドを持つかなど)だけを指定します。
Javaでは「extends」(実装の継承)ではスーパークラスを1つだけしか指定できません。そのため,実装の継承では単一継承となります。クラス関係が木構造に制限できるため,クラスライブラリの構成がシンプルになります。
一方,「implements」(仕様の継承)では複数のインタフェースを指定できます。インタフェースでは「オブジェクトをどのように扱いたいか」を指定します。
具体例として,java.util.Collectionという名前のインタフェースの階層構造を見てみましょう(図6[拡大表示])。java.util.Collectionは,オブジェクトの集合(コンテナ)の持つべき振る舞いを定義するインタフェースです。継承するインタフェースは2つあります。順序付きで要素を格納するjava.util.Listインタフェースと,重複のない集合であるjava.util.Setインタフェースです。つまり,java.util.Listやjava.util.Setをimplementsしたオブジェクトはjava.util.Collectionとしても扱うことができます。
インタフェースは実装を制限しません。つまり,インタフェースは実装の継承とは無関係に任意のクラスからextendsされることができます。つまり,これらのインタフェースを実現するクラスは,任意のクラスをextendsできます。例えば,java.utilパッケージでは,java.util.Listの実装として配列を用いた実装であるjava.util.ArrayListクラスと,リンクリストを用いた実装であるjava.util.LinkedListが提供されています。これらはいずれも直接Objectクラスをextendsしています。
インタフェースにも不満がある
仕様の継承と実装の継承の区別は,論文レベルでは古くから知られていました。しかし,広く使われる言語に組み込まれたのはJavaが初めてだと思われます。多重継承問題に対するJavaの解答と言えるでしょう。静的言語における多重継承の必要性を満たしながら,多重継承のデメリットであるデータ構造の衝突やクラス階層の複雑化などを回避しています。
しかし,インタフェースが完璧な解決策かと問われると,そうは言い切れないでしょう。インタフェースに対して不満が残っているからです。ずばり「実装を共有できないこと」が問題なのです。
多重継承の問題を回避するために,仕様の継承についてのみ多重継承を許したのですから,実装の継承が単一継承しかないことに文句を言うのはどうか,とも思いますが,一ユーザーとして不便なものはやっぱり不便です。Javaでは実装の共有の問題への対応として,単一継承のまま,共通する機能を実装する別クラスを作り,それを呼び出すCompositeパターンを推奨しています。
しかし,ただ単に継承の階層を越えてコードを共有したいだけなのに,わざわざ別の独立したオブジェクトを作り,メソッドをそのオブジェクトにいちいち転送するのも面白くない話です。実行効率も高いとはいえません。
実装を継承する方法
静的型言語であるJavaなどとは異なり,動的型言語には仕様の継承という概念がそもそも存在しません。解決しなければならないのは,実装の多重継承だけです。
動的型言語では実装の継承について,どのように対処しているのでしょうか。Lisp,Perl,Pythonでは単純に多重継承を提供しています。これで単一継承の問題はなくなります。多重継承で起こりうる問題に関しては「気を付けて使ってね」という立場のようです。
多重継承を変形したMix-in
RubyはJavaとも他の動的型言語とも違った手法を採っています。Rubyはモジュールを使ったMix-inという方法で多重継承の問題に対応しました。
Mix-inというのは元々Lisp界で始まった多重継承の使い方です。Mix-in手法には次の2つの条件があります。
●通常の継承は単一継承に限る
●2つめ以降の継承は,Mix-inと呼ばれる抽象クラスからに限定する
Mix-inクラスは以下のような特徴を備えた抽象クラスです。
●単独でインスタンスを作らない
●通常のクラスから継承しない
これらの規則に従うことで,クラス階層は単一継承と同じ木構成になりますし,機能の共有を実現するには,共有する機能だけを持つMix-inをクラス階層木に「差し込む」ことで達成できます。インタフェースを使って仕様の継承問題を解決したJavaの手法を,実装の継承に対して適用したと考えることができるでしょう。
Mix-inの実例を見てみましょう。図7[拡大表示]は,図2と図3で取り上げたSmalltalkのStreamと同等の構造を,Mix-inで構築したものです。
Mix-inを用いたクラス構成では,Streamの下に3つのサブクラスを作るだけです。その上で,実際の入出力機能はReadable(入力),Writable(出力)という2つのMix-inに実装します。このMix-inをそれぞれのサブクラスに継承させることで,入力,出力,入出力という3つのクラスを実現しています。
Streamのクラス階層だけを見るとスーパークラスのStream,入出力を担当するサブクラスReadStream,WriteStream,ReadWriteStreamというように明快な木構造になっています。クラスの構成がネットワーク状になっておらず,単純です。さらに,共有されるコードはMix-inにまとまっているので,コードのコピーもうまく避けています。
Mix-inは,一般的な多重継承と比べてクラス構成を単純にできる優れたテクニックと言えます。Mix-inというルールを導入して継承を制限し,多重継承をいわば「飼いならす」わけです。
ちょうど構造化プログラミングが任意のgotoを制限して分岐とループを導入したのと同じです。Mix-inは多重継承を備えた言語ならどれでも使えるので,覚えておくと良いテクニックでしょう。
Mix-inを自由に使えるRuby
多重継承をそのまま導入している他の言語と比べ,RubyはMix-inを直接サポートしている点に特徴があります。RubyではMix-inの単位として「モジュール」という構造が導入されているからです。モジュールは,まさにMix-inのための性質を備えています。
●オブジェクトが作れない
●通常のクラスから継承できない
では,RubyではMix-inをどのように使うかを見てみましょう(図8[拡大表示])。これは図7に示したStreamクラスを定義したRubyプログラムです。
Mix-inはmodule文で定義します。module文はクラスを定義するclass文とよく似ていますが,スーパークラスを指定できません。メソッド定義などの方法はクラスと同じです。
モジュールをクラスに取り込むためにはincludeを使います。includeを使うとそのモジュールで定義されているメソッドなどをクラスに継承します。あくまでも継承であって,コピーされるわけではないので,自クラスで同名のメソッドが定義されていた場合,自クラスのものが優先されます。
まとめ
単一継承と単純継承についてさまざまな面から扱いました。最後にまとめておきましょう(表1[拡大表示])。
いやあ,今回は本当に盛り沢山でしたね。次回は表1の内容を踏まえて,静的言語と動的言語について,特に動的言語におけるDuck Typingについて学ぼうと思います。