🥰

依存性注入と依存性逆転の原則とは?疎結合で柔軟な設計を実現するための基礎知識

2024/11/12に公開

イントロダクション

ソフトウェア開発において、「依存性」 は重要な設計ポイントです。特に、依存性注入(Dependency Injection, DI)と依存性逆転の原則(Dependency Inversion Principle, DIP)は、疎結合でテストがしやすく、柔軟性の高いシステムを作るために欠かせない技術です。本記事では、この2つの概念について、具体的なコード例を交えて解説し、その効果を確認します。


依存性とは?

依存性とは、あるコンポーネントが別のコンポーネントに依存する関係を指します。例えば、AクラスがBクラスのメソッドを呼び出す場合、AクラスはBクラスに依存しているといえます。

この依存関係は、システム全体の柔軟性や保守性を損なう原因になりかねません。例えば、Bクラスを変更した際にAクラスも変更が必要になると、その影響がシステム全体に波及してしまう可能性があります。

問題のあるコード例

以下のコードでは、RobotクラスがArmクラスに直接依存しており、柔軟性が制限されています。

class Arm:
    def move(self):
        print("Arm is moving")

    def stop(self):
        print("Arm is stopped")


class Robot:
    def __init__(self):
        self.arm = Arm()

    def move(self):
        self.arm.move()

    def stop(self):
        self.arm.stop()


if __name__ == "__main__":
    robot = Robot()
    robot.move()
    robot.stop()

このコードにはどんな問題があるでしょうか?

依存関係が固定されている問題点

例えば、あなたがある会社に勤務していると想像してみてください。
上司から「今まで単一のArmを使用していたが、来月からは5本指を持ったArmと、物を掴むためだけに特化したArmの2つが必要になった」と依頼されました。
このように仕様が変わるたびに、Robotクラス自体を修正する必要があります。
以下のようにコードが肥大化し、複雑で保守しづらくなってしまいます。

class HumanLikeArm:
    def move(self):
        print("HumanLikeArm is moving")

    def stop(self):
        print("HumanLikeArm is stopped")


class ArtificialArm:
    def move(self):
        print("ArtificialArm is moving")

    def stop(self):
        print("ArtificialArm is stopped")


class Robot:
    def __init__(self):
        self.artificial_arm = ArtificialArm()
        self.human_like_arm = HumanLikeArm()

    def move(self, is_human_like: bool = False):
        if is_human_like:
            self.human_like_arm.move()
        else:
            self.artificial_arm.move()

    def stop(self, is_human_like: bool = False):
        if is_human_like:
            self.human_like_arm.stop()
        else:
            self.artificial_arm.stop()


if __name__ == "__main__":
    robot = Robot()
    robot.move(True)
    robot.stop(True)

さらに、Leg(足)やBody(体)などが追加されるたびに、Robotクラスはどんどん複雑になり、保守性が低下します。

解決方法:依存性注入(Dependency Injection)

依存性注入とは、クラスが依存するオブジェクトを外部から注入することで、依存関係を外部に移譲する手法です。これにより、以下のようなメリットが得られます。

  • 疎結合: クラス間の依存関係が緩和される
  • テストしやすさ: テスト時にモックを注入することで、特定の依存関係に縛られずテスト可能になる
  • 再利用性と柔軟性: クラスがさまざまな状況で使用しやすくなる

以下は、依存性注入を使用した改修例です。

class HumanLikeArm:
    def move(self):
        print("HumanLikeArm is moving")

    def stop(self):
        print("HumanLikeArm is stopped")


class ArtificialArm:
    def move(self):
        print("ArtificialArm is moving")

    def stop(self):
        print("ArtificialArm is stopped")


class Robot:
    def __init__(self, arm):
        self.arm = arm

    def move(self):
        self.arm.move()

    def stop(self):
        self.arm.stop()


if __name__ == "__main__":
    arm = ArtificialArm()
    # arm = HumanLikeArm()

    robot = Robot(arm=arm)
    robot.move()
    robot.stop()

依存性注入の効果

新しい実装では、Robotクラスは特定のArmの実装に直接依存せず、外部から渡されるarmオブジェクトに依存しています。これにより、以下の改善が得られます。

  • 疎結合: Robotクラスは具体的なArmの種類に依存しないため、Armの種類が追加されてもRobotクラスの修正が不要
  • テストしやすさ: モックを使用したテストが可能
  • 再利用性と柔軟性: さまざまな種類のArmを注入でき、異なる状況に応じた動作が可能

依存性逆転の原則(Dependency Inversion Principle, DIP)

依存性逆転の原則(DIP)は、SOLID原則の一部で、モジュール間の依存関係を制御するルールです。DIPは「高レベルモジュールは低レベルモジュールに依存してはならない。両者は抽象に依存すべきである」というルールです。

具体的な実装ではなくインターフェース(抽象)を介して依存関係を管理することで、実装変更の影響を最小限に抑え、柔軟で拡張性の高い設計が可能になります。

DIPを適用した改修例

以下は、DIPを適用したコードです。

from abc import ABC, abstractmethod


class IArm(ABC):
    @abstractmethod
    def move(self):
        pass

    @abstractmethod
    def stop(self):
        pass


class HumanLikeArm(IArm):
    def move(self):
        print("HumanLikeArm is moving")

    def stop(self):
        print("HumanLikeArm is stopped")


class ArtificialArm(IArm):
    def move(self):
        print("ArtificialArm is moving")

    def stop(self):
        print("ArtificialArm is stopped")


class Robot:
    def __init__(self, arm: IArm):
        self.arm = arm

    def move(self):
        self.arm.move()

    def stop(self):
        self.arm.stop()


if __name__ == "__main__":
    arm = ArtificialArm()
    # arm = HumanLikeArm()

    robot = Robot(arm=arm)
    robot.move()
    robot.stop()

依存性の逆転のメリット

この設計により、Robotクラスは具体的なHumanLikeArmArtificialArmの実装に依存するのではなく、抽象であるIArmインターフェースに依存しています。これにより、以下のメリットが得られます。

  • 柔軟性の向上: 新たなIArmの実装(例えば、FlexibleArmHeavyDutyArm)を追加するだけで、Robotクラスを変更せずに新しい動作を提供可能
  • 変更への強さ: 低レベルの実装を変更しても、直接依存していないため影響が最小限
  • テストの容易性: モック(例:MockArm)を使用してIArmを差し替えることでテストしやすくなる

DIとDIPの関係

DIとDIPは、疎結合でテストしやすく柔軟性の高いコードを実現するための重要な手法ですが、それぞれ異なる役割を持っています。DIは依存オブジェクトを外部から注入する手段であり、DIPは依存関係の設計原則です。両者を組み合わせることで、より保守性の高い設計が可能です。

結論

依存性注入と依存性逆転の原則は、柔軟で保守しやすいシステム設計のための強力なツールです。DIによりオブジェクトの管理が簡単になり、DIPによって依存関係が安定します。これらの設計手法を駆使し、プロジェクトの保守性と拡張性を高め、エンジニアの生産性向上を目指しましょう。

Discussion