【Clean Architecture】SOLID原則 - 依存性逆転の原則
はじめに
この記事は私が「Clean Architecture 達人に学ぶソフトウェアの構造と設計」を読んだ際の備忘録です。
本記事を読んで興味を持たれたら是非読んでみてください。
本記事では、参考書籍にて語られている「依存性逆転の原則」に関わる部分についてまとめています。
「依存性逆転の原則」とは
「依存性逆転の原則」は、「上位レベルのモジュールは下位レベルのモジュールに依存しないべきで、両方とも抽象に依存するべき」という原則です。
ここでの「抽象」とは抽象クラスやインターフェースなどのことです。
「上位レベルのモジュール」はビジネスロジックなどのシステムの根幹となる処理のモジュールで、
「下位レベルのモジュール」はより入出力に近い処理のモジュールです。
会計ソフトを例とすると、会計計算や決算書の作成などが上位レベルに、DBアクセスなどが下位レベルに該当します。
「依存性逆転の原則」に違反するとどうなるか
「依存性逆転の原則」に違反するということは、上位レベルのモジュールが下位レベルのモジュールに依存することになります。
この状態では下位レベルのモジュールに変更が発生する時、上位レベルのモジュールにも修正が必要になります。
決算書の出力処理を例に、「依存性逆転の原則」に違反するケースを見ていきます。
class FinancialStatementData {
// 決算書の値を保持するデータクラス (実装略)
}
class FinancialStatementPrinter {
void printStatement(FinancialStatementData statementData) {
// 決算書を印刷する (実装略)
}
}
class FinancialStatement {
FinancialStatementData data;
FinancialStatement(/* 必要な引数 */) {
// 決算書の値を計算し、インスタンスを生成する (実装略)
}
void exportStatement() {
// 決算書を出力する
FinancialStatementPrinter printer = new FinancialStatementPrinter();
printer.printStatement(this.data);
}
}
ここで、出力方式を印刷とPDF生成から選択できるようにしたくなったとします。
すると以下のような実装になります。
~~~ 略 ~~~
class FinancialStatementPDFMaker {
void makeStatementPDF(FinancialStatementData statementData) {
~~~ 略 ~~~
}
}
class FinancialStatement {
~~~ 略 ~~~
void exportStatement(String mode) {
switch (mode) {
case "print":
FinancialStatementPrinter printer = new FinancialStatementPrinter();
printer.printStatement(this.data);
break;
case "pdf":
FinancialStatementPDFMaker pdfMaker = new FinancialStatementPDFMaker();
pdfMaker.makeStatementPDF(this.data);
break;
default:
throw new IllegalArgumentException(mode);
}
}
}
この方法では出力方式が増えるたびにFinancialStatementクラスの変更が発生してしまいます。
「依存性逆転の原則」を満たすとこの問題は解決します。
そのように修正したのが以下です。
class FinancialStatementData {
// 決算書の値を保持するデータクラス (実装略)
}
interface FinancialStatementExporter {
void export(FinancialStatementData statementData);
}
class FinancialStatementPrinter implements FinancialStatementExporter {
void export(FinancialStatementData statementData) {
// 決算書を印刷する (実装略)
}
}
class FinancialStatement {
FinancialStatementData data;
FinancialStatement(/* 必要な引数 */) {
// 決算書の値を計算し、インスタンスを生成する (実装略)
}
void exportStatement(FinancialStatementExporter exporter) {
// 決算書を出力する
exporter.export(this.statement);
}
}
このようにすると出力形式が増えてもFinancialStatementクラスは変更せずに済みます。
FinancialStatementクラスが実装クラスでなくインターフェースに依存するようになったからです。
クラス図だと以下のようになります。
図1. 依存性逆転の原則に違反したクラス図
図2. 依存性逆転の原則を満たしたクラス図
「依存性逆転の原則」に従うべき時・従わなくてよい時
「依存性逆転の原則」に従うと柔軟性が上がって変更に強くなりますが、多用するとコードが冗長になってしまいます。
全てのクラスを対象にせず、使いどころを見極めて使う必要があります。
以下のような例では、「依存性逆転の原則」に従わなくてよいと思います。
- 最上位レベルのモジュール
- 外部のライブラリ
- 変更頻度が小さいモジュール
最後の例の判断は難しいため、初期は上2例以外は「依存性逆転の原則」に従うように実装して後から直接依存に切り替えると、柔軟性を維持しつつ無理なく対応できると思います。
おわりに
ここまで読んでいただきありがとうございます。
今回で「SOLID原則」の記事は最後になります。
次回以降のテーマは未定ですが、それほど期間を開かずに公開する予定です。