継承とコンポジット
id:happy_ryoに「わかんねーんだよ、説明してみろゴルァ」されたので、書いてみる。
前書き*1
とりあえず、本日のエントリのキモを最初に。「オブジェクト指向は、隠す技術である」(俺談w)ということを意識して読んで見てください。隠すとは何か? 公開しすぎない事。分かりやすく言えば、publicをprivateに変える事。これが「隠す」。
んじゃあ、隠すと何が良いのか。あるクラスから、見えるもの(=操作できる可能性の範囲(scope))が狭ければ狭いほど出来る事のバリエーションが減るから、プログラムは単純になる。つまり、隠すと複雑性(complexity)が下がる*2。
要件を満たした上で、いかに型(class, interface, etc.)やメンバ(field, method)の可視性を落とすか。可能な限り可視性を下げ、プログラムを単純化し、メンテナンス性を上げるにはどうしたらいいのか。それがオブジェクト指向のテクニック。
よく「グローバル変数は悪」というお題目がありますが、これは超デカい可視性を持った変数だから。だって、どこからでも無条件に見えるんだから最大級の可視性だよね。同様にstaticの変数やメソッドも、通常の変数よりも広いスコープを持つため、濫用は問題視されることが多いです。
しかし、可視性を下げることによってメンテナンス性は上がるのだが、逆にコーディングは難しくなってくる。例えば、グローバル変数であれば、どこからでも見えて、使いたいときにいつでも使えたものが、可視性を落とすことによって、使える場所が限られて来る。その場所で本当に使いたいのならば、インスタンスの持ち回りを工夫したりしなきゃいけない。闇雲にやっていくと、スパゲッティになってゆく。
こういった事が起こらないように、上手に隠す方法論、これがオブジェクト指向のテクニック(例えばデザパタとか)です。
コードを書く力を「実装力」と「設計力」に分けて考えてみる。コードを読む(変数やメソッドを参照する)力が実装力、見る技術だ。逆に、コードを読ませる(参照されるための変数やメソッドを定義する)力が設計力、見せる*3(逆に言えば隠す)技術だ。
具体的には、メソッドの中身を書いている時に実装力を発揮していて、型やメンバの定義をしている時には設計力を発揮する必要がある。注釈で書いた通り「自分に対して見せる」事もあるので、メソッドの中身を書く時にも小さな設計力を発揮しているハズだけどね。
「オブジェクト指向は教えないでください」ってなんてよくわかった会社だろうか - きしだのはてな
見ることが出来なければ、見せることもできない。きしださんの言っている事に、とっても同意。オブジェクト指向を教えたくなっちゃう気持ちも分かるんだけどね、面白いし、なによりも「キャッチー」だから。
あー、前置きが超なげぇw 本題に入ります。
継承とコンポジ
この「継承」と「コンポジット」という2つのオブジェクト指向用語は、どちらも2つのオブジェクトの「関係」を表している。この辺りは、おもっきし設計力の分野。実装時には、2つのオブジェクトの関係が継承なのかコンポなのかなんてのはあまり気にしない。逆に設計時、2つのオブジェクトに関係を持たせる時、これは「継承」にすべきなのか「コンポジット」にすべきなのかを考える。
- クラスBは、クラスAを継承している。
- クラスCは、クラスAをコンポジットしている。
という感じで話を進めていきましょう。一般論としてよく言われるのはこんな感じ。
継承として例に挙げたクラスAとクラスBの関係は、"B is an A" という関係(is-a関係)が成り立つ。というか、成り立たなければなりません。んーー、成り立たないのに継承関係としてはいけません。
is-a関係というのは、AはBよりも抽象的(BはAよりも具体的)な関係になる。Javaのコードとしては、"class B extends A" になる。
次に、コンポジットとして例に挙げたクラスAとクラスCの関係は、"C has an A" という関係(has-a関係)が成り立つ。コードとしては、下記の通り。フィールドにAを持っている状態。
class C { private A a; // ... }
悪い例
ここで、AとかBとか分かり難いので、Seasar Conferenceでも使ったよくある例え、乗り物を出して来ましょうか*4。
登場人物(?)は以下の通り。
- ドライバー(乗る人)
- エンジン
- 自動車
これらのクラスを作ろうと思う。まず、「ドライバーが直接触るのは車で、ドライバーはエンジン回転を調節できなきゃいけない」という前提がある。そこで考える、超短絡的なコードはこれ。
class Engine { public void pushAccel() { // ... } // ...その他のメソッド } class Car extends Engine { // ... } class Driver { public void drive() { Car car = new Car(); car.pushAccel(); } }
確かにこれで、ドライバーは車を走らせることができる。が、CarとEngineの関係はhas-aのはずが、継承(is-a)になってしまっている。これは悪い例。なぜ悪いのか。「隠せるところを隠さずに見せているから」だ。
ここで「隠せるところ」とは、Engineの「その他のメソッド」である。Driver#drive()メソッドの中で、ドライバーはEngineの「その他のメソッド」が見える必要がない。隠してしまっても要件を満たす事はできる。
良い例
さて、ここでちょっと登場人物を追加。
そして、
以上の状態で、
class Engine { public void pushAccel() { // ... } // ...その他のメソッド } class RotaryEngine extends Engine { // ... } class Car { private Engine engine; public void pushAccel() { engine.pushAccel(); } } class RX7 extends Car { private RotaryEngine rotaryEngine; @Override public void pushAccel() { rotaryEngine.pushAccel(); } } class Driver { public void drive() { Car car = new Car(); car.pushAccel(); } }
見事なis-a, has-aが成り立っているのが分かるだろうか。
- RotaryEngine is a Engine
- Car has a Engine
- RX7 is a Car
- RX7 has a RotaryEngine
これらの英文の評価結果にfalseは無い。仮に、RX7 extends RotaryEngine としてしまったら「RX7 is a RotaryEngine」となってしまう。
さらに、DriverからEngineの「その他のメソッド」がしっかり隠されていて、呼び出す事ができなくなっている。これが良い例。
まとめ
なげーなぁ、今日のエントリw そろそろまとめる。
2つのクラスに関係を持たせる場合、まず継承が可能か(is-aが成り立つか)を考える。そして継承が可能な場合は継承する(※)。is-aが成り立たない場合はコンポジットを選択する。ちなみに、is-aが成り立つケースというのはそう多くないので、コンポジットを選ぶ事が多くなると思う。
これで、以前より複雑性が低くメンテナンス性の高い設計ができるようになると思う。
補足
上記※で示した部分。短絡的に書いたが、is-aが成り立つからといって継承しなければならず、コンポジットを選択してはならない、なーんてことは無い。コンポジットの方が複雑性を下げる効果が高いので、コンポジットで用が足りるならば、is-aであってもコンポジットを選ぶべきだ。
良い例が浮かばなかったのだが、例えば「ガラス」と「コップ」の関係を考えてみる。コップ is a ガラスだ。だから単純に考えると「コップ extends ガラス」になる。
で、ガラスには「割れる」っていうメソッドがあったとする。しかし、クライアント(コップを使うクラス)から見た時、コップは別に割れてくれる必要はない。むしろ割れないでくれw ならば、コップはガラスをコンポジットする形にして、必要なメソッドだけを外に向けて公開してやればいい。
ただし、これをやると、ポリモーフィズムは使えなくなる。それが必要ならば、まぁ継承しておくのが無難だろう。なーー、わかりにきぃな、最後w