【Clean Architecture】SOLID原則 - 依存性逆転の原則

Page content

はじめに

この記事は私が「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原則」の記事は最後になります。
次回以降のテーマは未定ですが、それほど期間を開かずに公開する予定です。