設計力の基礎を鍛え直す!SOLID原則×リファクタリング完全ガイド【設計・アーキテクチャ】

  • LINEで送る

― 壊れにくく、読みやすく、進化に強い設計へ ―

 「このクラス、なんでこんなに複雑なんだろう?」「少しの修正のはずが、全体が壊れた……」 そんな経験、あなたにもあるのではないでしょうか。

 実務の現場では、コードを書く以上に“設計”が問われます。機能追加に強く、修正時にも安心できるコード。そんな理想に近づくには、原則と改善手法を理解することが欠かせません。

 この記事では、Javaエンジニアが設計力を鍛え直すための2本柱「SOLID原則」と「リファクタリング」に焦点を当てます。なぜそれが重要なのか、そしてどう活かすのか。設計に対する“迷い”を減らし、確信を持ってコードを書けるようになるヒントが詰まっています。

はじめに:なぜSOLIDとリファクタリングを学ぶのか?

 「デザインパターンを一通り学んだけれど、実務ではうまく設計できている気がしない」そんな悩みを感じたことはありませんか?それもそのはず。デザインパターンは便利な「道具」ですが、それをどう使いこなすかという「考え方の軸」がなければ、設計は破綻しがちです。

 その軸こそが、SOLID原則です。そして、コードが原則から外れてしまったとき、設計を改善していく技術がリファクタリングです。

 本講座では、設計を支える「5つの原則(SOLID)」と、それに基づくリファクタリングの具体例を通して、壊れにくく、拡張しやすいコード設計の思考法を学びます。

SOLIDとは?

 以下は、SOLIDの5原則を簡潔にまとめた一覧表です。設計上の意識すべきポイントをざっくり把握するのに役立ちます。

原則名称意味と目的
S単一責任原則(SRP)クラスは1つの責任(変更理由)だけを持つべき
O開放/閉鎖原則(OCP)拡張には開かれ、変更には閉じているべき
Lリスコフの置換原則(LSP)派生クラスは親クラスとして置き換えても問題ない設計に
Iインターフェース分離原則(ISP)必要のない機能に依存させない、小さなインターフェース設計
D依存性逆転の原則(DIP)実装ではなく抽象に依存させることで柔軟な構造を保つ

SRP:単一責任原則

 **SRP(Single Responsibility Principle)**とは、「クラスはたった1つの理由でしか変更されてはならない」という原則です。

❌ 悪い例 ❌

public class ReportManager {
    public String createReport() {
        return "レポート内容";
    }
    public void saveToFile(String content) {
        System.out.println("保存しました: " + content);
    }
    public void sendByEmail(String content) {
        System.out.println("送信しました: " + content);
    }
}

問題点: レポートの生成、保存、送信という3つの責任を1つのクラスに押し込んでおり、単一責任原則に反しています。変更理由が複数あるため、保守性が著しく低下します。

良い例

public class ReportCreator {
    public String createReport() {
        return "レポート内容";
    }
}

public class FileSaver {
    public void save(String content) {
        System.out.println("保存しました: " + content);
    }
}

public class EmailSender {
    public void send(String content) {
        System.out.println("送信しました: " + content);
    }
}

改善ポイント: 各クラスの責務を明確に分離することで、レポートの作成、保存、送信という機能が独立し、個別に変更・再利用がしやすくなりました。たとえば保存方式だけを変更したい場合でも、他の機能に一切影響を与えずに済みます。

OCP:開放/閉鎖原則

 **OCP(Open/Closed Principle)**とは、「ソフトウェアの構成要素は拡張に対して開かれていて、変更に対して閉じていなければならない」という原則です。

OCP:開放/閉鎖原則

❌ 悪い例 ❌

public class DiscountService {
    public double calculateDiscount(String memberType, double price) {
        if ("GOLD".equals(memberType)) {
            return price * 0.8;
        } else if ("SILVER".equals(memberType)) {
            return price * 0.9;
        }
        return price;
    }
}

問題点: 新しい会員ランクが追加されるたびにこのクラスの if 文を変更する必要があり、既存のコードを変更しなければならない設計になっています。この実装はOCPに反しています。

良い例

public interface DiscountPolicy {
    double apply(double price);
}

public class GoldDiscount implements DiscountPolicy {
    public double apply(double price) {
        return price * 0.8;
    }
}

public class SilverDiscount implements DiscountPolicy {
    public double apply(double price) {
        return price * 0.9;
    }
}

改善ポイント: 新しい割引ルールを追加する場合、既存の DiscountService を変更せず、新しいクラスを追加するだけで拡張可能になります。これにより、既存のコードのバグを生むリスクを避けることができます。

LSP:リスコフの置換原則

 **LSP(Liskov Substitution Principle)**とは、「派生クラスは基底クラスの代わりに使えるべき」という原則です。

LSP:リスコフの置換原則

❌ 悪い例 ❌

class Bird {
    void fly() {
        System.out.println("飛ぶ");
    }
}

class Ostrich extends Bird {
    void fly() {
        throw new UnsupportedOperationException("ダチョウは飛べません");
    }
}

問題点: サブクラスの OstrichBirdfly() を適切に置き換えられていないため、リスコフの置換原則に違反しています。継承関係が正しく設計されていない状態です。

良い例

interface Bird {}

interface FlyableBird extends Bird {
    void fly();
}

class Sparrow implements FlyableBird {
    public void fly() {
        System.out.println("飛ぶ");
    }
}

class Ostrich implements Bird {
    // fly()を実装しない
}

改善ポイント: 飛べる鳥と飛べない鳥をインターフェースレベルで分けることで、誤って Ostrichfly() を呼ぶ設計ミスを防げます。基底型で扱うコードが例外を気にせず使えるようになり、安全性と整合性が向上します。

ISP:インターフェース分離原則

 **ISP(Interface Segregation Principle)**とは、「クライアントは使わないメソッドへの依存を強制されるべきでない」という原則です。

ISP:インターフェース分離原則

❌ 悪い例 ❌

interface Machine {
    void print();
    void scan();
    void fax();
}

class SimplePrinter implements Machine {
    // プリンターにはプリント機能しかない
    public void print() {}
    public void scan() {
        throw new UnsupportedOperationException();
    }
    public void fax() {
        throw new UnsupportedOperationException();
    }
}

問題点: SimplePrinter に必要のない scan()fax() のメソッド実装を強制されており、使わない機能への依存が発生しています。インターフェース分離原則に反しています。

良い例

interface Printer {
    void print();
}

class SimplePrinter implements Printer {
    public void print() {
        System.out.println("印刷中");
    }
}

改善ポイント: 不要な機能(スキャンやFAX)を無理に実装しなくてよくなり、無駄な例外処理や空メソッドの実装を避けられます。クライアントに必要な機能だけを提供するシンプルな設計になります。

DIP:依存性逆転の原則

 **DIP(Dependency Inversion Principle)**とは、「高水準モジュールは低水準モジュールに依存すべきではない。どちらも抽象に依存すべき」という原則です。

DIP:依存性逆転の原則

❌ 悪い例 ❌

class EmailService {
    public void send(String message) {
        System.out.println("送信: " + message);
    }
}

class Notification {
    private EmailService emailService = new EmailService();

    public void alert(String message) {
        emailService.send(message);
    }
}

問題点: 高水準の Notification クラスが具体的な EmailService クラスに直接依存しており、柔軟性に欠けます。他の通知手段への切り替えが困難で、依存性逆転の原則に反しています。

良い例

interface MessageSender {
    void send(String message);
}

class EmailService implements MessageSender {
    public void send(String message) {
        System.out.println("送信: " + message);
    }
}

class Notification {
    private MessageSender sender;

    public Notification(MessageSender sender) {
        this.sender = sender;
    }

    public void alert(String message) {
        sender.send(message);
    }
}

改善ポイント: Notification は具体的な実装 EmailService に依存せず、MessageSender という抽象に依存しています。これにより、将来的にSMS送信やLINE通知など別の手段に切り替えても、通知クラスの変更が不要になります。

リファクタリングと設計原則の関係

 以下の表は、各SOLID原則の違反と、それに対してよく使われる代表的なリファクタリング手法・設計改善の例です。

原則違反の兆候・問題例改善アプローチ(リファクタリング)
SRPクラスに複数の責任が混在しているクラスを役割ごとに分割(Extract Class)
OCPif文やswitch文が増え続ける構造抽象化の導入・ポリモーフィズムの活用(Strategyパターンなど)
LSPサブクラスが親クラスの仕様を満たしていない継承の見直し、適切なインターフェース分割
ISP使わないメソッドの実装を強制されているインターフェースを小さく分ける(Interface Segregation)
DIP高水準モジュールが具象クラスに依存している抽象インターフェースを導入し、DI(依存性注入)を使う

まとめ

 SOLID原則は設計の“体幹”であり、リファクタリングはその“鍛え方”です。原則を意識してコードを書き、違反してしまった場合にはリファクタリングで修正する。このサイクルを回すことで、設計力は確実に伸びていきます。

今すぐ、あなたのコードにも“設計の体幹トレーニング”を取り入れてみませんか?

最新の投稿

SNSでもご購読できます。

コメントを残す