【プログラミングしてみよう63】JavaのSOLID原則入門

Java

前回はクリーンコードを書くためのベストプラクティスを学びました。

今回は「SOLID原則」について解説します。 SOLIDはオブジェクト指向設計の5つの原則の頭文字を並べたもので、変更に強く・テストしやすく・拡張しやすいコードを書くための指針です。

SOLID原則とは

SOLID原則はロバート・C・マーチンが提唱した5つのオブジェクト指向設計原則です。

S: Single Responsibility Principle  (単一責任の原則)
O: Open/Closed Principle            (開放閉鎖の原則)
L: Liskov Substitution Principle    (リスコフの置換原則)
I: Interface Segregation Principle  (インターフェース分離の原則)
D: Dependency Inversion Principle   (依存性逆転の原則)

それぞれをJavaのコードで解説します。

S: 単一責任の原則

「クラスが変更される理由は1つだけであるべき」という原則です。言い換えると、1つのクラスは1つの責任だけを持つべきということです。

ファイル作成

「Java51」フォルダに SolidExample.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

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

public class SolidExample {

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

    // 悪い例: 注文の管理・計算・出力をすべて1つのクラスで行う
    static class BadOrder {
        String product;
        int price;
        int quantity;

        void printReceipt() { // 出力の責任
            System.out.println(product + " x " + quantity + " = " + (price * quantity) + "円");
        }

        int calcTotal() { // 計算の責任
            return price * quantity;
        }

        void saveToFile() { // 保存の責任
            System.out.println("(ファイルに保存: " + product + ")");
        }
    }

    // 良い例: 責任を分割する
    record OrderData(String product, int price, int quantity) {
        int total() { return price * quantity; }
    }

    static class ReceiptPrinter {
        void print(OrderData order) {
            System.out.println(order.product() + " x " + order.quantity()
                    + " = " + order.total() + "円");
        }
    }

    static class OrderRepository {
        void save(OrderData order) {
            System.out.println("(保存: " + order.product() + ")");
        }
    }

    // ===== O: 開放閉鎖の原則 =====
    // 「拡張に対して開いており、修正に対して閉じている」
    // 新しい機能を追加するとき、既存のコードを変更しない

    // 悪い例: 新しい図形を追加するたびにAreaCalculatorを変更が必要
    static class BadAreaCalculator {
        double calculate(Object shape) {
            if (shape instanceof double[] circle) {
                return Math.PI * circle[0] * circle[0];
            } else if (shape instanceof int[] rect) {
                return rect[0] * rect[1];
            }
            return 0;
        }
    }

    // 良い例: Shapeインターフェースを実装するだけで拡張できる
    interface Shape {
        double area();
    }

    record Circle(double radius) implements Shape {
        public double area() { return Math.PI * radius * radius; }
    }

    record Rectangle(double width, double height) implements Shape {
        public double area() { return width * height; }
    }

    // 新しい図形を追加してもAreaCalculatorは変更不要
    record Triangle(double base, double height) implements Shape {
        public double area() { return base * height / 2; }
    }

    static class AreaCalculator {
        double calculate(Shape shape) { return shape.area(); }
    }

    // ===== L: リスコフの置換原則 =====
    // 「親クラスの代わりに子クラスを使っても正しく動くべき」

    // 悪い例: 正方形は長方形の一種だが、setWidthの動作が変わる
    static class BadRectangle {
        int width, height;
        void setWidth(int w)  { this.width = w; }
        void setHeight(int h) { this.height = h; }
        int area() { return width * height; }
    }

    static class BadSquare extends BadRectangle {
        @Override
        void setWidth(int w) { this.width = w; this.height = w; } // 辺を同じにする
        @Override
        void setHeight(int h) { this.width = h; this.height = h; }
    }

    // 良い例: 長方形と正方形をそれぞれ独立した型として設計する
    interface Polygon { double area(); }
    record GoodRectangle(double width, double height) implements Polygon {
        public double area() { return width * height; }
    }
    record GoodSquare(double side) implements Polygon {
        public double area() { return side * side; }
    }

    // ===== I: インターフェース分離の原則 =====
    // 「使わないメソッドへの依存を強制しない」

    // 悪い例: すべての動物に全メソッドを強制
    interface BadAnimal {
        void eat();
        void fly();   // 飛べない動物もいる
        void swim();  // 泳げない動物もいる
    }

    // 良い例: 能力ごとに小さなインターフェースに分ける
    interface Eatable { void eat(); }
    interface Flyable  { void fly(); }
    interface Swimmable { void swim(); }

    static class Dog implements Eatable, Swimmable {
        public void eat()  { System.out.println("犬が食べる"); }
        public void swim() { System.out.println("犬が泳ぐ"); }
    }

    static class Eagle implements Eatable, Flyable {
        public void eat() { System.out.println("鷹が食べる"); }
        public void fly() { System.out.println("鷹が飛ぶ"); }
    }

    // ===== D: 依存性逆転の原則 =====
    // 「具体的なクラスではなく抽象(インターフェース)に依存する」

    // 悪い例: 具体的なクラスに直接依存している
    static class MySQLDatabase {
        void save(String data) { System.out.println("MySQL保存: " + data); }
    }

    static class BadUserService {
        private MySQLDatabase db = new MySQLDatabase(); // 具体的なクラスに依存
        void saveUser(String name) { db.save(name); }
    }

    // 良い例: インターフェースに依存することで実装を差し替えられる
    interface Database {
        void save(String data);
    }

    static class MySQLDb implements Database {
        public void save(String data) { System.out.println("MySQL保存: " + data); }
    }

    static class InMemoryDb implements Database {
        public void save(String data) { System.out.println("メモリ保存: " + data); }
    }

    static class GoodUserService {
        private final Database db; // 抽象に依存

        GoodUserService(Database db) { this.db = db; } // 依存性注入

        void saveUser(String name) { db.save(name); }
    }

    public static void main(String[] args) {
        // S: 単一責任
        System.out.println("=== S: 単一責任 ===");
        OrderData order = new OrderData("コーヒー", 350, 3);
        new ReceiptPrinter().print(order);
        new OrderRepository().save(order);

        // O: 開放閉鎖
        System.out.println("\n=== O: 開放閉鎖 ===");
        AreaCalculator calc = new AreaCalculator();
        List<Shape> shapes = List.of(new Circle(5), new Rectangle(4, 6), new Triangle(3, 8));
        shapes.forEach(s -> System.out.printf("%s: %.2f%n", s.getClass().getSimpleName(), calc.calculate(s)));

        // L: リスコフの置換
        System.out.println("\n=== L: リスコフの置換 ===");
        List<Polygon> polygons = List.of(new GoodRectangle(4, 6), new GoodSquare(5));
        polygons.forEach(p -> System.out.println(p.getClass().getSimpleName() + ": " + p.area()));

        // I: インターフェース分離
        System.out.println("\n=== I: インターフェース分離 ===");
        new Dog().eat();
        new Dog().swim();
        new Eagle().fly();

        // D: 依存性逆転
        System.out.println("\n=== D: 依存性逆転 ===");
        GoodUserService mysqlService   = new GoodUserService(new MySQLDb());
        GoodUserService memoryService  = new GoodUserService(new InMemoryDb());
        mysqlService.saveUser("ぱんまる");
        memoryService.saveUser("たろう");
    }
}

コンパイル

javac SolidExample.java

実行

java SolidExample

実行結果は次のようになります。

=== S: 単一責任 ===
コーヒー x 3 = 1050円
(保存: コーヒー)

=== O: 開放閉鎖 ===
Circle: 78.54
Rectangle: 24.00
Triangle: 12.00

=== L: リスコフの置換 ===
GoodRectangle: 24.0
GoodSquare: 25.0

=== I: インターフェース分離 ===
犬が食べる
犬が泳ぐ
鷹が飛ぶ

=== D: 依存性逆転 ===
MySQL保存: ぱんまる
メモリ保存: たろう

コードの解説

S(単一責任): BadOrder は注文データの管理・計算・出力・保存という複数の責任を持っていました。OrderData・ReceiptPrinter・OrderRepository に分割することで、出力の形式を変えたいときは ReceiptPrinter だけ、保存先を変えたいときは OrderRepository だけを修正すればよくなります。

O(開放閉鎖): BadAreaCalculator は新しい図形を追加するたびに if 文を追加する必要があります。Shape インターフェースを使うことで、新しい Triangle クラスを追加しても AreaCalculator は変更不要になります。

L(リスコフの置換): BadSquare は BadRectangle を継承していますが、setWidth の動作が変わるため「長方形の代わりに正方形を使うと動作が変わる」問題が起きます。Polygon インターフェースを使って独立した型として設計することで解決します。

I(インターフェース分離): BadAnimal は飛べない犬にも fly() の実装を強制します。Eatable・Flyable・Swimmable に分けることで、各動物は必要な能力だけを実装できます。

D(依存性逆転): BadUserService は MySQLDatabase に直接依存しているため、PostgreSQLに切り替えるとコードの変更が必要です。Database インターフェースに依存しコンストラクタで注入することで、実装を差し替えるだけで対応できます。これを「依存性注入(DI)」と呼び、Springなどのフレームワークのコアコンセプトでもあります。

まとめ

SOLID原則はオブジェクト指向設計の5つの指針です。S(単一責任)でクラスの責任を1つに絞り、O(開放閉鎖)でコードを変更せず拡張できるようにし、L(リスコフの置換)で親クラスの代わりに子クラスを使っても正しく動くようにし、I(インターフェース分離)で不要なメソッドへの依存を排除し、D(依存性逆転)で具体的なクラスでなく抽象に依存します。すべてを一度に守ろうとする必要はありませんが、コードを書くたびに意識することで設計の質が着実に上がっていきます。

2026/08/11(火) ぱんまる

コメント

タイトルとURLをコピーしました