Python | テスト・設計・品質:interface 分離

Python Python
スポンサーリンク

interface分離って何?一言でいうと「いらないボタンを押させないようにする設計」

interface 分離(インターフェース分離の原則 / Interface Segregation Principle, ISP)は、

クライアント(使う側)に、使わないメソッドへの依存を強制してはいけない

という設計ルールです。

もっと噛み砕くと、

「このクラスを使う人に、“使わないボタン”を見せるな」
「このクラスを実装する人に、“使わないメソッド”を実装させるな」

という考え方です。

Python では「抽象クラス(ABC)や Protocol を小さく分ける」ことで、この考え方を実現していきます。


悪い例から入る:何でも詰め込んだインターフェース

「乗り物」インターフェースが欲張りすぎている例

まずは、あえて「ダメな設計」を見てみます。

from abc import ABC, abstractmethod

class Vehicle(ABC):
    @abstractmethod
    def go(self) -> None:
        ...

    @abstractmethod
    def fly(self) -> None:
        ...
Python

Vehicle という抽象クラスに、gofly の2つのメソッドがあります。
ここから、飛行機と車を作ってみます。

class Aircraft(Vehicle):
    def go(self) -> None:
        print("Taxiing")

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


class Car(Vehicle):
    def go(self) -> None:
        print("Going")

    def fly(self) -> None:
        raise Exception("The car cannot fly")
Python

Car は飛べないのに、fly を「一応」実装しないといけません。
ここがすでに不自然です。

使う側から見ると、もっと問題がはっきりします。

def move_vehicle(v: Vehicle) -> None:
    v.go()
    v.fly()  # 車に対しても呼べてしまう
Python

Vehicle を受け取る関数は、「fly が必ずある」と思ってしまいます。
でも、実際には Car.fly は例外を投げるだけです。

これはまさに、

「クライアントに、使えないメソッドを見せてしまっている」
「実装側に、使わないメソッドの実装を強制している」

状態です。
これが interface 分離が言う「やっちゃダメなやつ」です。


interface分離の原則:小さく分けて「必要なものだけ」実装・依存させる

「飛ぶ」と「走る」を別インターフェースに分ける

さっきの例を、interface 分離の考え方で直してみます。

from abc import ABC, abstractmethod

class Drivable(ABC):
    @abstractmethod
    def go(self) -> None:
        ...


class Flyable(ABC):
    @abstractmethod
    def fly(self) -> None:
        ...
Python

「走る」と「飛ぶ」を別々のインターフェースに分けました。

それぞれのクラスは、「必要な能力だけ」を実装します。

class Car(Drivable):
    def go(self) -> None:
        print("Going")


class Aircraft(Drivable, Flyable):
    def go(self) -> None:
        print("Taxiing")

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

車は Drivable だけ
飛行機は DrivableFlyable の両方

という形です。

使う側も、「何が欲しいか」をはっきり宣言できます。

def drive(d: Drivable) -> None:
    d.go()

def fly(f: Flyable) -> None:
    f.fly()
Python

drive は「走れるもの」だけを受け取る
fly は「飛べるもの」だけを受け取る

これが interface 分離の原則が目指している世界です。


Pythonらしい書き方:Protocolで「能力ごとのインターフェース」を作る

Protocolで「できること」を小さく表現する

Python では、抽象クラスの代わりに Protocol を使うことも多いです。
「このメソッドを持っていればOK」という“能力インターフェース”を作れます。

from typing import Protocol

class Drivable(Protocol):
    def go(self) -> None:
        ...


class Flyable(Protocol):
    def fly(self) -> None:
        ...
Python

ここでは、継承しなくても「go を持っていれば Drivable とみなす」ことができます。

class Car:
    def go(self) -> None:
        print("Going")


class Aircraft:
    def go(self) -> None:
        print("Taxiing")

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

関数側は、こう書けます。

def drive(d: Drivable) -> None:
    d.go()

def fly(f: Flyable) -> None:
    f.fly()
Python

CarAircraft も、継承していなくても Drivable として扱えます。
AircraftFlyable としても扱えますが、CarFlyable ではありません。

ここでも、「必要な能力だけ」に依存できています。
これが interface 分離の原則を Python で自然に実現した形です。


interface分離がテスト・品質に効いてくるポイント

テストダブル(モック)を小さく作れる

例えば、「飛べるものだけをテストしたい」関数があるとします。

def emergency_landing(f: Flyable) -> None:
    print("Emergency!")
    f.fly()
Python

テストでは、本物の Aircraft を使う必要はありません。
Flyable を満たす小さなテスト用クラスを作れば十分です。

class DummyFlyer:
    def __init__(self) -> None:
        self.called = False

    def fly(self) -> None:
        self.called = True

def test_emergency_landing():
    flyer = DummyFlyer()
    emergency_landing(flyer)
    assert flyer.called is True
Python

Flyable が「飛ぶ」という能力だけに分離されているから、
テストもその能力だけに集中できます。

もし「走る・飛ぶ・泳ぐ・しゃべる」が全部1つのインターフェースに詰まっていたら、
テスト用クラスにも全部のメソッドを用意しないといけません。
それは明らかに無駄で、壊れやすいです。

変更の影響範囲が小さくなる

例えば、「飛び方の仕様だけ変えたい」とします。
Flyable がきちんと分離されていれば、

Flyable を使っているコードだけを見ればいい
Drivable だけを使っているコードには影響しない

という状態になります。

逆に、巨大なインターフェースに全部詰め込んでいると、

「このインターフェースを実装しているクラス全部」
「このインターフェースを受け取っている関数全部」

を確認しないといけなくなります。
interface 分離は、「変更の波及範囲を小さくする」ための設計でもあります。


初心者が意識すると一気にうまくなるポイント

「このインターフェースを実装するクラスは、全部のメソッドを本当に使うか?」

インターフェース(抽象クラス・Protocol)を作るとき、
必ず自分にこう聞いてみてください。

「このインターフェースを実装するクラスは、全部のメソッドを“自然に”使うか?」

もし、

「このクラスはこのメソッドは使わないから、例外投げるだけだな」
「このメソッドは一生呼ばれないけど、仕方なく実装するか」

と思ったら、それは interface 分離が必要なサインです。

そのときは、

能力ごとにインターフェースを分けられないか
クライアントごとに、もっと小さいインターフェースにできないか

を考えてみてください。

「使う側から見て、“いらないボタン”が見えていないか?」

クライアント側のコードを見るのも大事です。

ある関数が Vehicle を受け取っているのに、
実際には go しか呼んでいないなら、
それは Drivable だけを受け取るべきかもしれません。

「この関数は、本当は何ができるものを欲しがっているのか?」
を考えると、インターフェースをどう分けるべきかが見えてきます。


まとめ(interface分離は「必要な能力だけに依存させる」ための設計ルール)

初心者向けに整理すると、interface 分離はこういう考え方です。

大きなインターフェースに何でも詰め込むと、「使わないメソッドの実装」を強制されたり、「使えない機能が見えてしまう」状態になり、バグや混乱の元になる。
能力ごと・クライアントごとにインターフェース(抽象クラス・Protocol)を小さく分けることで、「必要なものだけ実装する」「必要なものだけに依存する」設計にでき、テストもしやすく、変更の影響範囲も小さくなる。

タイトルとURLをコピーしました