オブジェクト指向という言葉には
- オブジェクト指向分析(OOA)
- オブジェクト指向設計(OOD)
- オブジェクト指向プログラミング(OOP)
の三つの意味があります。 オブジェクト指向初心者泣かせです。
ここではオブジェクト指向設計を説明します。
ソフトウェアの設計
ソフトウェアの設計には二つの側面があります。
- 作成するソフトウェアの共通部分を探し出しモジュール化する
- 作成するソフトウェアが将来変更される部分を抽象化し変更しやすくする
一つ目のモジュール化は構造化設計からある手法です。 オブジェクト指向設計で特に取り上げる点はありません。 ここでは二つ目の将来の変更のために抽象化することに重点を当てます。
オブジェクト指向設計
オブジェクト指向設計とは多態を実装する部分を決めることです。
多態とはオブジェクト指向言語を活用した次のものです。
- 変更可能な点に抽象クラス*1 (オブジェクト指向言語の機能)を定義する
- 呼び出し元のオブジェクトは抽象クラスのインタフェースを使って呼び出し、実装オブジェクトを隠す(カプセル化)
抽象クラスのインタフェースにしたがっている限り実装オブジェクトを変更しても、呼び出し元オブジェクトを変更する必要が無くなります。これを実装クラスが「形を変える」ところから、「多態(ポリモフィズム)」と呼びます。
- 呼び出し元オブジェクトと実装オブジェクトの依存関係を切り離すことができる
- 抽象クラスと実装クラスを二つ定義するため実装の手間は増える
どこに抽象クラスを定義するか慎重に選ぶ必要があります。 そこで必要なのがオブジェクト指向設計です。*2
どうやって決めるのか?
多態を実装する部分ををどうやって決めていくのでしょうか?
オブジェクト指向設計は次の手順で進んでいきます
- 抽象的なオブジェクトに分ける
- オブジェクトを具体化する
2の具体化の作業を進めるときに変更されそうなオブジェクトを選び出します。
抽象的なオブジェクトに分ける
オブジェクト指向では作成するソフトウェアを「オブジェクト」という単位に分けます。 手続き型との違いは、状態と振る舞いを組として扱うことです。
オブジェクトは以下の性質を持ちます。
- アプリケーションの領域を構成する要素
- 状態(値)を持つ
- 振る舞いを持つ
オブジェクトを具体化する
オブジェクトを具体化するとき、オブジェクトを見る観点を変えていきます。
UML モデリングのエッセンス *3 ではオブジェクトモデリングには三つの観点があると書かれています。
- 概念
- 仕様
- 実装
観点のレベルが下がるにつれ具体的なオブジェクトになります。
概念的観点 - 概念オブジェクト
概念的観点を利用する場合、当のドメインにおける概念を表すダイアグラムを作成します。これら の概念は、それを実装するクラスに自然に結びつきますが、直接な対応関係が存在しないこともま まあります。実際、概念モデルは、それを実装するソフトウェアをほとんど、あるいはまったく無 視して作成されるべきです。そのため、概念的観点から書かれたダイアグラムは言語に依存しない と考えることできます(CookとDanielsはこれを本質的観点と呼びました)。
意味が分かりません*4。個人的に概念的オブジェクトは以下のようなものと考えています。
仕様的観点 - 仕様オブジェクト
実装的観点 - 実装オブジェクト
- 責任を実現する具体的な処理です。
- ソースコード上は実装クラスに定義する処理で表現します。
具体例を考える
もう少し具体的な例を考えてみましょう。
概念レベルでは、将来を含めて対応する責任を決める
同じ名前のオブジェクトでもアプリケーションによって責任が異なります。 「社員」という概念オブジェクトを例にします。
- 勤怠管理システム。正社員オブジェクトやパートタイマーオブジェクトが含まれ始業時間や就業時間を返すことを責任とします。「掃除のおばちゃん」オブジェクトは含まれません。
- 入室管理システム。「掃除のおばちゃん」オブジェクトも含まれ、入室可能時間を返すことが責任になります。
概念レベルの難しさはどこまで具体的に決めればいいかわからない点にあります。 当初勤怠管理システムとして構築されたシステムが、その後の機能追加で入室管理システムの機能も持つかもしれません。
- 曖昧に決めると、仕様レベルを決めるときに考慮する責任が増えすぎる
- 将来の可能性をしらみつぶしすると、いつまでも決められない
これに対するコツは、これは「対応しない」責任を決めることです。 上記の例であれば「勤怠管理システムなのでパートタイマーに対応することはあっても、掃除のおばちゃんに対応することはないだろう。」と決めます。間違えるかもしれませんが決めます。*7
仕様レベルでは、責任を実現するインタフェースを決める。
概念で対応すると決めた責任に「必要十分」なインタフェース仕様を目指します。
- 将来変更がないオブジェクトは抽象クラスが無いとシンプルで振る舞いがわかりやすく保守性が高い
- 将来変更されるオブジェクトに抽象クラスを定義しておけばクライアントクラスを変更せずに、実装クラスを修正・変更できる
これをオープン・クローズド・プリンシパル(開放閉鎖原則)*8といいます。
これを決める過程がオブジェクト指向設計です。 *9 *10
実装オブジェクトでは、今回のリリースで対応する責任を実装する。
今回のリリースで対応する責任を満たすなるべく小さく簡単な実装を目指します。 将来の変更は既に仕様オブジェクトで吸収しています。 YAGNI*11を実現できます。
これをアグニマーヤーと呼びます(うそです)。
良いオブジェクト指向設計とは?
オブジェクト指向設計が上手く行っているか見分けることはできるでしょうか?
コメントとドキュメント
良いオブジェクト指向設計では、各抽象クラスのコメントに「どういう時に実装クラスを変更するべきか」が書けます。 変更手順を書いて、変更理由に対して変更されるクラスが一つであればオープン・クローズド・プリンシパル(開放閉鎖原則)が実現できています。 クラスが変更される理由が一つであればシングル・レスポンシビリティ・プリンシパル(単一責任原則)が実現できています。
オープンソース
gitのメンテナの濱野さんは曰く*12
特にオープンソースが他と違うのは「後で拡張してもらえる基礎ができている」
変更点が明確でなければ協力者は集まりません。 参加者が多いオープンソースプロダクトは良いオブジェクト指向設計ができていると言えるでしょう。
まとめ
オブジェクト指向設計では拡張できる場所を抽象クラスで表現する
ただし、変更の仕方はクラスでは書けないのでコメントやドキュメントで書きます。
オブジェクト指向設計は遠い世界の偉い人が決めたものではなく、 現場のプログラマが日々ソフトウェア開発を行う中で積み重ねられた工夫から発見されたものです。 「よりきれいなソースコードを書く」努力がそこへ至る最短経路です。
参考文献
ここまで読んでよくわからなかったら「オブジェクト指向のこころ」を読んでください。おすすめです。
*1:JavaやC#では抽象クラスの代わりにインタフェースを使ったほうが設計意図が明確なります。
*2:DIコンテナを使えば、すべてのクラスを抽象クラスで定義しDIコンテナで実装クラスを設定することは可能です。しかし、初期の実装コストは大きくなり、どの実装オブジェクトが使われているか意識しながらデバッグする必要があり保守性が下がります。設計が効果を挙げているとはいえません。どこを変更可能にするか決める必要があります。
*3:第2版まで、第3版には書かれていません
*4:「CookとDanielsはこれを本質的観点と呼びました」と書かれていますが、引用元文献読むとだいぶ違うことが書いてあってびっくりします。マーチン・ファウラーは人のアイデアにもう一捻りして中二的な名前をつけるのがすごく上手いです。
*5:責任て単語を見るたびに「オブジェクトの責任」ってなんだよ意味分からねえ!と思うのですが、Oxford Dictionariesによると英語の「Responsiblity」の意味は「誰かの承認を得ることなく判断、行動できる能力や機会」だそうです。
*6:JavaやC#ではインタフェースを使います。Rubyには抽象クラスの文法はなく代わりにDucktypingをサポートしています。ですが、Ruby 1.9.3 デザインパターン速攻習得 ストラテジ[Strategy][Design Pattern] - 酒と泪とRubyとRailsとのFormatterクラスのように実装は可能です。やる意味があるかどうかはソースコードで表現したいか規約で表現したいかチーム毎に異なると思います。
*7:何を責任にするかは、KKD(間と経験と度胸)で決めるしかありません。この段階で厳密性を求め時間をかけても後で修正することが多いです。設計・実装を進めると作るアプリケーションへの理解が深まり新しいオブジェクトを発見できるようになります。新しいオブジェクトを見つけたら「手戻り」を恐れずに概念レベルのオブジェクトを見直しましょう。最終的にそれが「手戻り」を減らします。また、いつでも戻れるつもりで次の段階へ進みましょう。ソフトウェアの設計とは概念と実装の間を行ったり来たりしながらアプリケーションと扱う領域を理解していく作業です。経験を積めば最初から正しくオブジェクトを見つけられる確率は上がります。しかし100%にはなりません。ソフトウェアはのコピーコストは0(!)です。二度同じソフトウェアを作ることはありません。
*8:Bertrand Meyerが提唱した設計の良し悪しを測る原則。「ソフトウェアの構成要素は、拡張に対して開いていて、修正に対して閉じていなければならない。」という何を言っているのか分からないと思うが、俺にも何を言っているのかわからない、言葉遊びが過ぎる原則です。
*9:実際は「将来の変更を予測しない」戦略も可能です。最初のアプリケーションをすべて実装クラスで実装します。仕様変更により修正する際に「修正が入った箇所は将来も変更される可能性が高い」と仮定し抽象クラスを追加します。ただしクラス構成を変更する高度なリファクタリング技術が必要です。
*10:もしリファクタリング技術が無いのであれば、一回設計・実装した後に再び概念レベルから設計しなおせば、一度目の開発で得た知識に基づいたより適切なオブジェクトモデルを得ることができます。
*11:エクストリーム・プログラミングで提唱された原則。「You ain't gonna need it」の略、直訳すると「それは必要にならない」。実際に必要になるまで実装しないことで、使われないソースコードを作るムダ、使われないソースコードをテストするムダ、複雑なソースコードを保守するムダ、リリースが遅くなる時間のムダを減らそうとする考え方です。
*13:厳密には型付けの強いクラスベースオブジェクト指向言語での話です。RubyのようなDucktypingをサポートした動的型付け言語やJavaScriptのようなプロトタイプベースオブジェクト指向言語の場合はもう少し議論が必要だと思います。