SOLID原則

概要 プログラム設計上の原則である SOLID原則 に関する記事です。個々の原則は何となくわかるものの、それぞれが目指す目的は何か、その手段として何を試みているのか、それによってこの原則ができたのか、といったポイントは聞かれてもサッと出てこなかったりします。それは多分、まだ理解しきれていない部分があるということかなと思います。 今回はより俯瞰的な観点から、それぞれの原則の特徴や立ち位置を分析してみたいと思います。 各技法 Single Responsibility Principle (単一責任の原則) 「これはね、Webも DBも メールも 何でもこなしている便利なサーバーなんだよ。」 「それって下手に触ると全部止まってしまうってことかい?」 SRPはクラスの責務を限定することで、保守の範囲を限定化するための原則です。 SOLID原則 - SRP Open Closed Principle (開放閉鎖原則) 「このマシンは CPUもメモリもディスクも拡張可能にできているのさ。 それに変更がケースの内側だけなら、ユーザーや外部デバイスも気づかないだろ?」 OCPは クラスの拡張性を最大化し、デグレードのリスクを最小化するための原則です。 Liskov Substitution Principle (リスコフの置換原則) 「こちらが私の業務を引き継ぐ方です。 皆さんからの指示は従来どおりで大丈夫です。もちろん同じ結果も保証しますよ。」 LSPは 継承したクラスに対し互換性を保証させるための原則です。 Interface Segregation Principle (インターフェイス分離の原則) 「弁護士の資格と医師の資格、一緒にしたら便利な資格にならないか?」 「おいおい、取得する側の立場にもなってみろよ」 ISPは インターフェイスを適切に分離し実現の負荷を最小化する原則です。 Dependency Injection Principle (依存性逆転の原則) 「明日の会議には翻訳担当と議事担当が必要なんだが、誰に頼めばいい?」 「心配しなくても誰かを着席して待たせておきますから、本業に集中して大丈夫ですよ」 DIPは インターフェイス間の結合を完全に疎にするための原則です。 一覧 実際に一覧にしてみましたが、改めて見るとそれなりに情報量があります。一気に理解しようと思うと厳しいので、徐々に消化してください。 最初に表の項目に関して簡単に説明します。 英名・和名:原則の英名と和名を提示します 原則と実現手段:基本的な原則を太字で記載し、その下に実際にどう実現するかという手法を提示します クラス・継承・I/F:原則を作り込む際に使用する抽象度を(具象クラス・継承関係・I/F実現)の3種に分類して提示します 縦軸の作り込み・横軸の作り込み:原則を作り込む箇所を縦軸(クラスの継承関係)と横軸(クラスの利用関係)で提示します 縦軸の効果・横軸の効果:原則の効果を縦軸(クラスの継承関係)と横軸(クラスの利用関係)で提示します 最終目的:技法は手段ですが その目的は何なのかを改めて振り返ります # 英名 和名 原則と実現手段 具象 継承 実現(I/F) 縦軸の作り込み 横軸の作り込み 縦軸の効果 横軸の効果 最終目的 1 Single

Solid原則 - SRP

Single Responsibility Principle (単一責任の原則)  “クラス変更する理由は1つ以上存在してはならない” 「これはね、Webも DBも メールも 何でもこなしている便利なサーバーなんだよ。」 「それって下手に触ると全部止まってしまうってことかい?」 SRPはクラスの責務を限定することで、保守の範囲を限定化するための原則です。 概要 -- 冒頭の会話の例だと、最近ではコンテナを活用して環境を分離するのではないでしょうか。コンテナは一つのプロセスに責任を持つ存在です。一つの目的のために用意された環境なので、その目的に沿わない部品は気軽に変更・削除ができます。同じ考え方はプログラムにも適用可能です。プログラムはプロセスの構成要素ですから責務は更に細分化されます。 細分化はアプリケーションが提供する機能による細分化、または1つの機能を実現するための役割による細分化、といった複数の切り口から行うことができます。機能による細分化は作るアプリケーションに依って異なりますが、役割による細分化の具体例としては、次のようになります。 [情報系] メモリ上の情報保持に責任を持つモデル [処理系-Biz] アクターとのユースケースを制御するコントローラ [処理系-Biz] アトミックなロジックに責任を持つサービス [処理系-Biz] 永続データのやり取りに責任を持つリポジトリ [処理系] ビジネスを意識しない汎用的なユーティリティ *1) Biz = ビジネスロジック系 こうして責務を細分化してゆくと、クラスが負う責務は1つになり、責務が絞られるとそのクラスを変更する理由も1つに絞られます。つまり単一の責任しか負わないわけです。単一の責任しか負わないクラスでは次のような変化が起こります。 コード量減とノイズ減を実現することができ 読み込む際の理解容易性が向上し 更新する際の副作用が低下する これが保守をする際の安全性に繋がります。この様に、単一の責任だけを持ったクラスが協調して全体を構築するべきだという原則が、単一責任の原則となります。 特徴 単一責任の原則には幾つかの特徴的な部分があります。 1つ目はクラスの抽象化観点(縦軸)における継承やポリモーフィズムとの関連の薄さです。リスコフの置換原則やインターフェイス分離の法則、依存性逆転の法則は継承やポリモーフィズムといった言語機能の活用を前提としたパターンですが、単一責任の原則にはその前提がありません。単なるクラスにも適用可能です。単一責任の原則は純粋に責任を分割するだけの原則なので、特に言語機能には依存しないのです。しかし、依存しないとは言え無関係な訳ではありません。継承時には追加した実装分に単一の責任をもたせ、インターフェイス実現時にはその実装に対して単一の責任を持たせる…というデザインで付加価値を与えることは可能だからです。 2つ目はクラス間連携(横軸)における適用局面の広さです。単なるクラス間の関係にも、継承やインターフェイスの実現といった"is-a"関係にも関連する原則だからです。特定の言語機能に依存しない考え方の原則というのはこういう点で強力です。更にはサブシステム間、マイクロサービス間、システム間、といったより大きな粒度においても適用可能ですし、逆にクラスより粒度の細かいメソッドの責務設計にも適用できます。具体的には「ビジネストランザクション全体」「再利用可能なビジネスフロー」「非ビジネスユーティリティ」といった粒度での責務分割がこれに相当します。こういった適用範囲の広さも特徴的なわけです。 3つ目は適用局面の広さに関連しますが、初期フェーズにおける適用の重要さ(時間軸)です。デザイン全体に適用可能なものですから、設計初期に単一責任の原則を理解した設計者が居るか居ないかで、その後のシステムにおける保守性は大きく変わります。それは原野を前に都市計画を用意する様なもので、その有無で区画に用途を定めた整然とした街ができるのか、または予測不可能な蚤の市の様になるのかが分かれます。 以上の様に、非常に大きな効果を得られるので、最初に覚える原則としては最もおすすめできるもの、それが単一責任の原則となります。 コードサンプル (JVM系の言語のほうが得意ですが、勉強がてらGolangで書いてみました) Before 以下のコードはサーバーを表現しています。このサーバーは Webサーバ・DBサーバ・Mailサーバの3種類の役割を果たしています。昔はこの様なサーバも多く見かけましたが、1つのサーバー内に複数の責務をもたせると、このサーバーをメンテナンスする理由が増えます。しかも、メンテナンス時には他の機能に影響が出ないように慎重にならなければなりません。これは現実の世界でもコードの世界でも同じです。単一の責任をもたせるようにリファクタリングしなければなりません。 // 複数の責務を負ってしまっている状態 type Server struct { HTMLs []*web.HTML DBConn *db.Conn Mails []*mail.Mail } func (s Server) ServeWeb() error { } func (s Server) ServeDB() error { } func (s Server) ServeMail() error { } After 以下のコードはサーバーの問題点を修正したコンテナを表現しています。コンテナはインターフェイスで表現され、実体はWebコンテナ・DBコンテナ・Mailコンテナに分かれ、それぞれが独自の役割を果たします。つまり、各コンテナは単一の責任しか負いませんので、修正の理由もその責務の範囲内のものに限定されます。こうすると修正対象のコードが限定され、保守しやすくなります。さらに、修正の影響もコンテナ内に閉じられますから、他の機能に変更の副作用が出ることはありません。これもまた現実の世界でもコードの世界でも同じです。

Solid原則 - OCP

Open Closed Principle (開放閉鎖原則)  “クラスは拡張に対して開いていなければならず 修正に対して閉じていなければならない” 「このマシンは CPUもメモリもディスクも拡張可能にできているのさ。 それに変更がケースの内側だけなら、ユーザーや外部デバイスも気づかないだろ?」 OCPは クラスの拡張性を最大化し、デグレードのリスクを最小化するための原則です。修正が外部に影響しないようにクラス外との窓口を限定し(Close)、修正自体を阻害しないようにクラス内の拡張ポイントを意識して設計します(Open)。 概要 冒頭の例では、マシン(コンピュータ)を引き合いに出して説明しました。マシンは内部に多くの拡張ポイントを用意している一方で、外部との接触はPCケースによって限定され一部のボタンや端子からしかやり取りできません。 例えば、マシンのケースを開ければ HDDやメモリのスロットによりパーツの追加・交換を簡単に行うことができます。これが「拡張に対して開かれている」部分です。逆に、専用メモリが基盤に埋め込まれており追加・交換ができなければ「拡張に対して開かれていない」ことになります。 一方で、マシンのケース自体は外部との接触を限定する役割を持ちます。ケースはマザーボード等の脆弱な内部基盤を隠蔽し、最小限のインターフェイスを外部に公開します(カプセル化)。具体的には「電源ボタン・ディスクドライブ・USB端子・電源ソケット」という最小限の窓口のみ露出され残りはケースで覆われます。露出部分の挙動さえ担保できれば、それを利用する側に修正の影響は及びません。これが「修正に対して閉じられている」部分です。修正に対して閉じられていれば、デグレードの危険性や再テストの必要性を最小化できます。 修正に対して閉じられている構造は、何もマシンに限った構造ではありません。自動車や船舶等の機械製品はもちろんのこと、人間や動物の体だって修正に対しては閉じられています。ですから運転手が直接エンジンを触って始動することは不可能ですし、動物が食べ物を口を介さずに胃に押し込めることも不可能です。イグニッションキーによる始動や口で咀嚼してからの嚥下で問題ありませんし、むしろその方が便利で安全にできています。例えば、ブレーキが踏まれていなければ始動できませんし、毒草を口にした際には味覚が異常を検知してくれます。なお、これをプログラムで表現すると Setter内に Validation機能を付加した状態に相当します。 特徴 OCPの特徴は、スコープ視点では最適化の範囲がクラスに特化している点です。また、この最適化が、クラスには最適な責務が割り当てられていることが前提で、その責務における変更には継承や委譲を用いて開放し、他クラスからの干渉にはカプセル化を用いて閉鎖するという手段をとります。 コードサンプル 実装に話を移します。コード上での Open-Closed-Principle(開放閉鎖原則) は継承と委譲による縦方向(自身の関連)の開放と、カプセル化による横方向(他者との関連)の閉鎖によって実現します。縦方向と横方向と表現しているのは、一般的にクラス図上では自身の継承関係は親クラスを上方に、子クラスを下方に配置して表現するからです。 閉鎖 閉鎖部分は変数やメソッドの可視性で制御します。これらの可視性を private (Javaなら protectedも可) にすることで外部アクセスを遮断します。こうすれば外部のクラスから変数やメソッドにアクセスされることはありません。 public class Machine { private boolean powered; private CPU cpu; private Memory memory; private HardDisk hardDisk; private Set<USBDevice> usbDevices = new HashSet(); private Set<HDMIDevice> hdmiDevices = new HashSet(); // 振る舞いを省略 } (Javaによる閉鎖) 外部から変数へのアクセスが必要な場合は、Getter / Setter等の publicなアクセッサメソッドを通じてアクセスを許可します。結局はアクセスさせてしまうのであれば最初から変数を publicにするのと同じだと思われるかも知れませんが、以下の点が異なります。