皆さんはReaderWriter問題をご存知でしょうか?
オブジェクト指向の話題でたまに出る話で、ReaderクラスとWriterクラスがあるとき、それらを組み合わせてReaderWriterクラスを作るとしたら、どのように設計すべきか?という問題です。
ふと思い出したので、私なりの答えを紹介したいと思います。
なお、ここで話を楽にするためにReaderもWriterもファイルを扱うものとします。
※なお、クラスは「単一責任」であるべきだ!二つの機能を持ってしまったら単一責任ではないので、そのようなクラスは作るべきではない!なんていわないでくださいね。
理由を述べると、「責任」を何とするかがポイントで、「ファイルアクセス」を責任と捉えるならば、立派な単一責任になります。(GOFのFacadeパターンとか勉強してもいいかもです)何より、多くの人はオブジェクト指向をしたいのではなく、オブジェクト指向を活用して生産性を上げる(コードを再利用したり、可読性を上げたり、テスタビリティを向上させたり、等)事が目的だと思います。稀にオブジェクト指向をすることが目的になっている方がいますが、そのような方は是非ご自宅でして頂きたいと思います。
#さて、ではどうしようか?
C++言語だったら「多重継承」という機能があるので、以下のようにしちゃえば一発です。
しかしながら、だいぶ大古の時代より「多重継承ってよくないよね~」と言われており、Javaを含む最近の言語(Javaはもはや最近じゃないか)では多重継承はできなくなっています。
ならどうしたらいいでしょう?
Readerクラスを継承して、ReaderWriterクラスにWriterと同じものを書く?
それともWriterクラスを継承してReaderWriterクラスにReaderと同じものを書く?
どっちもコードの再利用ができないですね。
#継承(inherit)という言葉の功罪
オブジェクト指向を語るとき、必ず継承というワードが出てきます。C#なんかだと、inheritsというキーワードもありますよね。
しかし、これは問題があると思っています。
オブジェクト指向設計での「継承」とオブジェクト指向言語での「継承」は分けて考えるべきです。
実際、Javaが継承という言葉を使わず、extends(拡張)という言葉を使っている理由は、ここにあると思います。
#縦の継承と横の継承
こういわれてピンとくるでしょうか?
下の図の左側は縦の継承です。馴染みのあるやつですね。Javaならextendsです。右側は横の継承です。ChildがParentを所有して、ラップしています。
Child#method()の中身は、
method() {
parent.method()
}
のようになる事でしょう。
method()の中に親のmethod()を書く手間がありますが、コードの再利用は達成できています。
(引数とか戻り値に変更あったら変えなきゃだけど、全部書くよりははるかにましです)
これを使うと、ReaderWriterクラスのクラス図は以下のようになります。
#もうちょっと綺麗にしたい。
このクラス図だと、コードからはReaderを継承している事が読み取れますが、Writerについては読み込まないとわかりません。
何より、
Reader r = new ReaderWriter();
はできても、
Writer w = new ReaderWriter();
はできません。
#インタフェースを分離しよう
ここでの問題は、具象クラスとインターフェースが切り離されていないことにあります。
以下のようにインターフェースと具象クラスを分ける事ができます。
Concreteは具象クラスという意味です。
操作(インターフェース)と実装(具象クラス)を分ける事で、コード上必要なものがなんであるのか、わかる、という利点があります。
例えば、JavaなんかだとListインタフェース(順番を保ったオブジェクトの集合)があり、その具象クラスにはArrayList(配列による実装)、LinkedList(次要素へのリンクを保持するタイプの実装)があります。ArrayListは間にオブジェクトを挿入しようとすると、後ろを全部ずらさなければいけないが、LinkedListはリンクの付け替えで済むため、どのような使われ方をするかによって、具象クラスを選びます。
が、もしそこで必要とされている事がListで表現されるもののみであり、どのように実現されているかはどうでもいい場合は、変数はインタフェースにした方が、後々改変する際に改変の自由度が上がります。
#それを踏まえて、答え
はい、私の答えは以下のようになります。
ReaderWriterはReadableに直接矢印がつながっていないですが、つなげてしまっても問題ありません。
このようにしておけば、
Writable w = new ReaderWriter();
も問題なく行えます。
#最後に
という訳で、私なりの答えを紹介させて頂きましたが、これが別に絶対的な正解というつもりはありません。
この記事で一番言いたかったことは、「継承」という言葉に惑わされるな、という事です。
そして、このような設計も、あくまで「自分たち(またはお客様含む関係者)にとって有意義であるか?」が一番大事なことだと思います。
複雑なプログラムをこのような手法で単純化できるのであればどんどんやるべきだと思いますし、逆に複雑になって大変だよ…というならやらないほうがいいです。
あくまでケースバイケースなのですが、このような整理法もある、という事は知っておいて損はないと思います。