SOLID原則は、オブジェクト指向プログラミング(OOP)の設計における5つの重要な原則の頭文字を取ったものです。これらの原則は、ソフトウェアの保守性、拡張性、および再利用性を高めることを目的としています。
S - 単一責任の原則 (Single Responsibility Principle)
**「一つのクラスは一つの責任だけを持つべき」**という原則です。
ここでいう「責任」とは、変更の理由を意味します。
つまり、クラスに変更を加える理由はただ一つであるべきです。これにより、コードの凝集度が高まり、ある機能の変更が別の機能に予期せぬ影響を与えるリスクを減らすことができます。
例: あるクラスが「ユーザーデータの取得」と「レポートの出力」という2つの役割を持っていたとします。
レポートの仕様が変更された場合、そのクラスを修正する必要があります。
ユーザーデータの取得方法が変更された場合も、同じクラスを修正する必要があります。
このように、変更の理由が複数存在するため、単一責任の原則に反しています。これを解決するには、2つのクラスに分割します。
UserDataFetcher クラス:ユーザーデータの取得のみを担当
ReportGenerator クラス:レポートの出力のみを担当
O - 開放/閉鎖の原則 (Open/Closed Principle)
**「ソフトウェアの構成要素(クラス、モジュール、関数など)は、拡張に対して開かれており、変更に対して閉じているべき」**という原則です。
新しい機能を追加したい場合、既存のコードを修正するのではなく、新しいコードを追加することで対応すべきです。これにより、既存の安定したコードを壊すことなく、機能を拡張できます。通常、これはインターフェースや抽象クラス、ポリモーフィズムを利用して実現されます。
例: 決済システムを考えてみましょう。
原則に反する例: PaymentProcessor クラスが、クレジットカード決済、銀行振込など、決済方法ごとにif/else文で処理を分けている場合、新しい決済方法を追加するたびにこのクラスを修正する必要があります。
原則に沿った例: Payment というインターフェース(process() メソッドを持つ)を定義します。
CreditCardPayment クラスが Payment インターフェースを実装。
BankTransferPayment クラスが Payment インターフェースを実装。
新しい決済方法(例:CryptoPayment)を追加する場合、Payment インターフェースを実装した新しいクラスを追加するだけで済み、既存のPaymentProcessorクラスを修正する必要はありません。
L - リスコフの置換原則 (Liskov Substitution Principle)
**「派生クラスは、基本クラスの振る舞いを壊すことなく、基本クラスのインスタンスと置き換えることができなければならない」**という原則です。
これは、継承関係が正しく機能するためのルールです。子クラスは親クラスの機能を完全に満たしている必要があります。これにより、プログラムのどこかで親クラスのオブジェクトを扱っている場合でも、実際には子クラスのオブジェクトが渡されても問題なく動作することを保証します。
例:
原則に反する例: Rectangle クラスと、それを継承した Square クラスを考えます。正方形は長方形の一種ですが、Square の setWidth() や setHeight() メソッドは、幅と高さを常に同じ値に設定する必要があります。もしSquareが長方形としてsetWidth()を呼び出された場合、親クラスである長方形の振る舞い(高さが元のままになる)を壊してしまいます。
原則に沿った例: Shape という基本クラスを作成し、CircleやRectangleをその派生クラスとします。この場合、Shapeのインスタンスを扱う関数にCircleやRectangleのインスタンスを渡しても、期待通りに動作します。
I - インターフェース分離の原則 (Interface Segregation Principle)
**「クライアントが利用しないメソッドを強制的に実装させるべきではない」**という原則です。
大きな単一のインターフェースではなく、小さな役割に特化した複数のインターフェースを持つべきです。
これにより、クラスは必要なメソッドを持つインターフェースのみを実装すればよくなり、無関係なメソッドの定義を強制されなくなります。
例:
原則に反する例: Worker という大きなインターフェースがあり、work()、eat()、sleep()、drive() の4つのメソッドを定義しているとします。ロボットにはwork()とdrive()しか必要ないのに、eat()やsleep()の実装を強制されます。
原則に沿った例: Workable、Eatable、Drivableといった小さなインターフェースに分割します。
HumanWorker クラス:Workable、Eatable、Drivable を実装
RobotWorker クラス:Workable、Drivable を実装 これにより、各クラスは必要なインターフェースのみを実装すればよくなります。
D - 依存性逆転の原則 (Dependency Inversion Principle)
**「上位レベルのモジュールは、下位レベルのモジュールに依存してはならない。
どちらも抽象化に依存すべきである。
また、抽象化は実装の詳細に依存してはならない」**という原則です。
具体的な実装(下位レベル)に直接依存するのではなく、抽象化されたインターフェース(上位レベル)に依存すべきです。
これにより、モジュール間の結合度が低くなり、システムの柔軟性と再利用性が向上します。
例:
原則に反する例: ReportGenerator クラスが、MySQLDatabase という具体的なクラスに直接依存している場合。もしデータベースをPostgreSQLに変更したくなった場合、ReportGeneratorクラスのコードを修正する必要があります。
原則に沿った例: Database というインターフェースを定義します。
MySQLDatabase クラスが Database インターフェースを実装。
PostgreSQLDatabase クラスが Database インターフェースを実装。
ReportGenerator クラスは、具体的なMySQLDatabaseではなく、Database インターフェースに依存するように設計します。これにより、ReportGeneratorのコードを変更することなく、異なるデータベースを簡単に切り替えることができます。
補足
DI(依存性注入) この原則は、しばしば**依存性注入(Dependency Injection: DI)**というデザインパターンと組み合わせて実現されます。
クラス内で直接インスタンスを生成するのではなく、外部(コンストラクタやセッターメソッドなど)から依存するオブジェクトを渡すことで、依存性を逆転させます。