java-recipes

ホーム オブジェクト指向設計(OOP) › OOP-02

OOP-02: SOLID 原則の実例

SOLID 原則は、保守しやすく変更に強いオブジェクト指向設計のための5つのガイドラインです。 ここでは特に実装が明確な「S(単一責任)」「O(開放閉鎖)」「D(依存性逆転)」の3つを Java コードで解説します。

SOLID 原則とは

SOLID は以下の5原則の頭文字を取ったものです。コードが大きくなるほど、これらの原則に従うことで変更コストを抑えられます。

S — 単一責任原則(Single Responsibility Principle)

クラスが変更される理由は1つだけにします。複数の責任を1つのクラスに持たせると、1か所の変更が別の機能に影響を与えるリスクが高まります。

O — 開放閉鎖原則(Open/Closed Principle)

クラスは「拡張に対して開いており、変更に対して閉じている」べきです。新機能を追加するとき、既存のコードを変更せずに新しいクラスを追加するだけで対応できる設計を目指します。

D — 依存性逆転原則(Dependency Inversion Principle)

上位モジュールは下位モジュールに依存してはなりません。どちらも「抽象(インターフェース)」に依存するべきです。これにより、依存先の実装をテスト用のモックに差し替えることが容易になります。

サンプルコード

SolidPrinciplesSample.java
import java.util.ArrayList;
import java.util.List;

public class SolidPrinciplesSample {

    // === S: 単一責任原則 ===

    // ❌ 悪い例: Order クラスが注文データ + 永続化 + メール通知の責任を持つ
    static class BadOrder {
        private String product;
        private int quantity;

        public BadOrder(String product, int quantity) {
            this.product = product;
            this.quantity = quantity;
        }

        public void save() { // 永続化責任(本来は Repository の仕事)
            System.out.println("DB に保存: " + product);
        }

        public void sendEmail() { // 通知責任(本来は Notification の仕事)
            System.out.println("メール送信: " + product);
        }
    }

    // ✅ 良い例: 責任を分離
    static class Order {
        private final String product;
        private final int quantity;

        public Order(String product, int quantity) {
            this.product = product;
            this.quantity = quantity;
        }

        public String getProduct() { return product; }
        public int getQuantity() { return quantity; }
    }

    static class OrderRepository {
        public void save(Order order) {
            System.out.println("DB に保存: " + order.getProduct());
        }
    }

    static class OrderNotification {
        public void sendEmail(Order order) {
            System.out.println("メール送信: " + order.getProduct());
        }
    }

    // === O: 開放閉鎖原則 ===

    // ❌ 悪い例: 新しい割引タイプを追加するたびに if-else を修正
    static class BadDiscountCalculator {
        public double calculate(String type, double price) {
            if (type.equals("STUDENT")) {
                return price * 0.8;
            } else if (type.equals("MEMBER")) {
                return price * 0.9;
            }
            // 新タイプを追加するたびにこのクラスを変更 ❌
            return price;
        }
    }

    // ✅ 良い例: 拡張に開く(新タイプを追加しても既存コードを変更しない)
    interface DiscountStrategy {
        double apply(double price);
    }

    static class StudentDiscount implements DiscountStrategy {
        @Override
        public double apply(double price) { return price * 0.8; }
    }

    static class MemberDiscount implements DiscountStrategy {
        @Override
        public double apply(double price) { return price * 0.9; }
    }

    static class DiscountCalculator {
        public double calculate(DiscountStrategy strategy, double price) {
            return strategy.apply(price); // 既存コードを変更せず新タイプを追加可能
        }
    }

    // === D: 依存性逆転原則 ===

    // ❌ 悪い例: 具体クラスに依存
    static class BadOrderService {
        private final MySqlOrderRepository repo = new MySqlOrderRepository(); // 具体実装に依存

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

    static class MySqlOrderRepository {
        public void save(Order order) {
            System.out.println("MySQL に保存: " + order.getProduct());
        }
    }

    // ✅ 良い例: インターフェースに依存(テスト・実装の差し替えが容易)
    interface OrderRepositoryInterface {
        void save(Order order);
    }

    static class ProductionRepository implements OrderRepositoryInterface {
        @Override
        public void save(Order order) {
            System.out.println("本番DB に保存: " + order.getProduct());
        }
    }

    static class GoodOrderService {
        private final OrderRepositoryInterface repo; // インターフェースに依存 ✅

        public GoodOrderService(OrderRepositoryInterface repo) { // コンストラクタ注入
            this.repo = repo;
        }

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

    public static void main(String[] args) {
        System.out.println("=== S: 単一責任原則 ===");
        Order order = new Order("ノートPC", 1);
        new OrderRepository().save(order);
        new OrderNotification().sendEmail(order);

        System.out.println("\n=== O: 開放閉鎖原則 ===");
        DiscountCalculator calc = new DiscountCalculator();
        System.out.println("学生割引: " + calc.calculate(new StudentDiscount(), 10000));
        System.out.println("会員割引: " + calc.calculate(new MemberDiscount(), 10000));

        System.out.println("\n=== D: 依存性逆転原則 ===");
        GoodOrderService service = new GoodOrderService(new ProductionRepository());
        service.processOrder(order);
    }
}

Java 8 では Strategy パターンの実装にクラスを都度定義する必要があります。DiscountStrategy はインターフェースが1メソッドしか持たないため、Java 8 以降はラムダ式で簡潔に書けます。

よくあるミス・注意点

⚠️ God クラス(何でも一つのクラスにまとめる)

すべての処理を1つのクラスに書いてしまう「God クラス」は、単一責任原則の典型的な違反です。 最初は便利に見えても、クラスが大きくなると変更のたびに別の機能に影響が出やすくなります。 「注文データの管理」「DB 保存」「メール送信」のように責任を分離し、それぞれを独立したクラスに任せましょう。

⚠️ 具体クラスへの直接依存でテスト困難

BadOrderService のように、new MySqlOrderRepository() を直接フィールドに持つと、 テスト時に DB なしで動かすことができません。 インターフェースを介してコンストラクタで渡すコンストラクタ注入を使うと、テスト時にモックを渡せるようになります。 これが依存性逆転原則(D 原則)の実践的な恩恵です。

テストする観点

  • GoodOrderService にモックの Repository を注入して、実際の DB なしでテストできること(D 原則の恩恵)
  • 新しい割引タイプ(例: VIP 割引)を追加するとき、DiscountCalculator を変更しなくてよいこと(O 原則の確認)
  • OrderRepositoryOrderNotification が独立してテストできること(S 原則の確認)
  • 各割引率(学生 20% OFF、会員 10% OFF)が正しく計算されること(境界値: 価格 0 の場合も確認)

GitHub でソースコードを見る →