ちょうぜつソフトウェア設計入門メモ
まとめ
単一責任の原則は絶対に守る
1クラス1責務。
ある機能に関する汎用的なコードを集めたようなクラスも、メソッドの各役割ごとにまとめた単位のクラスに分割するべき。
メソッドも処理の単位で単一責任になるように細かく分ける。
パッケージの責務はひとつ、つまりそれぞれのパッケージは、何かを完全にうまくやって、それ以外の仕事をしないようになっているのが理想です。
分けすぎると部品が多くなって理解しにくくなるのではないかと感じる方もいるかもしれません。でも実際は逆で、開発が進むと小さく分けてあるほうが問題を理解しやすくなってきます。
最初はちょっと分けすぎぐらいで十分です。後で分けてあるものを混ぜるのは簡単ですが、混ざっているものを分け直すのは非常に困難です。
交換の単位として適切かを意識して設計する
データベースドライバのクラスの場合は、データベースの読み込みと書き込みを別のクラスに分けてしまうと使いづらいし、もし呼び出し側で、読み込みと書き込みに異なるデータベースドライバのクラスを使ってしまう間違いが起きる可能性もある。なので分けすぎも問題がある。
交換の単位として適切かを意識して設計するべき。
このような設計は機械的に決まるものではなく、どのようにまとめると後で再利用する人が困らないかと考えて、恣意的にデザインするもの。
実装より先に使用を想像し、複数の使うときの関心を結合させてしまうのを避けるよう注意すれば、最初からこのキーボードとマウスの例の結果が出せるようになります。いえ、むしろ、最初の困った設計を考えるほうが難しくなるぐらいです
凝集度
関係あるものは一か所にまとめる。分割したモジュール同士の関係性を狭くする。
そうする理由は、あるコードを変更したときに、影響範囲を特定の範囲内のみにしたいため。
凝集度が低いと、一か所変更したらどこに影響が出るか分からないのが問題。そうなると、作業者は変更が怖くなって、新しいコードを継ぎ足す形で実装し始め、雪だるま式に複雑で保守性の低いコードになっていく。
疎結合
無駄な癒着が無い状態のこと。
これも、一か所を変更したときに別の個所に影響が及ばないようにするための考え方。
疎結合なものは凝集度を上げやすい(どこにでも配置できるため、一つのディレクトリにに集めることもできる)
安定度
安定度が高い = コードが変更される可能性が少ない
安定度が低いコードに依存すると、コードが頻繁に変更され、そのたびに依存先に影響がないかを考えるコストが発生してしまう。
メソッドAをいろんな処理で使うと、それぞれの処理に仕様変更があった場合にメソッドAがそれに影響されて変更されてしまう。
ユースケースが異なるなら別のメソッドを新たに作るべき。
どうすればクリーンになるのか
1.ひとつの関心がひとつの箇所に閉じている
2.利用する/されるの関係箇所を可能なかぎり減らす
3.できるだけ変更頻度の高い事情に依存しない
クリーンアーキテクチャの円の図は、外側から、インフラストラクチャ、コントローラー、ユースケース、ドメインモデル。
外側から内側に向かって依存している。安定度の高いものほど外側にある。
※クリーンアーキテクチャでは、コントローラーにあたる部分はインターフェースアダプターと呼ばれる
ここで言うインフラストラクチャとは、データベースの接続や、メールの送信など、外部とのやり取りをする独立したモジュール。実際にはインフラストラクチャは業務ロジックにあってほしい。
この層の分け方がクリーンアーキテクチャではない。クリーンアーキテクチャとは、決まった形がある訳ではなく、あくまで層の分け方のアイデアであり、コンセプト。
どこになにを配置するかはそれぞれのソフトウェアごとに異なってくる。
あくまでガイドラインなので、例えばクリーンアーキテクチャのコンセプトが役立ちそうな特定の箇所に、クリーンアーキテクチャを参考にした構造を作ってもいい。
閉鎖性共通の原則
同じ理由、同じタイミングで変更されることが多いクラスは1つのコンポーネントにまとめておくべきという原則。
単一責任の原則をコンポーネント向けに言い換えたもの。
「コンポーネント」とは、「再利用可能な部品」のこと。
モジュール単位、クラス単位で一箇所にまとめて凝集度を上げる。
共通再利用の原則
パッケージ内のクラスは、全て一緒に利用されるものだけを入れるという原則。
- コンポーネントを再利用する際には、そのコンポーネントに含まれるすべてのクラスやモジュールが共に必要となるように設計する。
- もし、コンポーネント内の一部のクラスやモジュールだけが必要となる場合は、それらを別のコンポーネントに分離する。
なぜ共通再利用の原則が重要なのか?
-
再利用性の向上
コンポーネントの凝集性を高めることで、コンポーネント全体の再利用性を向上させることができます。 -
依存関係の管理
コンポーネント間の不要な依存関係を減らすことで、システム全体の保守性を向上させることができます。 -
設計の簡潔化
コンポーネントの役割を明確にすることで、設計を簡潔にし、理解しやすくすることができます。
これらの原則は要するに、適切な単位でパッケージにまとめるべきという事。1パッケージに1責務にすれば、凝集度が高くなり疎結合になる。
適切な単位がどの程度かは利用しやすさを考慮して決める。
循環依存とは
AにBを使うコードが含まれるとき、BがAを使うと相互依存になる。相互依存はもっとも小さくてわかりやすい循環依存。
AとBは分離できなく、常にセットで使うことになる。
AとBがそれぞれ適切に分けられていても、実質はAとBの大きなひとかたまりになっている。
実際に発生してしまう循環依存は、二者の相互依存のようなわかりやすいケースでない場合が多くあります。Aに使われるBがCを使ったところ、しばらくして実はCがAを使っていたという事実が判明し、誰も気づかないうちに、いつの間にか循環が出来上がっている場合です。
強い関係はパッケージで閉じる
循環依存で問題なのはあくまでパッケージ間で起きるもの。
どうしても循環参照が起きてしまう場合は、両者を1パッケージに閉じ込めればいい。
パッケージとは、こうした密な依存を閉じ込めるもの。
抽象度
抽象度の高いパッケージは安定度が高い。
抽象度が高いパッケージの例
- 抽象クラスやインターフェースなど、実装詳細を自身から排除したもの
- 時刻や配列などの汎用的な概念と操作
- フレームワークの機能や、業界標準となっているようなライブラリ
こういう抽象度の高いものに依存させるべき。
抽象度の高い順にクラスA、クラスB、クラスCがある場合、
クラスAにクラスBを依存させ、クラスBにクラスCを依存させる。このような作業を繰り返すときれいなアーキテクチャになる。
オブジェクト指向
3つの特徴
- カプセル化
関連性の高い変数群と処理群を一つの概念にまとめて名前を付けたもの。
つまり、例えば日付に関する処理をまとめたDateService
みたいなサービスクラスをつくるのもカプセル化?
詳細をほじくり出さない/出させないことを基本姿勢にするのが、よいオブジェクト指向プログラミングの基礎の基礎にあります。
これをデメテルの法則という。
詳細をいじるのはカプセルの中のメソッドの役割であり、呼び出す側はそのメソッドを呼ぶだけにするべき。
継承/汎化
優れた基底を確保するのが継承。
優れた基底を作っておくことで、その基底は変更されないため安定度を上げることができる。
安定パッケージ=抽象側に優れた基底を確保する話です。継承によってオーバーライドすれば詳細はどうとでもなるから、安心して抽象を抽象のまま使えるというのが、継承という用語を使って言いたいことの本質と考えてください。
仮想的に型があるものとしてプログラムせざるを得ません。通信プロトコルのような厳格なルール付けが必要です。その厳しさに何の支援もないのは辛いので、明示的にインターフェースや抽象クラスの名前を使って抽象の持つきまりを表明するのが、オブジェクト指向をサポートするプログラミング言語の型チェックの意義です。
差分プログラミングは、そこに付随するちょっと便利なおまけぐらいに考えるのがよいでしょう。
重要なのは抽象化で、抽象に対する具象が1つだけでも意味がある。
SOLID原則
単一責任原則
クラスと責務はぴったり1:1にすべきという原則。
ニュース記事管理の場合は、「入稿」と「購読」が、それぞれひとつの責務になります。
別のクラスやより新しいバージョンに交換したいと思うかを想像してみてください。実際に起こりそうな交換欲求への想像が、責務=クラスのカバー範囲を見極めるヒントになります。
データベースドライバのクラスの場合は、データベースの読み込みと書き込みを別のクラスに分けてしまうと使いづらいし、もし呼び出し側で、読み込みと書き込みに異なるデータベースドライバのクラスを使ってしまう間違いが起きる可能性もある。なので分けすぎも問題がある。
交換の単位として適切かを意識して設計するべき。
このような設計は機械的に決まるものではなくどのようにまとめると、あとで再利用する人が困らないかと考えて、恣意的にデザインするもの。
依存性逆転の原則
プログラムの重要な部分が、重要でない部分に依存しないよう設計すべきであるということ。
オブジェクト指向プログラミングにおいては、安定抽象に向く依存の向きが、呼び出し構造の上位下位の関係に縛られない、独立したものであると言っているのが、この依存関係逆転原則(DIP)の意味です。
極端に言えば、クリーンアーキテクチャとは、依存関係逆転原則をアーキテクチャに適用するという、応用方法の一例にすぎない、とも言えます。
例えば、USBポートクラスがUSBキーボードクラスを呼び出す構造の場合、USBポートがUSBキーボードに依存するという、現実で考えるとおかしな構造になってしまう。
そこで、新たにUSBデバイスインターフェースを作って、USBポートクラスからそれを呼び出すようにする。そうすることで、USBデバイスの種類を問わない設計になる。
依存性注入
クラスAを直接newするより、コンストラクタでクラスAを受け取る実装の方が良い。クラスAをnewするコードは、正しくクラスAに引数を渡して構築する責務が発生するため、クラスAへの依存になる。それを外部から注入することで責任を分離する。
こうすることでクラスAを別のクラスに置き換えることができるようになる。
DIを徹底することで、各クラスの実装は、外部のクラスの使用方法、つまりインターフェースにだけ依存する独立性の高い形になります。そうしたクラスをこのシンプルな考え方で数珠つなぎにしてやることで、どれだけ複雑な構造物を作っても、複雑さが組み合わせの掛け算で効いてくる辛さを忘れて、個々の部分の閉じたシンプルな関係だけを考えれば済むようになります。
NestJSはDIコンテナを用意してくれている。app.module.tsがDI(IoC)コンテナ。これで@Injectableが付いた各クラスをシングルトンで1つだけ生成してくれる。これのおかげでファクトリーパターンを使ったインスタンス生成用のクラスを書かなくていい。最近のフレームワークは大体このIoCコンテナの仕組みを持っている。