SOLID原則メモ
ちゃんと纏めておこうと思って書いてる。
オブジェクト思考で開発してるなら当たり前に守っていそうな制約を、ちゃんと言語化・定義してくれている原則。
概要
オブジェクト指向の5大原則
単一責任の原則(SRP:Single Responsibility Principle)
解放閉鎖の原則(OCP;Open/closed principle)
リスコフの置換原則(LSP:Liskov substitution principle)
インタフェース分離の原則(ISP:Interface segregation principle)
依存性逆転の原則(DIP:Dependency Inversion Principle)
の頭文字をとって、SOLID原則と呼ばれる。
個人的な感想だが、「依存性逆転の原則」って字面だけ見ても何のことか分からなくないか...。
何も知らなければ、A -> B の依存関係を B -> A にすればエエんやな!としか思わんやん...。
用語
安定/不安定:クラスの変更されやすさを表す。頻繁に変更される可能性のあるものは不安定である、と言える。
契約:クラスやメソッドの仕様。
✍️単一責任の原則(SRP:Single Responsibility Principle)
クラスを変更する理由は複数存在してはいけない
1つのクラスに複数の機能を持たせると、各機能に変更が発生する度に1つのクラスを修正しなければならない。
このようなクラスは非常に不安定であり、修正やデプロイも難しくなる。(コンフリクトとか)
理想的なクラスは安定しており、それぞれが独立している方が望ましい。
よって、1つのクラスには1つの責務しか持たせないようにする。
→単一責任の原則
ちなみにこれはクラスだけではなく、マイクロサービスにも当てはまる。
✍️解放閉鎖の原則(OCP:Open/closed principle)
拡張に対して開いて (open) いなければならず、
修正に対して閉じて (closed) いなければならない
概要だけ読んでも何のことかサッパリ分からない...。
簡単に説明すると
拡張することで目的を達成(新たな振る舞いが可能となる)することができ、元のコードは影響を受けない
設計方法のこと。
例えば、変更がありそうな処理は抽象化(Interface化)しておき、差し替え可能にしておく。
他にはオブザーバー使ったりとか。
雑なコード例だと以下のような感じ。
購入処理において、支払い方法は増減する可能性があるため「支払い方法インターフェース」として外出ししておく。
code:java
// 支払い方法IF
interface Payment {
// 支払い処理
PaymentResult pay();
// ファクトリ
static Payment getPayment(PaymentType type) {
switch(type) {
case CARD: return new CardPayment();
case POST: return new PostPayment();
default: throw new UnsupportedPaymentException("サポートされていない支払い方法です");
}
}
}
class CardPayment implements Payment {} // クレカ払い
class PostPayment implements Payment {} // 後払い
// 購入サービス
class PurchaseService {
void purchase(Product product, PaymentType paymentType) {
// 購入処理
Payment payment = Payment.getPeyment(paymentType);
payment.pay();
}
}
✍️リスコフの置換原則(LSP:Liskov substitution principle)
全てのサブクラスは、スーパークラスに置き換え可能でなければならない
要約に記載している以上のことはないのだけど、
他の言い方をするなら「サブクラスは、親クラスの抽象(IF)に依存するべき」。
Effectve Javaでも言及されている原則。
以下の記事は、個人的にLSPの説明としては噛み砕きすぎて、間違って解釈する人が出てきそうだなと思った。
特に
例の考え方重視した際humanクラスの持っているabilityメソッドは親と同じものを持つべきです。
Swiftではfinal演算子が使えるので親のabilityを変更させなければ良いのです。
という記載は間違っていないが極論すぎるかな、と。
「親のabilityを変更させなければ良い」というのは、置き換えではないので。
もし自分が書くなら
humanクラスの持っているabilityメソッドの契約(仕様)を、サブクラスも守るべきです。
サブクラスで契約を緩めたり、より強めたりしてはいけません。
契約を守ることでサブクラスの実装を意識せずに済むため、親クラスとしても振舞うことができます。
そのため、abilityメソッドの契約はhumanクラスで提示しておく必要があります。
になる。
✍️インタフェース分離の原則(ISP:Interface segregation principle)
クライアントが使用しないメソッドに依存することを強制するべきではない
ざっくり「インターフェースには最小限のメソッド(IF)だけを定義しましょう」ってことだと思っている。
単一責任の原則に近い。
例えば「空気清浄機」というインターフェースを定義し、各家電メーカーに「このIFに準拠して製品を作れ」と言った。
この「空気清浄機」には『空気清浄』と『加湿』メソッドが存在する。
code:java
interface 空気清浄機 {
void 空気清浄();
void 加湿();
}
各家電メーカーは「空気清浄機」IFに準拠しなければならないので、
例えコストを抑えるために空気清浄機能のみを提供する製品を作る場合でも、
"加湿機能は何もしない" という振る舞いを組み込まなければならなくなる。(加湿ボタンはあるけど飾り、みたいな感じ)
今回の場合は「空気清浄機」「加湿器」の2つのインターフェースに分け、2つを継承した「加湿空気清浄機」を作る。
code:java
interface 空気清浄機 { void 空気清浄(); }
interface 加湿器 { void 加湿(); }
interface 加湿空気清浄機 extends 加湿器, 空気清浄機 {}
こうすることで、各家電メーカーは提供したい機能ごとにインターフェースを取捨選択することが出来るようになる。
めでたし、めでたし。
✍️依存関係逆転の原則(DIP:Dependency Inversion Principle)
上位のモジュールは下位のモジュールに依存してはならない。
どちらのモジュールも「抽象」に依存すべきである。
3つ目の記事から拝借させてもらうと、
設計上望ましい依存の方向性と、素直に実装しようとしたときの方向性は矛盾しちゃうので、
そこをテクニックでカバーして逆転させると、
じつはスッキリと望ましい設計通りに実装できますよ!
という人類の知恵です。
です。
これに尽きるけど、イメージしづらい。
DDD的な例だと、
リポジトリのIFはドメイン層に置いて、リポジトリの実装はインフラ層に置く
的な感じ。
「原則」と書かれているけど、どちらかというと「手法」。