April 01, 2013
委譲か継承か、それが問題だ¶
この記事は 2013/4/1 に旧ブログの "Syn の独り言" で記載したものを加筆、修正しました。
なんか、「動的型付け言語では継承はダメ、委譲を使え」みたいな意見を聞いた。
いや、Rails だって、model は ActiveRedord 継承して作るじゃん。お前、David さんディスってんのかよ。
まあ、冗談は置いといて、その人の周りに継承を使いすぎる人がいたのかもしれない。 僕が彼の意見を曲解しているのかもしれない。 でも、ちょっと気になったので今の僕の考えをまとめてみた。
まず、話のスコープをはっきりさせよう。
「オブジェクト指向」というと、少し範囲が広すぎる。今回は、Java, Python, Ruby 等を前提に考えてみる。
僕の意見はこうだ。
まず、オブジェクト指向とかプログラムとか置いといて物作り一般に広く言える事だが、
大きい物を作りたければ、疎結合で抽象化されている小さいパーツに分けろ。
疎結合とはパーツ外部からパーツ内部に対して与える影響が少ない状態の事。 逆に、影響が多い状態は密結合。
抽象化の定義を書くと色々と長くなるが、簡単に言うと「抽象化されている」とは「直感的に使える」くらいに考えて欲しい。
人間、一度に多くの事を考える事は苦手だ。 また、頭のなかに有る作業用メモリが溢れると途端に難しくなる。
大きい = 複雑 = 難しい
くらいに考えて良いだろう。
そこで、疎結合で抽象化された小さいパーツに分けるのだ。 疎結合で小さければ、各パーツを作っている時にそのパーツだけに専念出来る。
IT のシステムに置いてパーツを疎結合にするには、パーツが外部とやり取りをする。
インターフェースで外部から内部に出来る事を制限すれば良い。 (ここで言うインターフェースとは、Java の interface ではなく外部との接続口という意味。)
インターフェースによる制限が厳しいほどパーツは疎結合になり、制限が緩いほどパーツは密結合になる。 また、このインターフェースが直感的であれば、そのパーツは抽象化されていると言えるだろう。
パーツの内部のやり取りの制限は緩くてよい。 というか、内部のやり取りを厳しく制限出来るならば、さらに小さいパーツに分ける方が良いだろう。 (もちろん、パーツが充分小さければ分けないという選択肢もあり得るが。)
具体例を上げよう。 例えば、ある処理には速度向上のためにバッファリングが必要な場合。 その処理自体を行う為のパーツを用意したとして、バッファリングはどこで行うか? 今回は、バッファリングをあるその処理を行うパーツ自体に持たせる事を考えよう。 この為にはバッファをそのパーツ内部に持ち、そのバッファを扱うインターフェースを用意しなければ良い。
上記の制限を行うと、どうなるか?
まず、パーツ内部を実装する際に「外部からバッファを操作される」という可能性を考えなくて良くなる。 また、外部からこのパーツを使用する際もバッファリングという難しい処理を忘れて直感的に使えるようになる。 内部の実装時も、外部の実装時も同時に考える項目数を減らす事ができるのだ。
後でバッファリングに関する部分を改修する時にも、この事は役に立つだろう。 改修時にはパーツ内部だけ考えれば良い。 なぜなら外部ではバッファリングの事を考えなくても良いようにしたから。
もちろん、制限を加えた事によって悪い事も発生する。 今回の場合、外部からはバッファリングの細かいチューニングが出来なくなった。 この事による速度の低下が致命的な場合は、この制限を加えたのは間違いだったのだろう。 前の方に書いたが、「可能な範囲で」疎結合にするのだ。
さて、ずいぶん前置きが長くなってしまったがここからクラスの話に戻る。
クラスとは、プログラムにおけるパーツの一種。
「一種」と書いたのは、パーツの分け方が一通りでは無いから。 (ファイルを分けたり、関数で分けたり。) 多くの場合、public method がクラスの内部と外部のやり取りをする為のインターフェースとなる。
では、クラスの委譲、継承とは何か?
どちらも、小さいクラスから大きいクラスを作る方法である。 両者の違いは小さいクラスに対する大きいクラスのアクセスの方法にある。
委譲は小さいクラスへ、そのインターフェースを通してアクセスする。 継承は小さいクラスの内部へ直接アクセスする。
委譲のメリットは、何と言っても小さいクラスと大きいクラスの疎結合性を保てる事だ。 小さいクラスで外部と疎結合にするインターフェースを実装できるならば小さいクラスと大きいクラスが疎結合になる。
逆にデメリットは、実装する必要のあるインターフェースが増える事だろう。 委譲の場合、大きいクラスでは、外部に公開するインターフェースを全て実装しなくてはいけない。 たとえ、ほとんど同じインターフェースが小さいクラスにあったとしてもだ。 また、小さいクラスでは大きいクラスが必要とするインターフェースを全て実装しなくてはいけない。
では、継承のメリットは?それは、実装時のステップ数が減る事だ。 小さいクラスではインターフェースを実装する必要は無い。 インターフェースを含めて小さいクラスで実装済みの物は、ほとんど全て大きいクラスで流用できる。 一つの子クラスを複数のクラスで流用すれば、全体のステップ数が減る。
継承のデメリットは、親クラスと子クラスが密結合になる事だ。 親クラスがどんなに優れたインターフェースを持っていたとしても、 子クラスはそのインターフェースを使わないので密結合になる。 n nでは委譲と継承はどうやって使い分けたら良いだろう?
継承は「異なるクラスの共通部分を外出しして DRY にする」為に使う。
上図において、class A と class B は青色の部分が共通している。 それだったら、青色部分を外出しして共通の親クラスとすれば DRY になる。
外出ししたクラス(親クラス)は、パーツとして必要な機能を全て実装しているわけではない。 (Java の抽象クラスなんかが、その典型。) そういう意味で、「小さいクラス」だ。 継承は、小さいパーツに不足している機能を付け足し、肥大化させる事で大きい(必要な機能を全て揃えた)パーツを作る。
委譲は「クラスをより小さい、疎結合なクラスに分割して実装する為」に使う。
上図のように、class A の中で青色部分と緑色部分に分かれており、互いのやり取りの方法が限定されていたら? そうやって疎結合な部分に分かれているのならば、より小さいクラスに分けて実装する事で同時に考えなくては行けない事がもっと減るかもしれない。 (青を実装中は緑を考えなくてよい。)
小さいクラスは、インターフェースを含めて小さいパーツに必要な物を全て備えている。 委譲は、その小さいパーツを組み立てて一回り大きいパーツを作る。 委譲によって後で組み立てられる事が保証されているから、安心してクラスを細かく分割できるのだ。
継承の本質は、重複を避けて DRY にする事だ。 複数クラスで重複した部分が無ければ、継承をする意味が無い。
委譲の本質は、大きいパーツをより小さい疎結合なパーツに分けて実装する事だ。 小さいクラスを複数のクラスで流用出来なかったとしても、委譲する意味はある。 (もちろん、複数クラスで小さいクラスを流用できるならば、するに越した事は無い。)
GoF を初め、多くの人が「継承よりも委譲を」と言っているのは、下記のような場合だと考えている。
クラス A は、青い部分と緑の部分に疎結合に分ける事が出来る。 クラス B は、青い部分と赤い部分に疎結合に分ける事が出来る。
この場合、青のクラスを共通の親クラスとしてクラス A と B で継承で実装する事もできる。 青のクラスを作ってクラス A と B で委譲で実装する事も出来る。 こういう場合は、委譲にするべきだろう。
継承か委譲か迷ったら、試しに青のクラスを作ってみると良い。 (これは、継承でも委譲でも、どのみち必要になる。) 青のクラスが疎結合になる制限の強いインターフェースを含め、パーツとして必要な機能を全て備えたクラスになった場合、継承という方法でその疎結合性を壊す必要は無い。 委譲を使えば良い。 そうならなければ、継承を使う。
ついでに、緑と赤もパーツとして必要な機能を備えた、疎結合なクラスを作れるのならば作ると良いだろう。 たとえ青の部分を継承で実装する事になったとしても。 たとえ緑と赤のクラスを他の部分で流用出来なかったとしても。
また、よく「is-A ならば継承を、has-A や use-A ならば委譲を使え」と言う。 これは、 「has-A や use-A の場合、A がロジック的に分かれているわけだから、実装時も疎結合なクラス A として分けられる事が多い。 is-A ならば A がロジックとして分かれていないから、実装時も疎結合なクラスに分けられない事が多い」 という事ではないか? 僕の経験としても、そうなっている事が多い。
さて、一番最初に話を戻して、僕が違和感を感じた「動的型付け言語で継承はダメ、委譲を使え」という意見について。
彼は
という結論に至っていた。
それは違うだろう。 だいたい、Java だって Interface を使えば親のクラスの型を引き継ぐ必要はない。 ステップ数を減らすための怠慢は、優れたプログラマに必要な物だ。
彼は、 「委譲を使えば疎結合に、継承を使えば密結合になる」 と考えているのではないか?
僕は、 「疎結合な小さいクラスを作ったという前提の元で、委譲を使うとその疎結合性を保つ事ができる。 継承を使うと疎結合性を保つ事が出来ない。」
つまり、 「もともと疎結合に分ける事ができなければ、委譲にしても継承にしても疎結合にならない。」 「どうせ密結合になるなら、継承の方がステップ数が短くなるから良いのではないか?」 と考えている。
もっとも、「どうせ密結合になるなら継承の方が良い」というのは、実は少し眉唾だ。 ただ、今回の話のスコープである、Java, Ruby, Python の場合は十分に正しいだろう。
最後に、僕が大きいシステムを作る際の作業フローを紹介してみる。
まず、僕は大きいシステムを一度に実装できるほど優れた人間ではない。 最初に、僕の手に負える程度のサイズまでシステムを疎結合なパーツに分ける。
何もクラスという形で分けるとは限らない。 小さいパーツは、より小さいシステムかもしれないし 「人間の目視確認」というワークフローかもしれない。 (人間による作業を増やすのは良い事じゃないけれど。)
また、パーツを分けたら今度は既存の物をそのパーツとして使用できないか考える。 既存のシステムとか、有名なミドルウェアとか、誰かが書いたライブラリとか、プログラム言語のビルトインのシステムとか。 (というか、パーツ分けの段階で既にこの辺りは意識している。)
流用出来ない部分は、自分で実装するしかない。 自分の手に負える範囲まで小さく分けてから実装の開始だ。
各パーツを実装する際は、できるだけ DRY に、できるだけシンプルに行いたい。 多くの関数内で共通化できる部分があれば、その部分を外だしして別の関数を作るだろう。 多くのクラスで共通化できる部分があれば、親クラスを1個作って継承させるだろう。
細かいパーツができたら、今度はそれを組み立てる。 ミドルウェアと組み合わせる場合はドライバを使うかもしれない。 システム同士を組み合わせる場合は API を使うかもしれない。
クラスとクラス、クラスと関数を組み合わせる場合は? その時は委譲を使うだろう。 個々のクラスは外部と疎結合になっているはずなので、それを保ったまま組み立てるのだ。
無事に組み上がってテストが通ったら、システムは完成する。 もっとも、上記のウォーターフォール的な流れを 1回行うだけで全て上手く行く事は少ない。 先に大きいクラスを作ってから小さいクラスを作る場合も有る。 作りながらリファクタリンングを繰り返して、パーツを組み立ててからパーツを再作成するかもしれない。 でも、何となく分かってもらえ無いだろうか?
ちなみに、多重継承はどんな時に使うだろう? 使うとしたら、上記の作業フローで言うと、単体継承と同じフェーズだ。 例えば、下図のようなクラスを作る場合。
各クラスの同じ色の部分は同じ内容。 しかし、赤、青、緑、黄のそれぞれは密結合。 この場合、赤、青、黄、緑のそれぞれの部分をクラスとして実装してそれを多重継承する方法もある。
ただ、自分でこれを実装するのは、非常にしんどい。 例えば、多重継承を使わない場合、クラス A の青い部分を実装している時は青と緑の部分だけ考えれば良い。 でも、多重継承を前提に青いクラスを実装する場合は青、緑、赤を全て一度に考える必要がある。
これを読む方は実装する人以上にしんどいだろう。 丁寧なドキュメントを用意する必要がある。
その他、多重継承のメソッド探索順序は非常に複雑になる場合も多い。 それについてはPython 菱形の継承にも書いたので良ければ見て欲しい。
多重継承を使用するのは、上記の労力を払ってでも実装する価値がある場合のみだ。 その多くは、汎用性の高いライブラリやフレームワーク、ビルトインや標準ライブラリのクラスだろう。
代表的な例は Ruby だと思う。 正確に言うと Ruby は単体継承しかサポートしていないが、module を複数 include 出来る。 これは、実質的にはメソッド探索順序を単純化した多重継承みたいなもんだ。 Ruby の場合、多重継承もどき(include)を前提とした親クラスもどき(module)が標準ライブラリにあふれているので、ユーザーも多重継承もどき(include)を行う事も多い。 でも、普通は多重継承を使う事は滅多に無いんじゃないかな?