💻

SOLID原則について

2025/02/18に公開

はじめに

ソフトウェア開発におけるSOLID原則について、解説いたします。
SOLID原則とは、オブジェクト指向において従うべき基本原則となります。

本記事の対象

  • これから開発をしていく方
  • SOLID原則を意識してこなかった方

SOLID原則とは?

SOLID原則は、ソフトウェア開発におけるオブジェクト指向設計の基本原則であり、保守性や拡張性の高いコードを書くために役立ちます。SOLIDは以下の5つの原則の頭文字を取ったものです。

  1. Single Responsibility Principle(単一責任の原則)
  2. Open/Closed Principle(開放/閉鎖の原則)
  3. Liskov Substitution Principle(リスコフの置換原則)
  4. Interface Segregation Principle(インターフェース分離の原則)
  5. Dependency Inversion Principle(依存関係逆転の原則)

以下、それぞれの原則についてコード例を交えて解説します。

1. 単一責任の原則 (Single Responsibility Principle)

概要

クラスは単一の責任を持ち、その責任を全うするために存在すべきです。つまり、クラスは一つの変更理由だけを持つべきという考え方です。

詳細

単一責任の原則により、クラスの役割が明確になり、理解しやすく、変更に強い設計が可能になります。複数の責任を持つクラスは、変更が必要な際に予期しない影響を受けやすくなります。

コードサンプル

悪い例:
Userクラスがユーザーの情報管理とメール送信という異なる責任を持っているため、責任が分散しています。

public class User
{
    public void GetUserDetails()
    {
        // ユーザー詳細の取得
    }

    public void SaveUser()
    {
        // ユーザーの保存
    }

    public void SendEmail()
    {
        // メール送信
    }
}

良い例:
Userクラスがユーザー情報の管理のみを担当し、EmailServiceクラスがメール送信を担当することで、責任が明確に分割されています。

public class User
{
    public void GetUserDetails()
    {
        // ユーザー詳細の取得
    }

    public void SaveUser()
    {
        // ユーザーの保存
    }
}

public class EmailService
{
    public void SendEmail()
    {
        // メール送信
    }
}

2. 開放/閉鎖の原則 (Open/Closed Principle)

概要

オブジェクトは拡張に対して開かれており、修正に対して閉じられているべきという考え方です。

詳細

新しい機能を追加する際に既存のコードを変更せずに拡張できるように設計することを目的としています。抽象化を利用して拡張性を提供することが一般的です。

コードサンプル

悪い例:
AreaCalculatorクラスが異なる図形の種類に応じて面積を計算するため、図形が増えるたびにクラスを修正する必要があります。

public class Shape
{
    public string Type { get; set; }
}

public class AreaCalculator
{
    public double CalculateArea(Shape shape)
    {
        if (shape.Type == "Circle")
        {
            // 円の面積計算
        }
        else if (shape.Type == "Rectangle")
        {
            // 長方形の面積計算
        }
        // 新しい図形が追加されるたびにこのメソッドを修正する必要がある
    }
}

良い例:
Shapeインターフェースを実装する各図形クラスが自身の面積計算を担当し、AreaCalculatorは新しい図形クラスの追加に影響されません。

// インターフェースを定義
public interface IShape
{
    double CalculateArea();
}

// インターフェースを実装
public class Circle : IShape
{
    public double Radius { get; set; }

    public double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

// Circleクラスと同一のインターフェースを実装することで拡張
public class Rectangle : IShape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public double CalculateArea()
    {
        return Width * Height;
    }
}

public class AreaCalculator
{
    public double CalculateArea(IShape shape)
    {
        return shape.CalculateArea();
    }
}

3. リスコフの置換原則 (Liskov Substitution Principle)

概要

派生クラスは、基底クラスと置換可能でなければなりません。つまり、基底クラスのオブジェクトが期待される場所に派生クラスのオブジェクトを置いても、正しく動作する必要があるという考え方です。

詳細

この原則を守ることで、継承関係における予測可能な振る舞いが保証され、ポリモーフィズムが正しく機能します。

コードサンプル

悪い例:
Birdクラスを継承したOstrich(ダチョウ)クラスが飛ぶ機能を実装できないため、基底クラスとの置換が不可能になります。

public class Bird
{
    public virtual void Fly()
    {
        // 飛ぶ処理
    }
}

public class Ostrich : Bird
{
    public override void Fly()
    {
        throw new NotSupportedException("ダチョウは飛べません");
    }
}

良い例:
飛ぶ機能をIFlyableインターフェースとして分離し、飛ぶ能力のある鳥のみがそのインターフェースを実装します。これにより、Birdクラス自体は飛ぶ機能を持たず、飛べる鳥と飛べない鳥を明確に区別できます。これによって、基底クラスBirdを使用するコードは、派生クラスが飛べるかどうかを意識する必要がなくなり、置換可能性が保たれます。

// 基底クラスとしてのBird
public abstract class Bird
{
    // 鳥に共通するプロパティやメソッドを定義
    public abstract void Eat();
}

// 飛ぶ能力を定義するインターフェース
public interface IFlyable
{
    void Fly();
}

// 飛ぶ能力を持つスズメクラス
public class Sparrow : Bird, IFlyable
{
    public override void Eat()
    {
        // スズメの食事処理
    }

    public void Fly()
    {
        // 飛ぶ処理
    }
}

// 飛ぶ能力を持たないダチョウクラス
public class Ostrich : Bird
{
    public override void Eat()
    {
        // ダチョウの食事処理
    }

    // Flyメソッドは実装しない
}

4. インターフェース分離の原則 (Interface Segregation Principle)

概要

実装クラスは、継承元の使用しないメソッドへの依存を強制されてはならない。大きなインターフェースよりも、特定の機能に特化した小さなインターフェースに分割すべきという考え方です。

詳細

インターフェースを分割することで、クラスは本当に必要な機能のみを実装することができ、不要なメソッドの実装を避けることができます。これにより、クラスの結合度が低くなり、変更に強い設計が可能になります。

コードサンプル

悪い例:
Robotは食事をしないため、Eatメソッドの実装が不要です。

public interface IWorker
{
    void Work();
    void Eat();
}

public class Robot : IWorker
{
    public void Work()
    {
        // 作業を行う
    }

    public void Eat()
    {
        // ロボットは食事しないが、メソッドを実装する必要がある
    }
}

良い例:
インターフェースを分割することで、RobotIWorkableのみを実装し、必要なメソッドだけを持つことができます。

public interface IWorkable
{
    void Work();
}

public interface IEatable
{
    void Eat();
}

public class Human : IWorkable, IEatable
{
    public void Work()
    {
        // 作業を行う
    }

    public void Eat()
    {
        // 食事をする
    }
}

public class Robot : IWorkable
{
    public void Work()
    {
        // 作業を行う
    }
}

5. 依存関係逆転の原則 (Dependency Inversion Principle)

概要

高水準モジュールは低水準モジュールに依存してはならず、両者は抽象に依存すべきです。また、抽象は詳細に依存してはならず、詳細が抽象に依存すべきという考え方です。

詳細

依存関係逆転の原則により、システムの柔軟性と再利用性が向上します。具体的な実装に依存せず、抽象的なインターフェースに依存することで、モジュール間の結合度を低減します。
また、モックを用いることでテストコードの作成が容易になり、再利用性が高まります。

コードサンプル

悪い例:
UserRepositoryクラスが具体的なMySQLConnectionに依存しているため、データベースを変更する際にUserRepositoryを修正する必要があります。

public class MySQLConnection
{
    // MySQL接続の実装
}

public class UserRepository
{
    private MySQLConnection _connection;

    public UserRepository()
    {
        _connection = new MySQLConnection();
    }

    // ユーザー操作のメソッド
}

良い例:
DBConnectionインターフェースに依存し、具体的な接続実装は外部から注入することで、データベースの変更(例えば、MySQLからOracleへの移行)に伴うクラスの修正を不要にします。

public interface IDBConnection
{
    void Connect();
    // その他の接続メソッド
}

public class MySQLConnection : IDBConnection
{
    public void Connect()
    {
        // MySQL接続の実装
    }
}

public class OracleConnection : IDBConnection
{
    public void Connect()
    {
        // Oracle接続の実装
    }
}

public class UserRepository
{
    private readonly IDBConnection _connection;

    // 対象データベースがMySQLなのか、Oracleなのかは依存性注入時に変更可能
    public UserRepository(IDBConnection connection)
    {
        _connection = connection;
    }

    // ユーザー操作のメソッド
}

まとめ

  • SOLID原則は、オブジェクト指向設計における重要なガイドラインであり、保守性や拡張性の高い柔軟なソフトウェアを構築することを目的とした原則です。
  • 各原則は相互に関連し合っており、総合的に適用することでより良い設計が実現可能かと思います。
  • ただし、原則であり必ず厳守する必要はなく、適材適所で適用することが重要だと考えています。

参考文献

Discussion