はじめに
クリーンアーキテクチャの勉強中にSOLIDの原則に触れる機会があった。
この設計の原則を知ってはいるものの、結構曖昧だったので、一から調べてまとめてみた。
そこで気づいたのは、あまり深く理解できていなかった。
やはりアウトプットする事で、新たな気づきが得られる。
SOLIDの原則とは?
オブジェクト指向の五大原則と言われている。
そして設計の際は意識するべき重要な事でもある。
・S - 単一責任の原則(SRP:Single Responsibility Principle)
・O - オープン・クローズドの原則 (OCP:Open-Closed Principle)
・L - リスコフ置換原則 (LSP:Liskov Substitution Principle)
・I - インターフェース分離の原則 (ISP:Interface Segregation Principle)
・D - 依存性逆転の原則(DIP:Dependency Inversion Principle)
単一責任の原則(SRP:Single Responsibility Principle)
「モジュールが果たす役割は1つであるべき」という原則である。
しかし、この捉え方が理解の妨げになっているようにも思う。
なので、まずは順を追ってこの言葉の意味を再定義することにする。
モジュールを使うのはアクター(ユーザのグループのような意味とする)である。
つまりモジュールが変更される理由はアクターのみにある。
よって「モジュールが果たす役割は1つであるべき」というのは、
**「モジュールはたった一つのアクターに対して責務を負うべきである。」**と解釈できる。
ただ、なぜ単一責任の原則に乗っ取りコードを書かなければいけないのか?
それは、影響を限りになく少なくできるからである。(依存を少なくできる)
ある修正が、他の箇所の修正が必要になり、そしてその連鎖が成り立つと、
一つの修正でも莫大な修正コストがかかる。
これを排除したいから単一責任の原則を採用する必要がある。
ここまでわかると単一責任の原則を実現する方法が見えてくる。
1.そのモジュールを使用するアクターを定義
例:ATMを利用する人
2.アクターの責務を見つける
例:お金を引き出す、お金を預ける
3.アクターの責務のみを負うようにモジュールを作成する
例えば、ATMを利用する人のためのモジュールがあるとしよう。
そこでは以下のような処理ができるとする。
//データ
お金
//メソッド
お金を引き出す
預金残高を確認する
お金を預ける
不正な利用がないか確認する
この責務の中でおかしなものがある。それは「不正な利用がないか確認する」だ。
この処理はアクターの債務でない。
つまり「アクターの異なるコードは分割するべき」という方針が定義できる。
ここがこの章で一番言いたい重要な事である。
少し話がそれるが、「継承」というオブジェクト思考の一つである概念は、
この単一責任の原則を果たすには、ぴったりだと思う。
例えば、アクターは違うが、同じデータを使った処理をしたいという要件は多くあると思う。
そんな時に、共有している部分は親にまとめて、振る舞いは各々の子で実装する。
そうする事で、アクターは違うが、同じデータを使った処理が実現可能になる。
オープン・クローズドの原則 (OCP:Open-Closed Principle)
「ソフトウェアの構成要素は拡張に対して開いていて、修正に対して閉じていなければならない」
という原則である。初めて聞いた時「はぁ???」と思った。
この原則を言い換えると、
「新しい機能を追加する時に、既存の成果物を変更せずに拡張できるようにするべきで、
修正は他の箇所に変更を与えてはいけない」という意味である。
上記の図ではAはBに依存していることを意味する。
※ 矢印の向き先に影響している。
つまりAの修正はBに影響はないが、Bの修正はAに影響がある。
この状態からオープン・クローズドの原則を用いて、Bの修正がAに影響を与えなくしたい。
こちら図もAはBに依存している。
ただ、Bの内部ではRepositoryの実装がインタフェースに依存している。
つまりRepository(実装)の修正は、Aに影響がないということになる。
AはRepository(インタフェース)に依存しているため、Repository(実装)の
中身が変更されても、中身のことをか知らないから影響がないとも言える。
つまり、コンポーネント通しは一方通行に依存するように設計する必要がある。
ここの詳細は、後日クリーンアーキテクチャを用いたRESTfulなAPIサーバを実装して説明する。(作成中)
リスコフ置換原則 (LSP:Liskov Substitution Principle)
「派生クラスは、基本クラスと置き換えられても正常に動作する必要がある」という原則である。
例えば、このようにBillingから基本型へ料金を催促したとする。
派生型では異なる計算モデルが適用されたPersonal用とCorporation用を使用している。
しかし、どちらの派生型もFeeを置き換えられるので、リスコフの置換原則を満たしていると言える。
Squareは正方形、縦と横の長さが常に一致している。
Rectangleは長方形、縦と横の長さが常に一致しているとは言えない。
なので縦横の長さを与えて面積を計算する処理があったとすると、UserはSquareの計算モデルを知っていないといけない。RectangleがSquareを置き換えることができない。
つまりリスコフの置換原則を満たすことで、「正しい継承関係」を実現できる。
これがこの章で一番大切なことだと思う。
インターフェース分離の原則 (ISP:Interface Segregation Principle)
「クライアントが利用しないメソッドへの依存を強制してはならない」という原則である。
上の図の例でいうと、user1がop1メソッドを、user2がop2メソッドを...使用している。
op1の変更が使用しているuser1だけでなく、user2やuser3にも影響することになる。
なぜなら依存しているから。単一責任の原則でも似たようなことを説明した。
依存をなくすために、インタフェースを分離しようぜ!という原則のもと分離すると
以下のような形になる。
これでop1の変更がuser2やuser3には関係ないものなる。
このように、使うものだけをインタフェースとして提供することで、依存関係を排除する取り組みが必要である。
依存性逆転の原則(DIP:Dependency Inversion Principle)
私はこの原則が一番好きだ。この原則を知ってからオニオンアーキテクチャやクリーンアーキテクチャの理解がすごく進んだ。
では、依存関係逆転の原則は以下の原則を満たす必要がある。
・上位のモジュールは下位のモジュールに依存してはならない。
・どちらのモジュールも「抽象」に依存すべきである。
・「抽象」は実装の詳細に依存してはならない。
・実装の詳細が「抽象」に依存すべきである。
上位モジュールとは内側のドメイン(ビジネスモデルなど)をさす。
下位モジュールとは外側の上位モジュールの実装の詳細をさす。
ダメな例では、
・上位のモジュールは下位のモジュールに依存してはならない。
→ ServiceからRepositoryに依存している。
そして以下の条件を満たしていない。
・どちらのモジュールも「抽象」に依存すべきである。
・「抽象」は実装の詳細に依存してはならない。
・実装の詳細が「抽象」に依存すべきである。
ダメな例から良い例にするために以下のことを実施した。
・赤の層にRepositoryのインタフェースを定義し、Service層が依存するようにした。
※ Repositoryのインタフェースを緑の層に書いてしまうと、下位のモジュールに依存するのでダメ
・Repositoryの実装がRepositoryインタフェースに依存するようにした。
結果、良い例では依存関係逆転の原則を満たしている。
・ServiceはRepositoryに依存していない。
・ServiceもRepositoryも「Repository(IF)」に依存している。
・「Repository(IF)」は実装の詳細に依存していない
・Repositoryの詳細が「Repository(IF)」に依存している。