趣味のPython・深層学習

中級者のための実装集

Pythonで学ぶSOLID原則

PythonでSOLID原則を学ぶ

SOLID 原則は、ロバート・C・マーティンによって提唱されたオブジェクト指向プログラミングの5つの原則です。この原則に従うことで、コードの柔軟性、保守性、拡張性が向上します。Pythonでもこの原則を適用することができます。

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

クラスは1つの責任しか持ってはならない つまり、クラスは1つのことしか行ってはいけません。複数の責任を持つクラスは、変更の影響範囲が広がり、保守性が低下します。

class Employee:
    def __init__(self, name: str, email: str, salary: float) -> None:
        self.name = name
        self.email = email
        self.salary = salary

    def work(self) -> str:
        return f"{self.name} is working..."

    def send_email(self, message: str) -> None:
        # メール送信処理
        print(f"Sent '{message}' to {self.email}")

上記の Employee クラスは、従業員の情報を保持し、作業を行う機能と、メールを送信する機能を持っています。このクラスは2つの責任を持っているため、単一責任の原則に違反しています。この問題を解決するには、次のように責任を分離します。

class Employee:
    def __init__(self, name: str, email: str, salary: float) -> None:
        self.name = name
        self.email = email
        self.salary = salary

    def work(self) -> str:
        return f"{self.name} is working..."

class EmailSender:
    def send_email(self, email: str, message: str) -> None:
        # メール送信処理
        print(f"Sent '{message}' to {email}")

これで Employee クラスは従業員情報と作業機能のみ、EmailSender クラスはメール送信機能のみを担当するようになり、単一責任の原則を満たすようになりました。

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

クラスは拡張に対して開かれており、変更に対しては閉じられているべきである つまり、既存のコードを変更することなく、新しい機能を追加できるようにするべきです。これにより、既存の機能を壊すリスクを最小限に抑えられます。

class Bird:
    def fly(self) -> None:
        print("Bird is flying")

class Penguin:
    def swim(self) -> None:
        print("Penguin is swimming")

上記の例では、Penguin クラスに fly メソッドを追加できません。なぜなら、ペンギンは飛べないからです。この問題を解決するには、継承を使います。

class Bird:
    def move(self) -> None:
        print("Bird is flying")

class FlyingBird(Bird):
    pass

class SwimmingBird(Bird):
    def move(self) -> None:
        print("Bird is swimming")

class Penguin(SwimmingBird):
    pass

これで、新しい FlyingBird や SwimmingBird を作ることで、機能を拡張できます。既存のコードを変更せずに済みます。

Liskovの置換の原則 (Liskov Substitution Principle)

サブタイプはその基本タイプで置換可能でなければならない つまり、サブクラスはスーパークラスの振る舞いを変更してはいけません。

class Bird:
    def fly(self) -> None:
        print("Bird is flying")

class Penguin(Bird):
    def fly(self) -> None:
        print("Penguin cannot fly")

上記の例では、 Penguin クラスが Bird クラスの fly メソッドを上書きしています。しかし、ペンギンは飛べないため、この実装は誤りです。この問題を解決するには、継承を避け、代わりにコンポジションを使います。

class FlyingBehavior:
    def fly(self) -> None:
        print("Flying")

class SwimmingBehavior:
    def swim(self) -> None:
        print("Swimming")

class Bird:
    def __init__(self, behavior: FlyingBehavior) -> None:
        self.behavior = behavior

    def move(self) -> None:
        self.behavior.fly()

class Penguin:
    def __init__(self, behavior: SwimmingBehavior) -> None:
        self.behavior = behavior

    def move(self) -> None:
        self.behavior.swim()

これで、Bird クラスと Penguin クラスは、それぞれ異なる振る舞いを持つことができます。

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

クライアントが使用しないメソッドに依存してはならない つまり、インターフェイスは特定のクライアントが必要としない機能には依存してはいけません。

from typing import Protocol

class WorkBehavior(Protocol):
    def work(self) -> None:
        ...

class EatBehavior(Protocol):
    def eat(self) -> None:
        ...

class SleepBehavior(Protocol):
    def sleep(self) -> None:
        ...

class Employee:
    def __init__(self, work_behavior: WorkBehavior, eat_behavior: EatBehavior, sleep_behavior: SleepBehavior) -> None:
        self.work_behavior = work_behavior
        self.eat_behavior = eat_behavior
        self.sleep_behavior = sleep_behavior

    def work(self) -> None:
        self.work_behavior.work()

    def eat(self) -> None:
        self.eat_behavior.eat()

    def sleep(self) -> None:
        self.sleep_behavior.sleep()

上記の例では、Employee クラスは WorkBehavior、EatBehavior、SleepBehavior の3つのインターフェイスに依存しています。しかし、クライアントによっては EatBehavior や SleepBehavior が不要な場合があります。この問題を解決するには、インターフェイスを分離します。

from typing import Protocol

class WorkBehavior(Protocol):
    def work(self) -> None:
        ...

class LivingBehavior(Protocol):
    def eat(self) -> None:
        ...

    def sleep(self) -> None:
        ...

class Employee:
    def __init__(self, work_behavior: WorkBehavior) -> None:
        self.work_behavior = work_behavior

    def work(self) -> None:
        self.work_behavior.work()

class HumanEmployee(Employee):
    def __init__(self, work_behavior: WorkBehavior, living_behavior: LivingBehavior) -> None:
        super().__init__(work_behavior)
        self.living_behavior = living_behavior

    def eat(self) -> None:
        self.living_behavior.eat()

    def sleep(self) -> None:
        self.living

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

上位のモジュールは下位のモジュールに依存してはならない。双方とも抽象に依存すべきである。 つまり、上位のクラス(高レベルのモジュール)が下位のクラス(低レベルのモジュール)に直接依存するのではなく、抽象(インターフェース)に依存するようにすべきです。

class Database:
    def get_data(self) -> str:
        # データベースからデータを取得する処理
        return "data from database"

class Application:
    def __init__(self, database: Database) -> None:
        self.database = database

    def get_data(self) -> str:
        return self.database.get_data()

上記の例では、Application クラスが Database クラスに直接依存しています。この依存関係を逆転させるには、抽象を導入します。

from abc import ABC, abstractmethod

class DatabaseInterface(ABC):
    @abstractmethod
    def get_data(self) -> str:
        pass

class Database(DatabaseInterface):
    def get_data(self) -> str:
        # データベースからデータを取得する処理
        return "data from database"

class Application:
    def __init__(self, database: DatabaseInterface) -> None:
        self.database = database

    def get_data(self) -> str:
        return self.database.get_data()

これで、Application クラスは DatabaseInterface に依存するようになりました。この設計により、Database の実装を変更しても Application クラスに影響を与えずに済みます。 SOLID 原則を適用することで、コードの保守性、拡張性、柔軟性が向上します。Pythonでも同様に適用が可能なため、大規模なプロジェクトでは必須の原則と言えます。適切に設計することで、質の高いコードを書くことができるでしょう。