概要

SOLID 原則は設計の教科書では必ず登場しますが、「原則は知っているけれど、実際のコードにどう落とし込むのか分からない」という声は少なくありません。とくに業務システムの保守現場では、単一責任原則に違反して肥大化したクラスや、新しい種別を追加するたびに if-else を書き足すコードに日常的に遭遇します。この記事では、注文処理という身近な題材を使い、S(単一責任)・O(開放閉鎖)・D(依存性逆転)の3原則を Java コードで実装します。悪い例と良い例を対比しながら、「なぜこの分割にするのか」「どこまで分ければ十分なのか」という現場での判断基準を整理します。Java 8 のクラスベースの実装から Java 17 の record、Java 21 の sealed interface + switch パターンマッチングまで、バージョンごとの書き方の進化も確認します。

使いどころ

注文クラスに永続化・メール通知・PDF生成が混在しているコードを、責任ごとにクラス分割してリファクタリングする

割引種別(学生・会員・VIP)の追加が発生するたびに既存コードを変更せずに済むよう、Strategy パターンで拡張ポイントを設ける

単体テストでデータベース接続なしに業務ロジックを検証するため、Repository を interface 化してモックに差し替える

コード例

SolidPrinciplesDemo.java
import java.util.List;

public class SolidPrinciplesDemo {

    // === S: 単一責任原則 ===
    // Order は「注文データを表す」責任だけを持つ
    record Order(String product, int quantity) {}

    // 永続化は Repository の責任
    static class OrderRepository {
        public void save(Order order) {
            System.out.println("DB に保存: " + order.product());
        }
    }

    // 通知は Notification の責任
    static class OrderNotification {
        public void sendEmail(Order order) {
            System.out.println("メール送信: " + order.product());
        }
    }

    // === O: 開放閉鎖原則 ===
    // 新しい割引タイプを追加しても DiscountCalculator は変更不要
    interface DiscountStrategy {
        double apply(double price);
    }

    static class DiscountCalculator {
        public double calculate(DiscountStrategy strategy, double price) {
            return strategy.apply(price);
        }
    }

    // === D: 依存性逆転原則 ===
    // 具体クラスではなく interface に依存する
    interface OrderRepositoryInterface {
        void save(Order order);
    }

    static class OrderService {
        private final OrderRepositoryInterface repo;

        // コンストラクタ注入: テスト時にモックへ差し替え可能
        public OrderService(OrderRepositoryInterface repo) {
            this.repo = repo;
        }

        public void processOrder(Order order) {
            repo.save(order);
        }
    }

    public static void main(String[] args) {
        // S: 責任ごとに分離されたクラスを使う
        var order = new Order("ノートPC", 1);
        new OrderRepository().save(order);
        new OrderNotification().sendEmail(order);

        // O: ラムダ式で戦略を定義(クラス追加不要)
        var calc = new DiscountCalculator();
        DiscountStrategy student = price -> price * 0.8;
        DiscountStrategy member  = price -> price * 0.9;
        DiscountStrategy vip     = price -> price * 0.7;

        System.out.println("学生割引: " + calc.calculate(student, 10000));
        System.out.println("会員割引: " + calc.calculate(member, 10000));
        System.out.println("VIP割引: " + calc.calculate(vip, 10000));

        // D: interface 経由でテスト用モックに差し替え
        OrderRepositoryInterface prodRepo =
            o -> System.out.println("本番DB: " + o.product());
        OrderRepositoryInterface testRepo =
            o -> System.out.println("[テスト] モック: " + o.product());

        new OrderService(prodRepo).processOrder(order);
        new OrderService(testRepo).processOrder(order);
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record で Order を不変データクラスとして簡潔に定義できる。DiscountStrategy をラムダ式で実装すると、個別クラスを作らずに済む。

Java 17
// Java 17: ラムダ式で戦略を定義(クラス不要)
record Order(String product, int quantity) {}

DiscountStrategy studentDiscount = price -> price * 0.8;
DiscountStrategy memberDiscount  = price -> price * 0.9;
DiscountStrategy vipDiscount     = price -> price * 0.7;

var calc = new DiscountCalculator();
System.out.println(calc.calculate(vipDiscount, 10000));

Library Comparison

標準 API(interface + record)依存なしで SOLID に沿った設計を組み立てるとき。小〜中規模のプロジェクトや、フレームワーク非依存の共通ライブラリに適する。DI コンテナがないため、依存の組み立て(配線)は手動で行う必要がある。クラス数が増えると初期化コードが煩雑になる。
Spring Framework(@Component / @Autowired)依存の自動解決やスコープ管理が必要な大規模プロジェクト。AOP でロギングやトランザクションを横断的に適用したいとき。フレームワーク自体の学習コストがある。テストでもSpring コンテキストの起動が必要になる場合がある。
Google GuiceSpring ほど大きなフレームワークは不要だが、DI コンテナの恩恵は受けたいとき。軽量で学習コストが低い。アノテーションベースの設定が主で、XML 設定はサポートしない。エコシステムは Spring ほど充実していない。

注意点

単一責任原則を厳密に適用しすぎると、クラスが細分化されすぎてコードの追跡が困難になる。「変更理由が異なるか」を基準に、過度な分割を避ける

開放閉鎖原則のために interface を導入しても、実装が1つしかない場合は過剰設計になりやすい。将来の拡張が見込まれるかどうかを判断材料にする

依存性逆転原則でコンストラクタ注入を使う場合、依存の数が多いクラスは責任過多のサインである。引数が4つを超えたら単一責任原則に立ち返る

SOLID はあくまで指針であり、すべてを満たすことが目的ではない。保守コストと開発速度のバランスを見て適用範囲を決めること

FAQ

SOLID の L(リスコフの置換原則)と I(インターフェース分離原則)は業務コードでどう意識すればよいですか。

L は「親クラスの契約をサブクラスが破らないこと」、I は「使わないメソッドへの依存を強制しないこと」です。まずは S・O・D を押さえれば、L と I は自然と守られるケースが多いです。

開放閉鎖原則のために最初から interface を用意すべきですか。

YAGNI の観点から、拡張の必要性が見えた時点で interface を抽出するのが現実的です。最初から用意すると未使用の抽象化が残りやすくなります。

依存性逆転原則はフレームワークなしでも実践できますか。

コンストラクタ引数で interface を受け取るだけで実現できます。DI コンテナは便利ですが、原則の理解と実践にフレームワークは必須ではありません。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます