初めに
第Ⅲ部では「設計の原則」SOLIDについて勉強メモ/備忘録です。
前回
概要
SOLIDの原則は中間レベルのそれぞれの原則の頭文字を組み合わせたものです。
SOLID原則の目的は、
- 「変更に強いこと」
- 「理解しやすいこと」
- 「基盤として多くのシステムで利用できること」
この3つの性質をもった中間レベルのソフトウェア構造を作ることです。
ここでいう「中間レベル」というのはコードレベルより上の
「モジュール」を意図しています。
つまり、関数やデータ構造をどのようにクラスに組み込むのか、
そしてクラスの相互接続をどのようにするのかの原則です。
例えるならば、レンガの組み合わせて壁や部屋を作る原則です。
SOLIDの原則とは?
- 単一責任の原則(SRP:Single Responsibility Principle)
- 変更する理由がたった一つだけになるように
- オープン・クローズドの原則(OCP:Open-Closed Principle)
- 拡張に対しては開いていて、修正に対しては閉じていなければならない
- リスコフの置換原則(LSP:Liskov Substitution Principle)
- 個々のパーツが交換可能となるような契約に従わなければいけない
- インターフェイス分離の原則(ISP:Interface Segregation Principle)
- 使っていないものへの依存を避けるべき
- 依存関係逆転の原則(DIP:Dependency Inversion Principle)
- 上位レベルの方針の実装コードは、下位レベルの詳細の実装コードに依存すべきではなく、逆に詳細側が方針に依存すべきである
単一責任の原則(SRP)
-
原則自体は関数やクラスに関する原則
- コンポーネントレベルでは「閉鎖性共通の原則(CCP)」と呼ばれる
- アーキテクチャレベルでは「アーキテクチャの境界」を作るための「変更の軸」と呼ばれる
-
ここではモジュール単位で話されている
-
モジュールはたった一つのアクターに対して責務を全うすべきである
最初の原則は「単一責任の原則」です。
ここでは「モジュール単位」での解説になります。
「モジュール」とは、システムの一部を構成するひとまとまりの機能を持った部品のことです。
Clean Architectureでは「モジュールはたった一つのアクターに対して責務を全うすべきである」と書かれています。
「アクター」とは、システムを利用する想定される人や組織、関係する外部システムなどを指しています。
では具体例を見てみたいと思います。
SRP: 給与システムにおけるEmployee クラス:事例
具体例として給与システムにおけるEmployeeクラスをみてみます。
図はユースケースとしてよくある図ですが、
Employeeを直訳すると「社員」
アクターの
「CFO(Chief Financial Officer)」は = 最高財務責任者
「COO(Chief Operating Officer)」は = 最高執行責任者、日々の業務執行の責任を請け負う
「CTO(Chief Technical Officer)」は = 最高技術責任者、になります。
その「Employee」クラスには
calculate(キャルキュレート)Payメソッド、つまり「給与計算」で経理部門が規定しCFO(最高財務責任者)へ報告、
reportHoursメソッド、つまり「勤怠表」で人事部門が使用しCOOに報告、
saveメソッドは、DB管理者であるCTOに報告します。
これら3つのメソッドはそれぞれ別のアクターに対する責務を負っており、単一責任の原則に違反しています。
SRP: 給与システムにおけるEmployee クラス:想定外の重複
先程のアクターの異なるメソッドが一つのクラスにあると想定外の重複が発生してしまいます。
開発者は全てのアクターを結合しようとし、その結果経理部門の何らかの操作が、人事部門の使うものに影響を及ぼしてしまう可能性が発生します。
図のように、給与計算のcalculatePayメソッドと勤怠表のreportHoursメソッドの両方で所定労働時間を算出していたとすると、重複を避けるために、この計算部分をregularHourseメソッドに切り出してしまうと、どちらかの算出方法を変更しようとすると、どちらかに影響が出てしまい誤った数値を出してしまいます。
本来はアクターが違うので、求めるものも違うはずです。
SRP: 給与システムにおけるEmployee クラス:マージ症状
アクターの異なるコードが分割されていないと、他にも症状が発生してしまいます。
それは、1つのソースファイルに対して変更理由が複数あるので、コンフリクトが多発してしまいます。
コンフリクトのリスクはいうまでもなく最悪の場合、多くのリソースをかけてデグレを解決しなければいけない状況に陥ります。
こうした問題を回避するためにもやはり、アクターの異なるコードは分割するべきです。
SRP: 給与システムにおけるEmployee クラス:解決策
- アクターの異なる関数を別のクラスへ移動させる
- データを関数から切り離す
- Facadeパターン
単一責任の原則に違反した際の解決策をみてみます。
まずはアクターの異なる関数を別のクラスへ移動させて、データを関数から切り離します。
export default class EmployeeData {
private id: number;
private name: string;
private salary: number;
constructor(id: number, name: string, salary: number) {
this.id = id;
this.name = name;
this.salary = salary;
}
}
import EmployeeData from './EmployeeData';
export default class PayCalculator {
private employeeData: EmployeeData;
constructor(employeeData: EmployeeData) {
this.employeeData = employeeData;
}
calculatePay() {}
}
3つのクラスは相手を知りません。
ただし、ここまででは都度クラスをインスタンス化して、常に追跡する必要があります。
その解決策として次の例がFacadeパターンです。
SPR: 給与システムにおけるEmployee クラス:Facade
先程の課題を解決するためのFacadeパターンの適用になります。
import EmployeeData './EmployeeData';
import EmployeeSaver './EmployeeSaver';
import HourReporter './HourReporter';
import PayCalculator './PayCalculator';
class EmployeeFacade {
private _payCalculator?: PayCalculator;
private _hourReporter?: HourReporter;
private _employeeSaver?: EmployeeSaver;
calculatePay = (data: EmployeeData): void => {
if(!this._payCalculator) {
this._payCalculator = new PayCalculator(data);
}
this._payCalculator!.calculatePay();
};
reportHours = (data: EmployeeData): void => {
if(!this._hourReporter) {
this._hourReporter = new HourReporter(data);
}
this._hourReporter!.reportHours();
};
save = (data: EmployeeData): void => {
if(!this._employeeSaver) {
this._employeeSaver = new EmployeeSaver(data);
}
this._employeeSaver!.save();
};
}
const employeeFacade = new EmployeeFacade();
export default employeeFacade;
Facadeに含まれるコードは決して多くはありませんが、その責務は実行したいメソッドを持つクラスのインスタンスを生成して、処理を委譲するだけになります。
また、重要なビジネスルールをデータの近くに置きたい場合は、
元のEmployeeクラスに重要なメソッドだけを残して、重要ではないメソッドを呼び出すクラスをして使うようにします。
メソッドが一つしかないクラスばかりが作られるので、このコードに疑問を持つかもしれませんが、
規模が大きくなるにつれて、メソッドの数は必ず増えていきます。
以上で単一責任の原則に違反して際の解決策を手順通りに行いFacadeパターンまでを適用しました。
オープン・クローズドの原則(OCP)
- 1988年にBertrand Meyerが提唱した
- 拡張に対しては開いていて、修正に対しては閉じていなければならない
続いてのSOLID原則の2つ目は、「オープン・クローズドの原則」についてです。
言い換えれば、既存の成果物を変更せずに拡張できるようにすべきであるということです。
ちょっとした拡張のために大量の書き換えが必要になるようなら、そのソフトウェアのアーキテクトは大失敗の道を進んでいるサインです。
OCP: 財務情報を表示するWeb app
例として、財務情報をWebページに表示するシステムがあり、データは上からスクロールできて負の数値は赤く表示されるものだったとします。
ステークホルダーから「画面と同じものを白黒のプリンターで印刷したい」と新しい要求があると、
実現させるために様々な考慮を重ねる必要があります。
要求を実現させるために新たにコードを書く必要があるのは確実だとして、既存コードの修正がどれくらいになるか?がこの原則の焦点になります。
- 新しい要求
- 画面と同じものを白黒のプリンターで印刷したい
- 実現させるには
- 積雪なページ処理
- 各ページのヘッダー/フッター
- 列の見出し
- 負の数値は括弧で囲む
OCP: 財務情報を表示するWeb app:解決策、SRP
- 変更する理由が異なるものは、単一責任の原則で分割する
- 単一責任の原則を適用するには、データフロー図を使う
- 今回はレポート作成の責務が2つになるため分割する
まず解決策として、単一責任の原則に則りデータフロー図を使いながら適切に分割します。
今回はレポート作成の責務が2つになるため分割します。
OCP: 財務情報を表示するWeb app:解決策、方向の制御
- それらの依存関係を依存関係逆転の原則で適切にまとめる
- 一方に影響を与えることなく変更できるように
- 処理をクラスに分割する
- それぞれのクラスをコンポーネントにまとめる
- 二重線(コンポーネント)を超える線は全て一方通行になっていること
- オープン・クローズドの原則に最も適しているのは、Interactorである
- Interactorは最上位レベルのビジネスルールを含んでいるため何の影響も及ぼさない
- 一方に影響を与えることなく変更できるように
続いて、単一責任の原則により分割したクラスを依存関係逆転の原則で適切に方向の制御を行います。
これは一方に影響を与えることなく変更できるようにするためで、例えば「コンポーネントA」を「コンポーネントB」の変更から保護したいのであれば、「コンポーネントB」から「コンポーネントA」に依存させるべきです。
アーキテクチャレベルでオープン・クローズドの原則に最も適しているのは、
Interactorになります。
Interactorには最上位レベルのビジネスルールを含んでいるためどの影響も及ぼさないためです。
OCP: クラスレベル
「拡張に対しては開いていて、修正に対しては閉じている」は、いわゆるStrategy(ストラテジー)パターンになると思います。
リスコフの置換原則(LSP)
- 1988年に Barbara Liskov が派生型について定義した
- ざっくりまとめると「派生クラスはその元となったベースクラスと置換が可能でなければならない」
- 派生クラスでオーバーライドされたメソッドはベースクラスのメソッドと同じ数・型の引数ととらなければならない
- 派生クラスでオーバーライドされたメソッドの返り値の型はベースクラスのメソッドの返り値の型と同じでなければならない
- 派生クラスでオーバーライドされたメソッドの例外はベースクラスのメソッドの例外と同じ型でなければならない
続いては、リスコフの置換原則についてです。
こちらの概要はざっくりまとめると、「派生クラスはその元となったベースクラスと置換が可能でなければならない」です。
・派生クラスでオーバーライドされたメソッドはベースクラスのメソッドと同じ数・型の引数ととらなければならない
・派生クラスでオーバーライドされたメソッドの返り値の型はベースクラスのメソッドの返り値の型と同じでなければならない
・派生クラスでオーバーライドされたメソッドの例外はベースクラスのメソッドの例外と同じ型でなければならない
となります。
噛み砕くと
親クラスと子クラスは異なる振る舞いを起こさず、互換性を持っているということです。
もっというと「親クラスのルールを子クラスが破ってはならない」になり、
意図しないオーバーライドに注意して親クラスを使っても子クラスを使っても同じ結果が得られるようにしましょうと言うことです。
継承のお約束ごとがあり、それは同じ動作が保証されることだったりで、それに該当します。
インターフェイス分離の原則(ISP)
- 使用していないものに意図せず依存すべきではない
続いては、インターフェイス分離の原則です。
これは至って簡単で、図のようにUser1/2/3それぞれが使用していないものに依存すべきではないということです。
出典元)Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ)
これを分離させると、以下になります。
出典元)Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ)
依存関係逆転の原則(DIP)
- 依存関係が具象ではなく抽象だけを参照しているものが、最も柔軟なシステムである
- JSだとimportの参照先がinterface/abstractなどの抽象宣言だけに限定する
- 現実的にはOSヤプラットフォームは変化しないとみなす
- 開発中のモジュールや頻繁に変更されているモジュールが対象になる
DIP: 安定した抽象
- 変化しやすい具象クラスを参照しない
- 型ありでも動的でもinterface/abstractを参照する
- 一般的にはAbstract Factoryパターンを使うしかない
- 変化しやすい具象クラスを継承しない
- 上記と同じ
- extendsの参照先は抽象クラスであるべき
- 具象関数をオーバーライドしない
- 具象関数はソースコードの依存を要求することが多い
- 関数を抽象関数にして、それに対する複数の実装を用意する
- リスコフの置換原則に違反しないことも求めています
- 変化しやすい具象を名指しで参照しない
- 上記の原則を言い換えただけ
上位モジュールは下位モジュールに依存せずに、どちらも抽象化に依存するべきであるということと、
抽象化は実装に依存することなく、実装は抽象化に依存するべきである。
モジュール間を疎結合にしましょうということです。
DIP: Factory
先程のルールに従うと、具象オブジェクトを生成する際にはAbstract Factoryパターンを使い依存性を管理するしかありません。
出典元)Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ)
こちらはクラス図でみたままになります。
こちらの図で横切る灰色の太線がありますが、これが抽象と具象の区切りになります。
この曲線を横切るコードの依存性は、全て具象側から抽象側へと向かっています。
また、この曲線はイコールコンポーネントの分割になります。
図のように曲線を横切る処理の流れは、コードの依存性とは逆向きになり、依存関係逆転の原則の由来です。
このAbstract Factoryパターンにおいても、依存関係逆転の原則の違反を完全に取り除くことはできませんが、
違反をしない具象コンポーネントを最小に抑えることはできます。
違反しているコンポーネントはすくなくと1つは存在しており、それは一番の汚れ役であるmain関数を含んでいるからです。
第Ⅲ部:まとめ
- 単一責任の原則
- (SRP:Single Responsibility Principle)
- 変更する理由がたった一つだけになるように
- オープン・クローズドの原則
- (OCP:Open-Closed Principle)
- 拡張に対しては開いていて、修正に対しては閉じていなければならない
- リスコフの置換原則
- (LSP:Liskov Substitution Principle)
- 個々のパーツが交換可能となるような契約に従わなければいけない
- インターフェイス分離の原則
- (ISP:Interface Segregation Principle)
- 使っていないものへの依存を避けるべき
- 依存関係逆転の原則
- (DIP:Dependency Inversion Principle)
- 上位レベルの方針の実装コードは、下位レベルの詳細の実装コードに依存すべきではなく、逆に詳細側が方針に依存すべきである
文献
Comments
Let's comment your feelings that are more than good