SOLID原則を理解する
SOLID原則は変更に強く、理解しやすいソフトウェアにすることを目的とするソフトウェアの設計原則のうち、以下の5つの原則の頭文字をとったもの。
- Single responsibility principle, SRP, 単一責任の原則
- Open–closed principle, OCP, 開放閉鎖の原則
- Liskov substitution principle, LSP, リスコフの置換原則
- Interface segregation principle, ISP, インターフェイス分離の原則
- Dependency inversion principle, DIP, 依存性逆転の原則
SRP、ISP、DIPはRobert C. Martin (opens new window)、OCPはBertrand Meyer (opens new window)、LSPはBarbara Liskov (opens new window)により考案された。
単一責任の原則
原則: モジュールを変更する理由はたったひとつだけであるべきである。
モジュールの凝集度 (opens new window)に関する原則。モジュールの責務は1つであると捉えると、簡単そうにみえる。しかし適用しようとすると、どのような粒度でみたときに責務を1つにすべきかという疑問が生まれる。考案者のRobert C. Martinは、「この原則は人についてのものである」と補足している。
This principle is about people.
モジュールを変更する理由とはそのモジュールを利用する人たち(アクター)であり、複数のアクターをもつモジュールを分割し、同じアクターを持つようにモジュールを設計すべきという原則と読み換えられる。
複数のアクターを持つモジュールの場合、以下のような変更への脆さが生まれる。
- 1つのアクターの要件変更が別のアクターの既存要件を破壊してしまう
- 複数のアクターによる要件変更により、マージ時にコンフリクトが発生する
つまり、モジュールは八方美人であってはいけない。複数の利用者がいるような複数の役割を持つモジュールでは他の利用者の要件を満たせなくなったり、複数の利用者の要件を同時に満たせなくなってしまう。
何を変更の理由と捉えるかはプロジェクトやアプリの適用する業務領域によっても異なるだろうし、何が変更の理由になりえるかを想定する必要がありそう。
自分の解釈としては、責務分割の観点として引き出しに追加しておこうと思う。
- 凝集度
- 結合度
- 再利用性
- 単一責任の原則
開放閉鎖の原則
原則: ソフトウェアの構成要素は拡張に対して開いていて、修正に対して閉じていなければならない。
なんかかっこいいけど、少し捉えづらいなという印象。平たく言えば、既存コードへの修正を最小限におさえつつ機能拡張ができるべきという原則。これができればとても安全にプロダクトを改善していくことができる。 これを実現するには、機能追加の際には新しく型を追加して振る舞いを入れ替える。さらにそのためには機能拡張が想定される箇所はポリモーフィックな呼び出しにしておく必要がある。
リスコフの置換原則
原則: ここで望まれるのは、次に述べるような置換可能な性質である:S型のオブジェクトo1の各々に、対応するT型のオブジェクトo2が1つ存在し、Tを使って定義されたプログラムPに対してo2の代わりにo1を使ってもPの振る舞いが変わらない場合、SはTの派生型であると言える。
(アジャイルソフトウェア開発の奥義 第2版より引用)
こう説明されているサイトもある。
派生型(抽象クラスやクラスを継承したクラス)は、基本型(スーパークラス)と置換可能でなければなりません。これは言語仕様のことではなく、API仕様のことです。つまり、実装での振る舞いの挙動が派生型により不都合を起こさないという意味です。
引用元: 7.1 リスコフの置換原則 (opens new window)
リスコフの置換原則はクラス同士の継承関係が正しいか(継承すべきか、正しいis-a関係か)を検証する原則で、基底クラスを派生クラスに置き換えて動作しなかった場合、その継承関係は破棄するべきとなる。
引用元: A Memorandum - The Liskov Substitution Principle (LSP) リスコフの置換原則 (opens new window)
円は楕円の派生系ではない、正方形は長方形の派生系でないという例がよく見つかる。 利用側のクラスが円を楕円として扱ってしまうと、縦幅と横幅を別々の値に変更しようとした際に、縦幅≠横幅となり円でなくなるか、想定通り変更されないということが起こる。 間違った継承で設計すると置換したときに不具合が生まれる可能性があるということだろうか。
インターフェイス分離の原則
原則: クライアントにクライアントが利用しないメソッドへの依存を強制してはならない。
この法則は理解がしやすい。必要のないものには依存しない、それだけだ。必要のないものに依存していると、関連性のないものの変更により、再ビルドやデプロイが必要になる。これを解消するには必要のあるメソッドのみをインタフェースとして定義し、そのインタフェースに依存させる。変更の影響を最小限にするために、関心のないものには依存させない。
依存性逆転の原則
原則: 上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも抽象に依存すべきである。抽象は実装の詳細に依存してはならない。実装の詳細が抽象に依存すべきである。
どちらのモジュールも抽象に依存すべきである。
具体(具体型)と抽象(抽象型)が何か、というのが初心者のプログラマーにわかりにくい部分だと思う。
オブジェクト指向の継承が実装可能なプログラミング言語の場合、スーパークラスは抽象に該当する。例えば、Catを定義した場合に、Cat型がAnimal型を継承していればCat型は具体型、Animal型は抽象型ということになる。ポリモーフィズム (opens new window)を利用したコーディング(抽象に対する実装)をすれば、具体型を自由に置換可能になる。
Swiftの場合、オブジェクト指向だけでなく、プロトコル指向というプログラミングパラダイムが適用されている。プロトコル (opens new window)でインタフェースを定義でき、これが抽象として扱える。プロトコルは単一継承しかできないクラスと違い、複数のプロトコルへの準拠が可能なため、その組み合わせにより柔軟な設計ができる。(プロトコルコンポジション)
この具体型を依存性注入(Dependency Injection, DI)パターンを用いて利用側の型の外部から設定することで、利用側の型の内部では具体型に依存しなくなる。
# ポリモーフィズムを利用しない場合
利用側の型--->具体型
# スーパークラスの場合の依存関係
利用側の型--->抽象型(スーパークラス)<---具体型(サブクラス)
# プロトコルの場合の依存関係
利用側の型--->抽象型(プロトコル)<---具体型(プロトコル実装)
こうすることで、利用側の型も具体型も抽象に依存していることがわかる。さらに、元の状態に比べ、下位が上位に依存するようになり、依存関係の方向が逆転する。
ただし、標準のフレームワークといった下位モジュールへの依存は気にしない。それらは変化することが稀であり、すべてを抽象化することも難しい。この法則は変わりやすいものを抽象化(厳密には上位モジュールが必要な振る舞いを抽象化)し、変わりやすい実装の詳細でなく安定したインタフェースに依存させるべき、というものになる。
まとめ
SOLID原則はシステムに起こりうる変更に着目し、それをどう対処すべきかと言う方針がまとめられている。
- 単一責任の原則: モジュールを変更する理由となりえる利用者が複数いるような八方美人なモジュールは分割する。
- 開放閉鎖の原則: 機能拡張として変更が想定される箇所は型によって置換可能にしておく
- リスコフの置換原則:
- インターフェイス分離の原則: 変更の影響を最小限にするために、関心のないものには依存させない。
- 依存性逆転の原則: 変更されやすい実装には依存させず、安定した抽象に依存させる。
上記は自分なりの解釈なので、提唱当時の時代背景や現在のプログラミング方法からすると少し違うかもしれない。 もっと噛み砕いた表現をすると、以下のようになる。
- 変わりやすいものと変わりにくいものを分離する、想定する
- 変わりやすい関係のないものに依存させない、変わりにくいものに依存させる
- 変わるものは既存に影響させない、置き換えても壊れない、置き換えやすい作りにすべき
感想
全ての原則が全く関連のないものではなく、少しずつ関連していてわりと同じことを別の視点、言い方で表現しているようにも感じた。 同じ目的に対する別の観点でのアプローチなので、当然と言えば当然かもしれない。
参考書籍
- アジャイルソフトウェア開発の奥義 第2版|SBクリエイティブ
- Clean Architecture 達人に学ぶソフトウェアの構造と設計