今回読み進める本は、『アジャイルソフトウェア開発の奥義』です。 第2版までは日本語版が出てます。内容的にはどの版のものを読んでも大丈夫。
第1版
第2版
第3版
ちなみに版が進むごとにサンプルコードの言語がより高水準な言語に変わっています。
- 第1版: C/C++
- 第2版: Java
- 第3版: C#
第1版と第2版の内容はほとんど一緒だけど、第3版には各種ダイアグラムに関する説明の Chapter.14~20 が追加で挿入されています。 C/C++ のコードで読みたい場合は、書店からなくなる前に第1版を買っておいた方がよいです。
以下、各章ごとのポイントや議論のメモです。
Preface(序文)
- 単なるパターン集ではなく、そのパターンが「なぜ」生き残ったのかという過程を知ることが大切。
- 著者 Robert C. Martin(Object Mentor 社の創設者。社長)は、11個のオブジェクト指向の原則をまとめている。それに従って設計することで、デザインパターンですら導き出される。
Section I: Agile Development(アジャイル開発)
▼議論
- Q. アジャイルを大規模な開発に適用できるか?
当初は大規模開発に適さないのでは?という懸念があったが、結果として大規模開発においてもアジャイル開発が主流になりつつある。ただ、いつものことだが日本では普及が遅れている。
大規模プロジェクトにおけるアジャイル開発に関しては、下記の記事や書籍が参考になる。
- IBM Rational アジャイル開発
- (そのうち3分の1は従業員数10,000人以上) の88%がアジャイル・プロセスを使用中または評価中である
- 書籍: 『The Object Primer(邦題: オブジェクト開発の神髄)』
- アジャイルソフトウェア開発の長所の一つ: 規模の大小を問わずうまくいく。
- The Rational Edge (72) アジャイル開発の広範な普及を目指して
- The Rational Edge (28) 大規模プロジェクトにアジャイルを適用する方法
- 「この大規模プロジェクトがアジャイル手法で管理できるのか? 」という疑問が出てくる。その答えはイエスだ。
- アジャイルソフトウェアプロセスを使ってオフショア開発(English)
- アジャイルソフトウェアプロセスを使ってオフショア開発(日本語)
- 分析と設計はオンショアで行い、構築をオフショアで行い、そして受入試験をオンショアで行うというやり方よりも、オフショアのチームにできるだけ多くの工程をやらせると問題が改善される。作業工程に沿って分けるのではなく、機能面に沿った分割を行う。
- バグフィクスからやらせると、開発者は変更するよりも多くのコードを読むことになるので、コードベースに精通することができた。
- オフショア開発においては、ドキュメントを作成するための時間を確保する必要がある。
- 最低限、IM と Wiki、良質の電話回線を用意すること。
Chapter 1. Agile Practices(アジャイルプラクティス)
- プロジェクトをうまくまわすためにルールやプロセスをどんどん導入してしまうと、逆に重くなってうまくいかない。
- 重いプロセスのせいで開発ペースが落ちる ⇒ まだプロセスが足りないと思い込む ⇒ ルールやプロセスを追加する ⇒ 悪循環…
- マネージャーは開発環境を構築する前にチームを構築するべき。環境はチームメンバーに最適化させるのがよい。
- 高価なツールを導入する前に小さなところから始める。
- まずはフリーのツールを実際に使ってみて、本当に使えるのか試してみる。
- CASE (Computer-Aided Software Engineering) ツールの前にホワイトボードやグラフ用紙を使ってみる。
- 大きなデータベースを使う前にプレーンテキストファイルを使ってみる。
- ドキュメントに関して
- Martin のドキュメントに関する第一法則 … 重要で差し迫った必要のあるドキュメント以外は作成しない。
- 読みやすいドキュメントを用意する必要はあるが、多すぎるドキュメントは、少なすぎるドキュメントよりもたちが悪い。ドキュメントが多すぎると、ソースコードを変更したときにドキュメントを修正して整合性を保つのに時間がかかる。メンテナンスされていないドキュメントが存在していると、そこには嘘が書かれていることになり、逆に混乱を招く。ソースコードは嘘をつかない。
- ドキュメントは手短に、洗練されたものだけ用意するのがよい。ドキュメントは長くても12ページ~24ページ(1、2ダース)に保ち、抽象度の高い設計構造を示したドキュメントを用意するとよい。新メンバーが参入した場合に、より詳細な設計構造を教えたい場合は、隣に座ってコミュニケーションを取るのが効率がよい。
- プロジェクト成功の鍵
- 顧客との密接な協調関係を築くこと。
- 「コスト」や「納期」などの条件を決める契約ではなく、「相互の協調関係」について取り決めた契約書を用意する。「近くで一緒に働こう」といったものがよい。
- うまくいった一例としては、「顧客側の受け入れテストに合格した機能ブロックの分だけ対価を支払う」という契約がある。どのように受け入れテストを行うかなどの詳細は契約では決めていない。
- 計画は日程がずれるだけでなく、形そのものが変わっていくもの。
- 詳細な予定 … 2週間先まで決める。
- 大まかな計画 … 3ヶ月先まで決める。
- それ以外は柔軟にしておく。システムがどんな形になるのかを把握するくらいでよい。
Chapter 2. Overview of Extreme Programming
この章には XP の概要が書かれている。
Chapter 3. Planning
ブレイクダウンの順番
- Features (by customer)
- Stories (by customer & developer)
- Development Tasks (by developer)
プロジェクト開始時
- developer と customer は話し合って feature を洗い出し、それぞれの feature を複数の story に分割してインデックスカードに記入する。それぞれの story には相対的な実装コストを示す story point を記入する。より正確な見積もりのため、大きすぎる story は分割し、小さすぎる story は結合する。
- developer と customer で話し合い、iteration ごとの期間を決める。一般的には1週間か2週間。実装の進捗度合いによって iteration の期間が変わることはない。
iteration 開始時
- customer は次の iteration で、どの story を実装してもらいたいかを決める。story の選択は、iteration あたりにこなせる story point 数である velocity の範囲内で決めないといけない。それ以上の story は選択してはいけない。iteration の途中で実装してもらう story を変更してはいけない。
- velocity は通常、前回の iteration でこなせた story point と同じ値にする。
- iteration の中で story をどのような順で実装していくかは、developer が自由に決めてよい。
- developer は story を development task に分割し、story point と同じように task point を付け、メンバーへのタスク分担の目安とする。
- メンバーは自分の得意不得意にかかわらず、好きな task を選んでよい。
iteration 中間点
- iteration の中間点でミーティングを行い、story の半分をこなせているかを調べる。もし遅れているようであれば、task の分担をやり直す。それでも間に合いそうになかったら、customer と話し合い、story (あるいは task)を減らしたり優先度を付けるといった決断をする。
iteration 終了時
- story の実装が終了したかどうかは、acceptance test をパスしたかどうかで決める。各 story の acceptance test は iteration の開始時に作成される。
- task の進捗が 90% であっても、story の進捗が 0% では意味がない。
- developer は iteration ごとに動作するソフトウェアを customer に触ってもらい、見た目はどうか、パフォーマンスはどうかといったフィードバックを得る。このフィードバックは新しい story 作成のために使われる。
- iteration 開始時に計画した story の実装が終了しなかったら、次の iteration の velocity を調整する。
- 技術が向上したり、設備が整えば、velocity は上がる。
- プロジェクトの開始時は velocity が分かりにくく、見積もりしにくいかもしれないが、そこであまり時間をかけてはいけない。3, 4週間すれば平均 velocity が分かる。平均 velocity が分かれば、最初のリリース(多くの場合24ヵ月後)までにどういった feature が実現できるか明らかになる。
メンバー全員のよく見える場所に少なくとも次の2つの表を貼っておく。
- Velocity chart … 横軸に iteration、縦軸に velocity(その iteration でこなした story point)を示した棒グラフ。
- Burn-down chart … velocity chart とは少し違い、縦軸に次のマイルストーン(またはリリース)までの残り story point を示した棒グラフ。story が追加されれば残り story point は増加するし、story の再見積もりによっても変化する。
Chapter 4. Testing(テスティング)
- ユニットテストを書くという行為は、機能検証というより設計に近い行為である。
- テストを最初に書くことは、ソフトウェアを次のようにする効果があり、設計の質が高まる。
- テスト可能な形式。
- 分離された形式。
- テストはコンパイルも実行できるドキュメントである。テストは「常に最新の用例集」としての役割を果たす。
- ユニットテストと受け入れテスト
- ユニットテスト … ホワイトボックステスト。プログラマが読めるようにプログラム言語で記述される。
- 受け入れテスト … ブラックボックステスト。究極の仕様書。顧客が読めるように顧客自身が設計した言語で記述される。
- 1回分のイテレーションの仕様をとり上げて、受け入れテストのフレームワークを作るのはそれほど困難ではない。作っただけの見返りは得られる。
Chapter 5. Refactoring(リファクタリング)
- 書き上げるモジュールも、保守するモジュールもすべてリファクタリングするべきである。
Chapter 6. A Programming Episode(プログラミングエピソード)
- UML ダイアグラムを使うことが適切でない時もある。それは、ダイアグラムを検証するコードを作らず、それに従ってプログラムを作ろうとする場合。
- アイデアの模索に UML ダイアグラムを使うことは問題はないが、できたダイアグラムが最適なものであると思い込んでしまうことに問題がある。
- 最良の設計は、まずテストを用意し、小さなステップの積み重ねで生まれていくもの。
Section II: Agile Design(アジャイル設計)
以下は、貧弱な設計の兆候である。
- 硬さ
- もろさ
- 移植性のなさ
- 扱いにくさ
- 不必要な複雑さ
- 不必要な繰り返し
- 不透明さ
これらの兆候が現れるということは、1 つ以上の原則に違反している可能性が高い。 下記は、オブジェクト指向設計の原則である。
- SRP: Single Responsibility Principle (単一責任の原則 → 8章)
- OCP: Open-Closed Principle(オープン・クローズドの原則 → 9章)
- LSP: Liskov Substitution Principle(リスコフの置換原則 → 10章)
- DIP: Dependency Inversion Principle(依存関係逆転の原則 → 11章)
- ISP: Interface Segregation Principle(インタフェース分離の原則 → 12章)
Chapter 7. What Is Agile Design?(アジャイル設計とは?)
- アジャイル設計とは、ソフトウェアの構造や可読性を向上させるために、原則、パターン、プラクティスを継続的に適用する行為である。 ⇒ 仕様変更にも素早く対応できる。
- 仕様が最も変わりやすいものだということはみんなが知っている。仕様変更が原因で設計が劣化していくなら、それは自分たちの設計やプラクティスが間違っている。
- 仕様は変化するものであり、設計もその変わっていくものなので、アジャイルなチームは初期の設計に時間をかけるようなことはしない。そこに時間をかけるのではなく、ユニットテストや受け入れテストをできるだけ頻繁に行うようにして、変更しやすいように保つべき。
- 新しい要求、仕様変更が来た時点で、今後の変更にも対応できるように設計を改善するのがよい。先を見越して設計をするのは工数の無駄に終わることが多いし、逆に不要なコードが含まれて理解が難しいコードになってしまう。 アジャイルでいう「柔軟」とは、どちらかというと設計のシンプルさに重点を置いている。 汎用性を揚げようとすると、ほとんどの場合、永遠に使われないモジュールであふれかえって複雑なものができあがる。 ⇒ YAGNI: You Aren’t Going to Need It! (あなたはそれを必要としないだろう)
▼議論
- Q. クリーンなコードってなんや?シンプルなコードとは違うのか?
無駄なコードがない。バグっぽいコードがないってことではないか。少しでも汚いコードがあると、そこからどんどんコードが腐敗していくということは 『達人プログラマー』 でも言及されている 。 - Q. 後からたぶん仕様変更があると分かっていて、その対応をあらかじめいれておかないと、変更するときに二度手間にならない?
確実に仕様変更があると分かっているのならあらかじめ対応をいれておけばよいし、その判断は期待値によるのでは?ただ、あらかじめ対応コードを入れておく方法は、逆に仕様変更がなかった場合に意味のない無駄なコードを残すことになるリスクや、結局あとからコードを消さないといけなくなるリスクが常にある。 - Q. アジャイル設計はプロセスではないのか?
翻訳ミスですね。翻訳本では「アジャイル設計はプロセスでもイベントでもない」となっているが、原書では “Agile design is a process, not an event.” となっている。アジャイルプロセスって言葉もあるしね。
Chapter 8. Single Responsibility Principle (SRP) 単一責任の原則
- クラスが複数の責任を持っていることの弊害
- そのクラスを使うモジュールが必要のない部分まで含んでしまう。
- そのクラスを変更すると、そのクラスを使用しているモジュールをリビルド、再テスト、再ロードしないといけない。
- 永続性のあるシステム(データベースなど)と、ビジネスルールは、単一クラスに絶対に混ぜてはいけない。
- 永続性のあるシステムはあまり変化しないけど、ビジネスルールは頻繁に変化する。
- 永続性のあるシステムとビジネスルールはまったく違う理由で変化する。
- クラスの役割の分離は、Chapter 7 で言っているように、必要になってから行えばよい。
▼議論
- Q. 「永続性のあるシステム」と「ビジネスルール」を分離する必要性がいまいち分からない。
ホットスポットとなる部分を別クラスに分離するのは Agile でなくてもごく基本的な考え。分離していないと、あまり変更のない部分だけを利用したいクラスでも無駄なリビルドやテストが頻繁に発生してしまう。 また、永続化部分とビジネスロジックが分かれていないと、ビジネスロジックのユニットテストを記述することが困難になる。
Chapter 9. Open-Closed Principle (OCP) オープン・クローズドの原則
- クラス図の中で
A → B
という矢印があったら、「B を変更したら A も変更しなければならない」 と考える。 - 変更に対して Closed にするには、依存の矢印があると無理なので「抽象」を使用する。どの部分をどのように抽象化すべきかは、求められているシステムによって異なる。
- 抽象クラスを使用する方法 … Strategy パターン
- 抽象メソッドを使用する方法 … Template Method パターン
- OCP をむやみに適用すべきではない。つまり、抽象をむやみに使用すべきではない。
- 抽象を使いすぎるとコードは複雑になる。
- 抽象化にはある程度の時間がかかる。
- 抽象化は、変化が起こる部分(変化しやすい部分)が明らかになってから行えばよい。 前世紀の考え方では、あらかじめ抽象を仕込むこと=柔軟と考えられていたがこれは間違いである。 なぜなら、
- 抽象化には、将来の仕様変更をある程度予測する洞察力が必要だ。
- そして、その予測はほとんどの場合外れる。
- 抽象化されたクラスは邪魔でしかなくなる。
- 変化が起こってから抽象化を行うのであれば、変化の可能性はプロジェクトの早めに知ることができたほうがよい。そのためには、
- テストファーストで開発をする。
- 短いサイクルで開発し、顧客へのリリース頻繁に行う。
- 最も重要な機能から優先的に開発を進める。
▼議論
- Q. テストファーストでどうして変化の可能性を早く知ることができるのか?
変化の可能性を知るというよりは、あらかじめテストで想定されうる入力を洗い出すことで、早い時点で抽象化するべき部分を見極めることができるということではないか。
Chapter 10. The Liskov Substitution Principle (LSP) リスコフの置換原則
- 基本クラスの使えるところでは必ず派生クラスのオブジェクトが使えなければならないという意味。派生型の本当の定義は、基本型と置き換えられるということ。例えば、
MyFunc(Base *pObj)
という関数には、Base
クラスのサブクラスのオブジェクトを渡しても適切に振舞わなければならない。だからといって、if 文でオブジェクトの型を判断して処理を振り分けるようなコードは記述してはならない。これは OCP (Open-Closed Principle) に違反し、修正に対して「閉じ」なくなってしまう。 - 派生クラスが基本クラスの機能を制限してしている場合は LSP に違反している。なぜなら、基本クラス以下のことしかできないものは基本クラスの代わりにはなり得ないから。あるメソッドをオーバーライドして、実装を空っぽにしてしまうのは典型的な違反例。
- 派生クラスで基本クラスにない例外を投げてしまうと、LSP に違反する。基本クラスを使っているメソッドは、派生クラスが投げる例外をキャッチするようには設計されていないため。
- LSP に準拠させるためには、「振る舞い」の等しさに注目して継承関係を持たせないといけない。 ソフトウェアは「振る舞い」そのものであり、ソフトウェアの世界で「IS-A 関係」を持つということは、つまり、「振る舞い」が等しいということである。例えば、
Rectangle::SetWidth()
とSquare::SetWidth()
の「振る舞い」は異なるので、Rectangle
クラスとSquare
クラスは継承関係 (IS-A) を持たせてはいけない。 - モデルの正当性は立場によって異なり、その使い方によって正しかったり正しくなかったりするので、普遍的に正しいモデルは存在しない。以下のテクニックを使えば、そのクラスが合理的だと仮定している「振る舞い」を明確にできる。
- 契約による設計 (Design by Contract)
Bertrand Meyer 1997 によって提唱。メソッドの事前条件と事後条件を明確にすることでユーザに対して「振る舞い」を明示する。派生クラスの事前条件は、基本クラスで許可されているものは全て許可しなければならない(派生クラスで事前条件を厳しくしてはいけない)。派生クラスの事後条件は、基本クラスで課せられている条件を全て満たさなければならない。これにより、基本クラスのオブジェクトの代わりに派生クラスのオブジェクトを使用できることが保証される。 - ユニットテスト
ユニットテストによって契約内容を記述することもできる。ユーザはユニットテストを見れば、そのクラスについて何が仮定されているか知ることができる。
- 契約による設計 (Design by Contract)
- 2つのクラスに単純な継承関係を持たせることによって LSP に違反してしまう場合は、共通部分だけを親クラスにくくり出すことによって解決できることがある。
▼議論
- Q. 共通部分を親クラスにくくり出すというが、実装の多重継承ができない言語を使用していて、既にあるクラスを継承している場合はどうするのか?
そーゆー場合は、インタフェースだけくくり出すしかないのでは。 - Q. 純粋仮想関数として親クラスにくくり出した場合、ここで言っている「振る舞い」が IS-A 関係にあることが保証されるのか?
インタフェースはそもそも何も「振る舞い」を定義しないので、IS-A 関係は必ず成立する。そのインタフェースを使うメソッドは何らかの「振る舞い」を期待したコーディングをしてはいけない。
Chapter 11. The Dependency-Inversion Principle (DIP) 依存関係逆転の原則
- 抽象は実装の詳細に依存してはならない。実装の詳細が抽象に依存すべき。
- 下位レイヤのモジュールがサブルーチンライブラリという形で再利用できるのは当たり前。そういうことを言っているのではなく、ここでは、上位のモジュールを再利用できるようにするために、下位のモジュールに依存しないように設計しようと言っている。特に C → C++ という学習過程を辿ってきた人には重要な原則。
- ここで「上位のモジュール」といっているのは、アプリケーションの全体的な動きなどの方針を決める部分で、実装の詳細(下位のモジュール)が変更されても影響を受けない本質的な部分。
- 下位のモジュールが提供するインタフェースを利用するのではなく、上位のモジュールが要求しているインタフェースを下位のモジュールが実装するという考え。分かりやすいのは Observer パターンとか。
下記は DIP に違反している例。
下記は DIP に従っている例。
- DIP に違反していれば「手続き型」の設計をしており、DIP に従っていれば「オブジェクト指向」の設計をしている。
- 下位のモジュールの再利用は、サブルーチンライブラリという形で確保されている。我々が本当に確保したいのは上位のモジュールの再利用性である。実装の詳細(下位のモジュール)を変更しても、上位のモジュールに影響がないようにしないといけない。
- 再利用可能なフレームワークの構築には DIP の適切な適用が欠かせない。例えば、あるメニュー項目をクリックしたときに、具体的な処理が行われるとか。メニューのフレームワークが特定の処理に依存しているわけではない。
- あるクラスがほかのクラスにメッセージを送るようなケースでは、この依存関係の逆転は常に適用できる。
▼議論
- Q. 上位モジュールと下位モジュールの依存関係を逆転するために最初から上位モジュールが利用するインタフェースを用意するという実装方法(図11-2)は、常にコードをシンプルに保つという思想と矛盾していないか?DIP はシンプルさうんぬんの前に最低限守るべきということ?
レイヤーの違うモジュールに関してはこの原則を適用すべきということ。
Chapter 12. The Interface Segregation Principle (ISP) インタフェース分離の原則
- すべてのインタフェースを1つのクラスにまとめてしまわず、関連性のあるインタフェースごとにグループ分けして分離する。
- インタフェースが太りすぎると、そのインタフェースを変更したとき、多くのサブクラスを再コンパイルしなければいけない。
- インタフェースを追加するときは、既存のインタフェースを弄るのではなく、新しいインタフェースを追加する方法もある。この方法で追加した新しいインタフェースをクライアントが利用するには、次のように新しいインタフェースを取得することができる。
Chapter 13. Overview of UML for C# Programmers
Chapter 14. Working with Diagrams
Chapter 15. State Diagrams
Chapter 16. Object Diagrams
Chapter 17. Use Cases
Chapter 18. Sequence Diagrams
Chapter 19. Class Diagrams
Chapter 20. Heuristics and Coffee
Section III: The Payroll Case Study
Chapter 21. COMMAND and ACTIVE OBJECT: Versatility and Multitasking
- Command パターンはシンプルだが万能で、様々な目的に利用できる。
- データベースのトランザクションの生成と処理
- デバイスの制御
- マルチスレッドのコア
- GUI の do/undo の管理
- 例えばデバイスの制御に適用すれば、システムの論理的な内部構造とシステムに接続するデバイスの詳細を切り離すことができる。Sensor はイベントを検出したら結び付けられた Command インタフェースの do メソッドを呼び出すだけでよい(初期化時の関連付けは必要だが、実行の際に Sensor が出力先のハードウェアの詳細を意識する必要はない)。
- 命令の一時的な保存場所として Command オブジェクトを使用することもできる。例えば、Transaction インタフェースに validate() メソッドを加えれば、Transaction オブジェクト生成時にその有効性だけをチェックしておき、後からまとめてバッチ処理を行うことができる。
- Command インタフェースに do() メソッドだけでなく、undo() メソッドを用意すれば、Undo 処理をサポートできる。各 Command オブジェクトは do() 実行時に以前の状態を覚えてスタックに積まれる(当然、操作対象となるオブジェクトの参照も覚えておく)。Undo するときは、スタックから pop して、undo() メソッドにより以前の状態に戻される。
- Command パターンを有効活用しているパターンの 1 つに ActiveObject パターンがある。 ActiveObject パターンでは、メソッドの実行は 1 つのスレッドでシーケンシャルに行われるため、スタック領域を節約しなければならないようなメモリの限られたシステムでメリットが大きい。
参考: ActiveObject パターンの論文
Active Object: An Object Behavioral Pattern for Concurrent Programming
下記は P.1 ~ P.4 までのまとめ。
- Active Object パターンとは、複数のスレッドからオブジェクトにアクセスする際の “method execution” と “method invocation” のスレッドを分離するパターン。
- 別名 (Also Known As) は “Concurrent Object and Actor”
- Active Object が解決しようとしているのは、複数スレッドからの共有オブジェクトへのアクセスが引き起こす下記のような課題。
- 共有オブジェクトにおけるメソッド呼び出しが実行されるときに、全体のスレッドがブロックされてしまうのを防ぎたい。
- 複数スレッドからの共有オブジェクトへのアクセスはできるだけシンプルにしたい。例えば、アクセスごとに mutex lock をかけなければならないのは複雑だ。
- アプリケーションは透過的に並列性を利用するように設計されるべきだ。←▼これの意図がよくわからない。
- Active Object パターンを使う場合、メソッドの呼び出しは、自動的に Method Requrest オブジェクトに変換されるので、普通のメソッド呼び出しと同じようになる。
- 全体のフローの概要は以下の通り。
- クライアントのスレッドは Proxy 経由で共有オブジェクトのメソッドを呼び出す。
- Proxy が各メソッドに対応する MethodRequest オブジェクトを生成し、別スレッドの Scheduler に enqueue() する。
- Scheduler 側のスレッドは常にシーケンシャルに MethodRequest を処理していく。Proxy で呼びだされるメソッドの実装は Servant と呼ばれているもので実装される。
- クライアント側では Proxy 経由のメソッド呼び出しの戻り値として Future オブジェクトを取得し、Future オブジェクトから各メソッドの戻り値を取得できる。
Chapter 22. TEMPLATE METHOD and STRATEGY: Inheritance versus Delegation (継承と委譲)
- 1990年代初期は、継承によって差分プログラミングできると信じられていた。1995年には、継承の乱用の欠点が明らかになった。GoF は「クラスの継承よりオブジェクトのコンポジションを使え」というまでになった。
- Template Method パターンは継承を使って問題を解決し、Strategy パターンは委譲を使って問題を解決する。もっと詳しくいうと、Template Method パターンでは汎用的なアルゴリズムを抽象クラスで実装し、具体的な処理の内容を派生クラスで実装する。Strategy パターンでは汎用的なアルゴリズムを具象クラスで実装し、具体的な処理の内容はインタフェースに委譲する。
- Template Method パターンも Strategy パターンも上位レベルのアルゴリズムを再利用するパターン。Strategy パターンは、Template Method パターンより複雑さ、メモリ効率、実行時間の点で若干不利だが、Strategy パターンには下位レベルの実装の詳細を再利用できるという利点がある。
- Template Method パターン
- 欠点: 上位クラス(アルゴリズム部分)が下位クラス(実装の詳細)が継承という強い関係で結ばれてしまう。つまり、下位クラスはその上位クラスとは切り離して使用することができないので、別のアルゴリズムに再利用することができなくなってしまう(DIP に違反)。たとえ下位クラスの実装から上位クラスのメソッドを利用していないとしても、継承関係にある限り切り離せない。
- 欠点: ▼そもそも多重継承の許されない言語を使っている場合は採用しにくいでしょ?
- Strategy パターン
- 利点: 実装の詳細が、アルゴリズムを表現しているクラスの実装に依存しないため、いろんなアルゴリズムで利用できる(DIP に完全に準拠している)。
- 欠点: Template Method パターンに比べ、クラスの数が多く、間接参照が多い(メモリ、実行時間のコストがかかる)。ただし、多くの場合、これはほとんど問題にならない。
結論としては、実装の詳細を再利用できる可能性が少しでもあり、微々たる実行コストが問題にならないなら、常に Strategy パターンを採用すべし。
Chapter 23. Facade and Mediator
- Facade パターンも Mediator パターンもある種の方針をオブジェクトに強制する。
- Facade パターンは上から方針を制約する。その方針は視覚的に捉えられる。
- Mediator パターンは下から方針を制約する。その方針は視覚的には捉えられない。
- Facade パターンを採用する場合は、全員が Facade を利用することに同意しなければならないことを暗に意味する。上から使い方の制約を課すことができるのであれば、Facade パターンを利用できる。Facade のクライアントは、Facade の下に隠されたライブラリに直接アクセスしないように約束する必要がある。
- Mediator パターンは、Mediator オブジェクトを作成した時点でオブジェクト間の方針が課される。Mediator パターンの場合、オブジェクト同士にどのような制約が課されるか(どのように連携されるか)は、Mediator の実装によって隠蔽される。
Chapter 24. Singleton and Monostate
Singleton はインスタンスが複数生成されないような「構造」である。 Monostate はすべてのオブジェクトが同じ「振る舞い」をする。
- Singleton パターンの特徴
- Singleton でない既存のクラスを派生して Singleton にすることができる。
- Singleton であるクラスを継承すると、そのままでは Singleton ではなくなってしまう。
- Monostate パターンの特徴
- Monostate を継承したクラスは Monostate になる(派生による多態性を利用できる)。
- Monostate でないクラスを継承して Monostate にすることはできない。
- Monostate のメソッドは static でないので多態性を持たせられる。
- Singleton に比べて Monostate は振る舞い的に気持ち悪い。
あるクラスを Monostate にするには、メンバ変数をすべて static にすればよい(メンバメソッドは static にしない)。
Monostate パターンによる実装例 (MyMonostate.cs)
▼議論
- Q. Singleton は削除するいい方法がないといっているが、参照カウンタで生存期間を管理すればよいのでは?
参照カウンタが 0 になった時点でオブジェクトが消えてしまうのであれば、次に生成されるオブジェクトは別のオブジェクトになってメンバ変数も初期化されるので、それはもはや Singleton とは言えない。つまり、メモリ使用量が気になるようなオブジェクトは、Singleton にしてはいけない。 - Q. Singleton のメンバメソッドも static ではないので Monostate と同様に多態性を持たせられるのでは?
そもそも Singleton を継承するのは難しい。なぜなら、親クラスの GetInstance() は static なので継承ができないし、親クラスと子クラス両方に GetInstance() 系のメソッドを用意すると、クライアントは親クラスと子クラスのオブジェクトを別々に生成できることになり、結果として親クラスの性質を持つオブジェクトが 2 つ存在することになる。要するに分かりにくい。
Chapter 25. Null Object
Null Object パターンは、オブジェクトを返すメソッドが null や 0 を返さないことを保証し、メソッド呼び出し側での null チェックの必要をなくす。
Null Object が唯一のインスタンスであることが保証されているのであれば、従来の null チェックのような分岐処理を記述することもできる。
▼議論
- Q. null チェックしなくてよくなったとしても、バグが表面化しないだけで、潜在的なバグを Null Object によって包み込んでしまうだけでは?
そうかもしれない。結局のところ、Null Object が提供する振る舞い(一般的に何も実行しないという振る舞い)が正しい振る舞いでないのであれば、分岐処理は必要である。 - Q. では、Null Object パターンの主な価値は何?だって、Null Object で何もしないでスルーしてもよいケースなんてそんなないでしょ?
プレースホルダ的な機能を用意しておくケースはいろいろある。あるピアへの接続が完了するまではイベント発生しても何もしないとか、イベントに対して何らかの振る舞いを後付けで割り当てるケースなど。 「null チェック忘れによってクラッシュすることを防ぐ」ために Null Object を導入するというのは、本質的な使い方ではない。 元のコードで、null 時に特別なシーケンスを実行していたのであれば、Null Object を導入したところでそのシーケンスが必要なくなるわけではない。
Chapter 26. The Payroll Case Study: Iteration 1 (給与システムのケーススタディ:最初のイテレーション)
- データベースは実装の詳細にすぎないので、データベースについて考えるのはできるだけ後回しにすべき。
- Jacobson が提唱したユースケースは、XP のユーザーストーリーの概念にそっくり。ユースケースはユーザーストーリーをもう少し詳細にしたものだと考えればよい。
- ユースケースを使った落とし込みは、現在のイテレーションで実装する必要のあるユーザーストーリーについてだけ行うべき。
- 各イテレーションの最初で、チーム全員がホワイトボードの前で、選択されたユースケースの簡単な設計議論を行うのは「考えるプロセスを始める」ためであり、設計の詳細を詰めるためではない。
Chapter 27. The Payroll Case Study: Implementation (給与システムのケーススタディ:実装)
- コードの検証を怠って、UML にのめり込んでしまうのは危険である。コードは UML が教えてくれなかった設計の問題点を教えてくれる。UML ダイアグラムは役立つツールだが、コードからのフィードバックなしに依存しきってしまうのは危険だ。
- フィードバックなしに設計を進めると必ずエラーが発生する。エラーを防ぐには、テストケースやコードを走らせることでフィードバックを得ること。
- データベースを設計や実装の主要部分とみなすべきではない。データベースに対する考察は最後まで残し、詳細設計として扱うべきである。そうすることで、データの保存方法や、テストのメカニズムをどう作るかに関して色々な選択肢を残しておける。
Section IV: Packaging the Payroll System
Chapter 28. Principles of Package and Component Design (パッケージ設計の原則)
大きなアプリケーションを体系化するにはクラスだけでなく、もっと大きな単位のパッケージが必要になる。UML ではクラスをとりまとめるコンテナとしてパッケージを扱える。
パッケージ内部の凝集度 (cohesion) に関する原則
- (1) 再利用・リリース等価の原則 (REP: Reuse-Release Equivalency Principle)
- 再利用の単位とリリースの単位は等価になる。
- 再利用されるものはリリースされて、そのパッケージにリリース番号を与えてトラッキング可能でなければならない。
- パッケージ内のクラスはすべて再利用可能なものか、すべて再利用できないものかのどちらかにすべき。
- (2) 全再利用の原則 (CRP: Common Reuse Principle)
- パッケージに含まれるクラスはすべて一緒に再利用される。
- 一緒に使われる傾向のあるクラスは同じパッケージに属す。コンテナとイテレータなど。
- 逆に、一緒に使われる傾向のないクラスは同じパッケージに入れるべきではない。
- ある新しいクラスを使おうとしてパッケージを新しいものに置き換えると、そのパッケージ内のクラスを利用しているコードをすべて再評価しなければならない。
- (3) 閉鎖性共通の原則 (CCP: Common Closure Principle)
- パッケージに含まれるクラスは、みな同じ種類の変更に対して閉じているべきである。
- 単一責任の原則 (SRP) のパッケージ版。
- こうすれば、仕様変更があってもその影響を受けるパッケージを最小限にできる。
パッケージ同士の結合度に関する原則
- (4) 非循環依存関係の原則 (ADP: Acyclic Dependencies Principle)
- パッケージ依存グラフに循環を持ち込んではいけない。
- パッケージの依存グラフの矢印を辿っていったときに、最初のパッケージに戻ってきてはいけない。
- 矢印を下向きに書くと、上向きの矢印が出てきたときに依存関係が循環してしまっていることがすぐに分かる。
- 非循環性の有効グラフ (DAG: Directed Acyclic Graph) になっていれば、新しいパッケージのリリースによる影響範囲を絞り込みやすい。矢印をただ逆に辿ればよいだけ。
- 依存グラフが循環すると、ほとんどのパッケージを同時にビルド、リリースしないといけなくなる。
- パッケージ依存構造は、アプリケーションの作成の過程で変化していくものなので、常に依存構造に循環が起きていないか見張っておかなければならない。
- パッケージ依存ダイアグラムはビルド方法を示すマップであり、アプリケーションの機能の概要を示すことはほとんどない。
- クラスを作る前にパッケージダイアグラムを作ろうとするのは間違い。確実に依存関係が循環することになる。
- (5) 安定依存の原則 (SDP: Stable Dependencies Principle)
- 安定する方向に依存せよ。パッケージの不安程度は、それが依存するパッケージの不安程度より大きくあるべきだ。
- 不安定なパッケージを常に上に描くようにするとよい。こうしておけば、上向きの矢印が見つかったらすぐに SDP に違反していることが分かる。
- (6) 安定度・抽象度等価の原則 (SAP: Stable Abstractions Principle)
- パッケージの抽象度と安定度は同程度でなければならない。
- パッケージの安定度が高いということは、それ相応の抽象クラスで構成されているべきということ。
- このバランスが崩れると、無意味なパッケージ、苦痛を伴うパッケージが生まれることになる。
Chapter 29. Factory
- いくらインタフェースを使用して具象クラスのオブジェクトにアクセスしても、具象クラスを new する部分がコードが含まれていれば、そのクラスに依存してしまう。Factory パターンは、抽象インタフェースのみで具象クラスを作成することを可能にする。
- Factory パターンのメリットのひとつとして、Factory の実装を自由に入れ替えられることがあげられる。Factory を抽象化しておけば、オブジェクトのインスタンス化方法を簡単に変更できる。
- Factory パターンを、テスト用のモックオブジェクトを作成するために使うこともできる。 Factory の抽象インタフェースをグローバルに保持するようにしておけば、テストコードの中でこのグローバル変数に、テスト用の Factory の参照をセットできるようになる。つまり、本物のコードに一切手をつけずにテスト用の振る舞いをさせることができる。
- 依存関係逆転の法則 (DIP) に完全に準拠しようとすると、多くの場所で Factory パターンを適用しなければならなくなる。ただし、これはやりすぎで、例えば、下記のような場合に Factory パターンを適用するとよい。
- Proxy パターンを使う場合。Factory を使って永続性のあるオブジェクトを作るために必要になる。
- ユニットテストのために、オブジェクトを生成するオブジェクトをだまさないといけない場合。
Chapter 30. The Payroll Case Study: Package Analysis (給与システムのケーススタディ:ふたたび)
- ダメなパッケージ分割 … 詳細部分が下の方に配置されている。
- よいパッケージ分割 … 抽象化された部分がより下に配置されている。
- 再利用の単位はパッケージ単位になる。なぜならば、パッケージ内のクラスには凝集性 (cohesive) があるからで、互いに強く依存しているから。これはパッケージ分割のひとつの指標になる。
- パッケージ分割はシンプルなところから始めて、徐々に複雑にしていくのがよい。
- 再利用されないものや、変更の少ないものまでパッケージ分割するのはやりすぎ。
- パッケージ間の依存度は、Factory パターンによって下げることができる。パッケージごとに、Factory を 1 つ用意し、そのパッケージ内に含まれる public なオブジェクトをその Factory を通してインスタンス化するようにすればよい。
- 機能面を軸にパッケージ分割するよりも、トランザクションを軸にパッケージ分割する方がずっと意味がある。
Chapter 31. Composite
- Command パターンに Composite パターンを付加すれば、ある Command を使用しているクラスに変更をいれずに複数の Command を扱えるようになる。 → OCP をうまく適用した例。
- 複数のオブジェクトを同様に扱うためのリストや配列などをメンバ変数として持っているクラスは、Composite パターンを導入することで、「1対多」の関係から「1対1」の関係に落とし込める。こうすることで、コードはシンプルになり、コーディングも保守も楽になる。各クライアントにリストを繰り返し処理するためのコードを埋め込むのではなく、Composite の中に一度だけ繰り返し処理を記述するだけで済むようになる。
Chapter 32. Observer: Evolving into a Pattern (デザインパターンへの回帰)
- テストファーストを実践することで、必然的に設計の分離性が促進される。テストの方法を考えるだけで、インタフェースを設計に追加することになる。
- Observer パターンを使用すると直接的な依存関係をなくすことができるので、多くの場所で使用したくなるが、Observer パターンを乱用すると、理解しにくく、流れを追いにくくなってしまう。
Chapter 33. Abstract Server, Adapter, and Bridge
- 依存関係逆転の法則 (DIP) は抽象クラスに依存することが望ましいと主張している。
- 通常、継承階層構造は同じパッケージにまとめるべきものではない。クライアントは、それがコントロールするインタフェースとともにパッケージ化されるべき。
- システムの一部をその場しのぎで解決しても、あとからそれが原因で厄介な依存関係が生じ、まったく関係ない別のシステムで問題を引き起こす。
- Adapter パターンを使用するときに Factory パターンも一緒に使用すれば、Adapter の存在を隠蔽することができ、Adapter への依存関係をなくすことができる。
- Bridge パターンを利用することで、複数の階層構造を 1 つに併合せず、分離したまま結合することができる。
- Adapter パターンを使った方法は、シンプルで依存関係も正しい方向になる。一方、Bridge パターンは 2 つの階層構造を完全に分離することができるが、かなり複雑になるので、その必要がある局面でない限りおすすめできない。
- 責任の所在をたやすく「不十分な設計」のせいにするべきではない。この世に十分な設計などありえない。存在するのは、コストと利益のバランスがとれるように設計された構造だけ。設計の変化をうまく管理するには、システムを極力シンプルかつ柔軟に保つこと。
Chapter 34. PROXY and GATEWAY: Managing Third-Party APIs (第1版タイトル: Proxy, Stairway to Heaven)
- Proxy パターンを導入すると、ビジネスルールを他のあらゆる実装(データベース、COM、CORBA、EJB)から切り離すことができる。
- Proxy パターンの取り扱いはそれほど簡単ではなく、ほとんどのアプリケーションでは導入すべきではない。ただし、アプリケーションと API を徹底的に分離することが望ましいケースもある。その代表的なケースは、システムが大きく、データベーススキーマや API が頻繁に変更されるような場合、あるいは複数の MW、データベースエンジン上に構築されているシステム。
- パフォーマンスの改善方法の検討は、実際にパフォーマンスが問題になっていることを検証して、それが実証された場合に限って行うべき。
- Stairway to Heaven パターンを使うと、Proxy パターンと同様に依存関係の逆転を実現できる。ビジネスルールの継承構造を、他の実装(永続メカニズムなど)の継承構造と分離できる。ただし、C++ のような多重継承をサポートした言語でしか採用できない。
- Proxy パターンや Stairway to Heaven パターンは本当に必要になるまでは導入すべきではない(特に Proxy パターン)。それまでは Facade パターンなどを使えばよい。
▼議論
- Q. Stairway to Heaven ってインタフェース導入すれば多重継承しなくてもよいんじゃない?
その通り。そうするとまさに Proxy っぽくなる。だから Stairway to Heaven は Proxy の退化版(インタフェース使わない版)とも言えるかもしれない。第3版から削除されたのはそれが理由かも。 - Q. Facade のダイアグラムの依存の方向逆じゃない?
データクラスが Facade のクライアントということを示していると思われる。でも、この Facade の使い方は気持ち悪い。この図だけではデータの永続化のタイミングはどうやって指定しているのか分からない。
Chapter 35. Visitor
- Visitor パターンは大きなデータ構造を渡り歩き、そのレポートを作成するような場合によく使われる。1 つのデータ構造をいろいろな形で利用しなければならないアプリケーションで使用される。
- Visitor パターンがうまくいくのは、visit される側のクラスをあまり変更することがない場合。visit される側に派生クラスが追加されるたびに Visitor 側も再コンパイルしなければならない。
- visit される側のクラスの階層構造が頻繁に変わるような場合は、Acyclic Visitor パターン(非循環 Visitor パターン)を採用するとよい。ただし、Acyclic Visitor パターンは若干複雑で、キャストを必要とする。
- Visitor パターンは魅惑的だが、通常はもっとシンプルな方法で解決できることが多い。
- Decorator パターン、Extension Object パターンも、既存のクラス構造に影響を与えずに機能を追加するパターン。
- Extension Object は他のパターンより複雑だが、パワフルで柔軟性がある。
▼議論
- Q. Extension オブジェクトは、getExtension() した時点で作成した方が効率てきではないか?Hash を使った方がシンプルで分かりやすいというのは確かだが、CSV や XML の両方の Extension オブジェクトをいつも使うわけではないので、Extension オブジェクト生成のコストが無駄になるのでは?
その通り。
Chapter 36. State
- 有限状態マシン (FSM: Finite State Machine)、状態遷移図 (STD: State Transition Diagram) は、ほとんどどんな場合でも使用できる万能な設計ツール。もっと活用すべき。
- 状態遷移図における遷移は、状態遷移テーブル (STT: State Transition Table) でも表現できる。
- 状態遷移図、状態遷移テーブルを使うことで、設計者が見落としてしまいがちなマイナーな状態を発見できる。
- Java の弱点のひとつは、C++ の friend と同等の機能が存在しないこと。この機能がないので、private な state 変数をチェックするテストコードが書けない。
- 有限状態マシンを実装する 3 つの方法
- switch 文を使う方法
- ○ 単純な有限状態マシンならエレガントに効率的に実装できる
- × 状態が多くなると保守できなくなる
- × 状態マシンの論理部分とアクション部分を分離できない
- 遷移テーブルを使う方法
- ○ 状態遷移テーブルの構成を理解しやすい
- ○ プログラム実行時にテーブルを変更できる
- ○ 複数の遷移テーブルを作れる
- × 遷移テーブルをサーチするため効率が悪い
- × 遷移テーブルをサポートするためにサポート関数が多数必要
- State パターンを使う方法
- ○ switch を使う方法の効率性、遷移テーブルを使う方法の柔軟性の両方を備えている。
- × State クラスのサブクラスを作成する作業が飽き飽きする。
- × 論理が記述している部分が複数の State 派生クラスに分離してしまい、保守が困難になる。
- switch 文を使う方法
- State パターンと Strategy パターンの違いは、State パターンでは State クラスが Context クラスへの参照を保持していること。
- State クラスのメソッドのデフォルト実装として、例外を投げるコードを記述しておくと、ある State で発生してはいけないイベントが発生したときにすぐ分かるようになる。
▼議論
- Q. テーブルをサーチするのは switch 文より効率が悪いって本当?
currentState * event の組み合わせでリニアサーチするから switch より平均的に遅くなる。さらに switch 分岐はインデクシングにより最適化される可能性が高い。 - Q. n回イベントが発生したときに状態を遷移させるようなステートチャートは記述可能か?
ガード条件を付加すれば可能。ただ、単純な表からコードを自動生成するようなことができなくなりそう。