ちょうぜつソフトウェア設計入門
一言でまとめ
良いアーキテクチャとは何かを解説してくれる本
概要
絵柄とタイトルからやさしい入門書のように思えるが、中身は重厚。
実際の開発経験を積んだ上で読まないと、根本的な意図まで理解するのは難しい。
しかし、内容を理解することが出来れば確かにステップアップできる。そんな本。
本書で解説するのは、具体的なアルゴリズムというより「アーキテクチャ」について。
何を意識すれば、柔軟で安全な、かつ、持続可能なコードに出来るのか。
これを教えてくれる。
キーワードは、
「クリーンアーキテクチャ」と、それを実現するための「パッケージ原則」「オブジェクト指向原則 SOLID」。
より良い設計に気づかせてくれる「テスト駆動開発」「ドメイン駆動開発」。
そして、そもそもなぜより良い設計が必要なのかという根本的な疑問に答えてくれる「アジャイル開発宣言」。
これらを順序立てて、体系的にまとめ上げてくれる。
一部要約
アーキテクチャとクリーンアーキテクチャ
アーキテクチャはプログラムの動作に直接貢献するものではない。
良いアーキテクチャが作れたからと言って、ソフトウェアの性能が向上したり、機能が増えたり、誤動作が無くなったり、
ソフトウェア利用者に知覚できる効果を生むわけではない。
アーキテクチャは開発者側のパフォーマンスに関わるものである。
良いアーキテクチャは、開発をスムーズなものにしてくれる。
不具合修正や仕様変更などの対応を迅速に、且つ、安全に実施できるようになる。
開発者は変化を受け入れやすくなり、その結果、顧客の要望にも応えやすくなる。
逆に悪いアーキテクチャは、開発スピードを低下させる。
どこを修正すべきか分かりにくく手が出せない。小さな変更が様々に影響し安全性を保障できない。
こうなってしまうと開発者は変化に対して否定的になってしまう。
その結果、顧客の要望に応えにくくなる。
アーキテクチャにこだわるべき理由は、それが継続的な提供を支えてくれるからである。
良いアーキテクチャは少なくとも、以下の3つの条件を満たす
1つの関心が1つの箇所に閉じていること
利用する/されるの関係箇所を可能な限り減らすこと
自身より変更頻度の高いモジュールに依存しないこと
特に3の「自身より変更頻度の高いモジュールに依存しないこと」に着目して、具体的な設計方針を示してくれるのが「クリーンアーキテクチャ」
層構造の形をとり、本質的で変更の余地が少ないものを下の層に、それを利用する側・変更の余地が大きいものを上の層に置く。
そして、ある層のモジュールは自身より下の層(より安定している層)にあるモジュールのみに依存する。
依存の向きを安定側に向けることで、あるモジュールの変更に対する他モジュールへの影響を小さく抑えることが出来る。
クリーンアーキテクチャが提唱する「依存の向きのコントロール」
これを実現するためにオブジェクト指向開発のスキルが必要となる
オブジェクト指向原則 SOLID
オブジェクト指向で開発する際に守るべき5つの原則がある
単一責務原則 Single Responsibility Principle SRP
開放閉鎖原則 Open Close Principle OCP
リスコフの置換原則 Liskov Substitution Principle LSP
インターフェース分離原則 Interface Separation Principle ISP
依存性逆転原則 Dependency Inversion Principle DIP
これらは頭文字をとってSOLIDとして知られている
オブジェクト指向の特徴を理解し、これらの原則を守れば、クリーンアーキテクチャが提唱する依存のコントロールが実現可能となる。
単一責任原則 Single Responsibility Principle SRP
1つのクラスは、1つの責務のみ持つ。
シンプルなルールだが「1つの責務」の見分け方が意外に難しい。
コツとしては、「交換単位」を考えること。
もしクラスの中で、他の部分は変えずに、ある部分は交換可能であるとき、
その交換可能な部分は他のクラスとして分けられる可能性が高い。
開放閉鎖原則 Open Close Principle OCP
拡張は容易であるが、そのことによる変更量は少なくさせる。
(拡張に対してOpenだが、それに伴う変更に対してはCloseな作りにするべき)
モジュールは必ず利用する側と、利用される側がある。
利用される側に何らかの変更を加える時(拡張する時)、利用する側にまでその影響がおよぶのは避けたい。
これを実現するには、抽象クラスと具象クラスの切り分けが重要。
利用側から見たら同じ抽象クラスであり、使い方は何も変わらない。
しかし、その抽象クラスの実装である具象クラスは様々な振る舞いを取り得る。
つまり、具象クラスを様々に切り替えることが出来るが(拡張にOpenで)
そのことによる利用側の変更はない(変更にClose)
というように作るべきというルール
リスコフの置換原則 Liskov Substitution Principle LSP
派生クラスは基底クラスの"使われ方"を過不足なくカバーする。
モジュールを利用する側は、同じ基底クラスであれば、同じように指示を出したい。
つまり、利用される側がどのような派生クラスであるのかは気にしない。
もし、派生クラスによって入出力の仕様が異なる時、利用する側は意図しない不具合を踏む可能性がある。
そうなると、利用する側が、利用される側の実体を気にする様になってしまい、多態性の利点が失われる。
これを防ぐために、派生クラスは基底クラスが定める入出力の制限、振る舞いを過不足なくカバーさせる。
派生クラスが勝手に入力可能値を広げてはいけないし狭めてもいけない。
出力のフォーマットも変えてはいけない。
利用する側が、置き換わっていることに気づかないように作るべきというルール
インターフェース分離原則 Interface Separation Principle ISP
1つのインターフェースは必要なだけの機能を備える。
単一責任原則と似ているが、言及しているのがクラスかインターフェースかで異なる。
例えばDBの操作はユースケースに応じて、入力のみ、出力のみ、入出力、の3パターンの要求があり得る。
この時入力と出力をそれぞれ別のインターフェース(機能)として分離させておくことで、クライアントコードの要求に応じて、必要なだけの責務を備えたクラスを実現できる。
"使われ方"を意識してインターフェースを細かく区切ることで、クラスに無駄な機能を持たせずに済む。
クラスが無駄な機能を持たなければ、問題分析や別クラスでの代替が容易になる
依存性逆転原則 Dependency Inversion Principle DIP
呼び出す側と呼び出される側の制御の向きに対して、呼び出される側の依存の向きを逆転させる
オブジェクト指向を象徴する原則で、SOLIDのその他の原則と密接にかかわっている
例えば、USBPortがUSBKeyboardに指示を出すようなシチュエーションで、
制御の向き(Port→Keyboard)のまま、依存関係を作ったとする。
class USBKeyboard
{
public function connect(InternalBus $bus): void{}
}
class USBPort
{
private InternalBus $internalBus;
public function plugKeyboard(USBKeyboard $keyboard): void
{
$keyboard->connect($this->internalBus);
}
}
これは、USBPortはKeyboardが存在しないと成立しない作りになっている。
つまり使う側が、使われる側の実体に依存してしまう、構造化プログラミングの欠点がそのまま反映されている。
USBPortに接続可能なデバイスが増えるとなったときに、都度USBPortの側に手を加えることになる。
そこで、使う側(USBPort)から具象クラスへの依存を取り除き、より安定的な抽象クラスへの依存に置き換える
interface USBDeviceInterface
{
public function connect(InternalBus $bus): void{}
}
class USBPort
{
private InternalBus $internalBus;
public function plug(USBDeviceInterface $device): void
{
$device->connect($this->internalBus);
}
}
抽象的なUSBDeviceInterfaceを設けて、USBPortはそれに依存させる。
USBDeviceInterfaceは抽象的で安定的なInterfaceであるため、変更の余地が少ない。
よって、USBPortの実装はこれで完了する
実際に接続するデバイスはUSBDeviceInterfaceに依存させる
class USBKeyboard implements USBDeviceInterface
{
public function connect(InternalBus $bus): void{}
}
USBDeviceInterfaceを満たす具象クラスであればどの様なものでもUSBPortに接続可能となり、
また、USBPortはその具象クラスの実体を気にせず使うことが出来る。(開放閉鎖原則)
構造化プログラミングは、使う側が、使われる側の仕様をカバーする。という構造であるのに対して、
オブジェクト指向プログラミングは、使う側が、使い方をInterfaceとして定義し、使われる側がそのInterfaceを実装する。という構造で作る。
こうすることで、使われる側の変更に対して使う側は影響を少なくできる。
感想
読むのに結構時間かかったし、内容をまとめるのにも時間がかかった。
その分、大きな学びも得られた良書。
実際の開発を通しながら、また読み返したい本。
コード例やUMLも多く掲載してくれていて理解の助けになった。
クリーンアーキテクチャ、TDD、DDDはいずれも原典があると思うが、その導入や事前準備として読んでも良いと思う。
お勧め。