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にするのと同じだと思われるかも知れませんが、以下の点が異なります。

Factory Method

目的 FactoryMethod はインスタンスの生成に関するパターンです。 このパターンの目的は、インターフェイスに対する実装の生成・代入局面を隠し、疎結合を完成させることです。 概要 インターフェイスを用いてポリモーフィズム(多様性)を実現したいとします。また、利用側にインターフェイスだけを見せることで疎結合も実現したいとします。こうすることで、多様な実装の切り替えに対応することができ保守性を高められるからです。さて、この時気をつけなければならないのが、ポリモーフィズム適用時の隙(仮称)です。 ポリモーフィズム適用時の隙 あるサスペンスドラマで犯人が怪人の仮面をかぶっていたとします。犯人はAさんかも知れない。しかしBさんかも知れない。AさんにもBさんにも第1の犯行が可能だ。そして難解なことに第2の犯行も可能だ。 ポリモーフィズムとは「多様性」です。仕事を決めてやり方を決めない「インターフェイス」によってその多様性を担保します。多様性はインターフェイスを満足させられる実装であれば、どれでも実現可能…という仕組みで担保します。この例で説明するならば、犯人はAさんでもあり得るしBさんでもあり得るのという部分です。そして「疎結合」というのは、利用者は何を使っているのかを感知していないという構造で担保します。この例で説明するならば、目撃者は仮面をつけて行った2つの犯行しか見ていないという部分です しかし、AさんかBさんの何れかが仮面を被るその瞬間を見られていたら? 多様性はいとも簡単に崩れ去ります。ですから犯人は仮面を被る瞬間を見られてはいけません。仮面を付けた状態で物陰から登場するのです。この、物陰の役割を果たす存在こそが Factory Method、物語や設計をミステリアスにさせるエッセンスです。 以上の例からもわかるように、Factoryと聞くと生成プロセスに注目してしまいがちですが、FactoryMethodの真の目的は生成物をインターフェイスの型で提供することで、その実体を隠しきることなのです。 例えば、良く見かける生成プロセスを隠しきれていないコードを提示します。このコードは生成のプロセスを完全に隠蔽してはいません。そのため当該スコープにおける多様性は担保出来ていません。これが Factoryを使わないコードの限界です。 var e tech.Engineer = &impl.Onda{} e.Program() e.Test() e.Publish()  ではこのコードが問題であるかというと、一概にそうとも言えません。何故なら代入の瞬間以外はインターフェイスに依存していないコードであることは担保できているので、本当にポリモーフィズムが必要になったその後に、簡単にポリモーフィズムを完成させることができるからです。 [余談] ポリモーフィズムとインターフェイス分離の原則 先の例に関してですが、名探偵が、様々な状況から推測して犯人を絞り込んでゆく様は、インターフェイスの責務を増やしてゆく過程に相当します。犯行日の深夜に動けた人、凶器を調達できた人、そして被害者の行動を知っていた人。最終的にインターフェイスを満足させ続けた人が犯人です。この様にして重すぎるインターフェイスの責務は実現クラスの枯渇を招きます。謎を解き明かしたい探偵の技術としては良いものですが、抽象化の力を振るいたい設計者として悪しきものです。この件に関する詳細は「インターフェイス分離の原則」で説明したいと思います 実現 次のシナリオでは、"Hello"という文字列を格納するため、Storageを生成しています。最初のテストケースでは factoryを用いてその実体を隠蔽できていますが、二番目のテストケースでは factoryを用いないため、その実体を隠しきれていません。この瞬間に疎結合という価値は失われてしまいます。 package factory_method_pattern import ( "testing" "github.com/stretchr/testify/assert" "github.com/koooyooo/go-design-pattern/factory_method/factory" ) // FactoryMethodを適用した場合 // Storageの実体を知ることなく利用できる (疎結合) func TestFactoryMethod(t *testing.T) { // Factory経由で実体を隠蔽 s := factory.CreateStorage() err := s.Store([]byte("Hello")) assert.NoError(t, err) } // FactoryMethodを適用しない場合 // Storageの実体を知ってしまう (密結合) func TestNonFactory(t *testing.

Singleton

目的 Singletonの目的は一つだけインスタンスを生成し、それを使い回すことです。 パターンの実装としては「一つしか生成できなくする」ための制約を作り込む形となります。 OOPとインスタンス数 オブジェクト指向(OOP)の世界では、特別な理由がない限り現実世界のモノの数と、インスタンスの数を一致させます。豚が3匹いるなら Pigインスタンスを 3つ生成します。同様に車が1000台なら Carインスタンスを1000個生成します。この理由は、複製したインスタンスの数だけ固有の情報を格納できるからです。現実世界のものが増えたのならば、それを管理するオブジェクトも同じだけ増やすのが基本です。オブジェクト指向言語が現実の世界や概念の構造を雄弁に表現できるのは、この状態の複製が簡単に(new Car() or Car{})実現できるからという点が大きいのです。 さて、オブジェクトと言えば状態と振る舞いを持ちます。この両者で特に大きな進化をもたらしたのはどちらでしょうか?実は、インスタンスの増加で増えるのは状態だけです。状態はクラス(struct)定義時のメソッドの宣言から変わることはありません。インスタンスが複製されてもその振る舞いが同じ様にコピーされるだけです。唯一の振る舞いの変化が許される部分は、そのインスタンスの状態に依って振る舞いが変わるロジックが定義されている時です。しかし、オブジェクト指向言語によって主役の座に位置するのが状態であることに変わりはありません。 この常識を根底から覆すのが Singletonです。何故ならその状態を1つで良い…と言い張るのですから。しかし Singletonについて深く考察してゆくと、そこにもまた OOPの本質に迫る側面があることに気付くはずです。 一つだけインスタンスを生成してそれを使い回すと何が良いのでしょうか。すぐには思い浮かばないかも知れませんが、これが活きる局面は数多くあります。 1. 数の限定 最初に紹介するのは、本当に数が限られているものをそのとおりに表現するものです。1つしかない物を厳格に表現するなら Singletonは良い選択です。例えば日本に政府は一つしかありません。つまり以下のコードが成立します。 gov := jpgov.Instance() ディズニーランド内にミッキーマウスは複数存在しないという話を聞いたことがあります。それが本当なら次のコードも成立します。 m := disney.MickyInstance() 次にシステム的な面で考えてみます。データベースを管理するマネージャが1つしかないのであれば、以下の表現が可能です。インスタンスは関数経由で取得しても良いですし、初期ロード時に生成しても構わないなら変数として用意しておいても良いでしょう。 // 関数で取得(実装上の選択肢は広がる) dbMgr := database.ManagerInstance() // 変数で取得 dbMgr := database.ManagerInstance 2. 状態の共有 Singletonが公開されるということは、単一の状態も公開されるということです。一般的にSingletonの参照はグローバルに提供されることが多いため、どこからでもアクセス可能な単一の状態という便利なものが手に入ります。例えば、Util系のクラス(package)は振る舞いという観点で単一のものを提供しますが、Singletonでは状態という観点で単一のものが提供出来るわけです。 次の例は、Observerパターンで通知を行う局面です。システムの各所からこのObserverに通知が入ります。このObserverは Singletonとして常に単一のインスタンスが参照されるので情報を集中的に管理できています。各所で別々のインスタンスを生成してしまったのでは集中管理になりません。 func (s Service) report() { o := observer.Instance() o.reportAccess(1) } 一つしか無いというのは、一箇所で情報を管理できるという強みがあるのです。総理大臣や大統領はいくら忙しくても2名以上にしません。責務あるインスタンスに情報を集約させるための手段としても Singletonは有効なのです。 3. 参照の取得 上の例をもう少し使います。通常、Observerのインスタンスが必要なら上位の生成元から延々と引数で引き渡す必要があります。しかし、Singletonではその必要がありません。Singletonの生成部分はグローバルな関数(Javaなら staticメソッド, Golangなら グローバル関数)ですので、調達は簡単です。引数をシンプルに保てますのでその点では設計上の長所となります。 一方で、グローバルな関数に触れるというのは、その処理が副作用を持つということです。単純な長所だけではないので、この部分には気をつける必要があります。余談ですが、この解決策としてDI(Dependency Injection: 依存性の注入)という仕組みが存在し、DIが導入の際には Singletonは DIコンテナ側で実現されます。これがSingletonの完成形かもしれません。 type Service struct { // 注入される依存性 o *observer.