ここ最近、SOLID原則の「単一責任の原則」と「オープン・クローズドの原則」に関する検討を行っていて、オープン・クローズドの原則と単一責任の原則の関係性に考えていました。
具体的には、オープン・クローズドの原則に従った設計を行うと、自然と単一責任の原則に従うのではないかということです。必ずしも正しいとはいえませんが、多くの場合に成り立つのではないかと思います。
単一責任の原則とは
単一責任の原則(Single Responsibility Principle)は、オブジェクト指向プログラミングにおける設計原則の一つであり、「モジュール、クラス、関数などは、単一の機能についてのみ責任を持ち、その機能をカプセル化するべきである」というものです。
単一責任の原則のメリット
単一責任の原則を適用することで得られるメリットには以下のようなものがあります。
- 理解しやすさの向上: 責務が明確に分かれているため、コードの理解が容易になり、保守性も向上します。
- 変更の容易化: 特定の機能を変更する際、影響を受ける範囲が限定されるため、変更に伴うリスクが低減されます。
- 再利用性の向上: 単一の機能に特化したモジュールは、他の箇所でも容易に再利用することができます。
- テストの容易化: 単一の機能に特化したモジュールは、個別にテストしやすくなります。
単一責任の原則を満たすための手法
単一責任の原則を満たすための手法として以下のような手法が挙げられます。
- クラスごとに単一の責務を割り当てる: あるクラスは、特定の機能のみを担当するように設計します。
- メソッドを小さくする: メソッドは、単一のタスクを実行するように設計します。
- インターフェースを使用する: 共通の機能をインターフェースで定義し、それを複数のクラスで実装します。
オープン・クローズドの原則とは
オープン・クローズドの原則(Open/Closed Principle)は、オブジェクト指向プログラミングにおける設計原則の一つであり、「ソフトウェアは、既存のコードを変更することなく、機能拡張できるようにすべきである」というものです。
オープン・クローズドの原則のメリット
オープン・クローズドの原則を適用することで得られるメリットには以下のようなものがあります。
- 柔軟性の向上: 新機能の追加や変更を、既存のコードに手を加えることなく行うことができます。
- 保守性の向上: 既存のコードを触らずに変更できるため、コードの保守性が向上します。
- 再利用性の向上: 既存のコードを拡張することで、新しい機能を追加することができます。
- テストの容易化: 既存のコードを変更しないため、テストが容易になります。
オープン・クローズドの原則を満たすための手法
オープン・クローズドの原則を満たすための手法として以下のような手法が挙げられます。
- 抽象化を利用する: 共通の機能を抽象化し、それを継承したクラスで具体的な処理を実装します。
- 依存関係を明確にする: クラス間の依存関係を明確にし、できるだけ疎結合な設計を目指します。
- ポリモーフィズムを利用する: 異なるクラスで同じインターフェースを実装することで、柔軟な振る舞いを可能にします。
オープン・クローズドの原則に従って設計の例を確認する
ここからが本題です。
オープン・クローズドの原則に従うと、自然と単一責任の原則に従うことになるのではないかと考えました。もちろん、「オープン・クローズドの原則に従っている=単一責任の原則」が必ずしも成り立つわけではありませんが、多くのケースにおいては成り立つのではないかと思います。
拡張に対して開いていて、修正に対して閉じている
オープン・クローズドの原則には、「拡張に対して開いていて、修正に対して閉じている」という考え方があります。「拡張に対して開いている」とは、既存のコードを変更することなく、新しい機能を追加したり、既存の機能を拡張したりできることを意味します。一方、「修正に対して閉じている」とは、既存のコードを変更することなく、新しい機能を追加したり、既存の機能を拡張したりできることを意味します。
新しい機能を既存コードの変更をせずに追加する方法として、継承があります。抽象クラスやインターフェースを使用し、個別の実装をサブクラスで実現することで、新しい機能の追加を新しいクラスの作成で実現することができます。
また、修正に対して閉じているためには、各機能が別々のクラスに分かれているだけでなく、ひとつのメソッドに複数の処理が含まれていないようにすることが重要です。他の処理が含まれているとコードを修正した際に、修正対象ではない処理にも影響を与えている可能性があります。
この点について、具体的なコードで見ていきましょう。
オープン・クローズドの原則に従っていない設計例
例えば、図形クラスを考えてみます。
このクラスは様々な図形の面積を計算することができます。
class Shape:
def __init__(self, type: str):
self.type = type
def get_area(self) -> float:
if self.type == "circle":
return 3.1415 * self.radius * self.radius
elif self.type == "rectangle":
return self.width * self.height
elif self.type == "triangle":
return 0.5 * self.base * self.height
else:
raise ValueError(f"Unknown shape type: {self.type}")
この図形クラスでは、新しい図形を扱えるようにするためにクラス自身に手を加えなくてはなりません。上記のコードにおいては、新しい図形を扱えるようにするにはif
文に条件を追加する必要があります。そのため、新しい図形を追加する度にコードの複雑度が増し、同じメソッドに修正を加えるため、デグレのリスクがつきまといます。
オープン・クローズドの原則に従っている設計例
オープン・クローズドの原則の原則に従うようにコードを修正してみましょう。
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def get_area(self) -> float:
pass
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def get_area(self) -> float:
return 3.1415 * self.radius * self.radius
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def get_area(self) -> float:
return self.width * self.height
class Triangle(Shape):
def __init__(self, base: float, height: float):
self.base = base
self.height = height
def get_area(self) -> float:
return 0.5 * self.base * self.height
抽象クラスShape
でインターフェースを定義し、具体的な図形はこれを継承する形で定義するようにします。どの具象クラスをインスタンス化するかによってget_area
メソッドの動作が変わります。新しい図形に対応する時には既存コードに手を加えずに新しいクラスを作成することで実現します。新たな図形の追加に対して既存の図形に対して影響を及ぼしません。
拡張(新しい図形の追加)に対して開いており(既存コードに修正を加えずに機能を追加できる)、修正(ある図形の計算方法が誤っている問題を修正)に対して閉じている(他の機能に影響を与えずに修正できる)ことがわかります。
各図形クラスは単一の図形に特化しており、get_area
メソッドは面積を返すという単一の機能を実現しているため、単一責任の原則に従っているといえます。
まとめ
成り立たないこともありますが、オープン・クローズドの原則に従うことは、単一責任の原則に従うことにもなります。これは、それぞれの原則は完全に独立しているのではなく、互いに関連していることを意味しています。一方の原則に従うことは他方の原則に従うことを意味することもあれば、トレードオフの関係にある場合もあります。必ずしも原則に従うことが正しいわけではなく、要求や仕様に応じてバランスをとることが重要です。