インターフェース分離の原則(ISP)

SOLID の原則というソフトウェア設計・実装をする際に覚えておいた方がいい5つの原則のうち、インターフェース分離の原則(Interface Segregation Principle = ISP)の紹介をしたいと思います。

記事中の実装言語は .NET 8(C#12)です。

インターフェース分離の原則の定義

日本語では、ネットや書籍の2次・3次情報を更に再解釈したみたいな発信が多いので、もう少し正確に紹介したいと思います。

以下、ロバート・C・マーティンの ISP の定義です。

CLIENTS SHOULD NOT BE FORCED TO DEPEND UPON INTERFACES THAT THEY DO NOT USE.
↓
クライアントは、自分たちが使用しないインターフェースに依存することを強制されるべきではない

また、2020年にロバート・C・マーティンがブログで以下のように説明しています。

Robert C. Martin本人ブログから引用 → Clean Coder Blog

ISP) The Interface Segregation Principle.
Keep interfaces small so that users don’t end up depending on things they don’t need.
↓
利用者が不要なものに依存してしまわないようにインターフェースは小さく保つ

CLIENTSusers はあるクラスやインターフェースを使用する側のことです。利用者視点で不要な依存をしないということなので、提供側の実装クラスが小さいインターフェースを複数実装しましょうという意味ではないので注意が必要です。

よくあるISP違反とか誤解

そもそも、ISP は 利用者側 視点のため以下のような、サービスの提供側の視点でインターフェースを切ることと直接関係なくない?というのが以下の例です。

スマートフォン、ガラケー、ポケベルがあり、それらが電話 IPhone インターフェースを実装する

というか、ポケベルをリリースしたときにスマートフォンなどほぼ確実に想定できないため架空の存在を仮定したインターフェースをポケベルに継承することは逆にアンチパターンの可能性が高い…事は今回はさておき)通信機器だから似てる程度で全く異なる機器に対して IPhone インターフェースを実装すると、それっていらない機能がインターフェースに実装されがち = 関係ないメソッドへの依存が発生 = ISP 違反発生のような流れに自然となってしまう可能性が高いです(この場合、スマホ機種1、スマホ機種2 などのほうが例としては良いですね)

じゃあどうすればいい?

じゃあ実際どうすればいい?って話ですが、クライアント視点で不要な依存をしないためには例えば Adapter 経由で興味の範囲を限定する構造が考えられます(というか ISP 単体で説明が難しいので ISP + Adapter パターン形式で具体化してみます)

1) 通話機能を利用する処理
↓
2) その処理が必要とする最小限のインターフェース
↓
3) (2)のインターフェースを実装し、実デバイスとの差を吸収するアダプターまたは実装クラス
↓
4) スマホやガラケーなどの具体的な機能提供元

1) 通話をしたいユーザーの実装 が必要とする操作だけをインターフェースとして定義し、(2) と (3) を作成して (4) - (1) 間の直接の依存を排除しつつ、(1)が必要とする最小のインターフェースを定義しましょう、とするとクライアント視点では 自分たちが使用しないインターフェースに依存 が無くなり ISP に沿ってる事になります。

ちょっと IPhone の例が悪くて全体が微妙でしたが、これを抽象化すると以下のようになります。

1) ある機能や処理(クライアント)
↓
2) その処理が必要とする最小限のインターフェース
↓
3) (2)のインターフェースを実装し、依存先の都合を吸収するアダプターまたは実装クラス
↓
4) 外部の具体的な機能依存先(インターフェースや具象クラス)

3) (2)のインターフェースを実装し、依存先が何であろうと自分の興味の範囲だけに限定するアダプターまたは実装クラス とすることで最小のインターフェースとなる ISP の不要なものに依存しない 部分を強化できます。

クラス図とC#の実装例

上記の抽象例をクラス図に表すとこのような形になります。

クライアント自身にもインターフェースを実装したほうがいいとか、ISP としては Adapter の先の構造は興味の範囲外というのはさておき C# で実装すると以下の通りです。

// 外部の依存先のインターフェース
public interface IExternalService
{
    void Method1();
    void Method2();
    void Method3();
    void Method4();
    // このほかにメソッドが多数存在する
}
// 外部の依存先の実装
public class ExternalService : IExternalService
{
    public void Method1()
    {
        // any process
    }

    public void Method2()
    {
        // any process
    }

    public void Method3()
    {
        // any process
    }

    public void Method4()
    {
        // any process
    }
    // このほかにメソッドが多数存在する
}
// クライアントが必要とする操作のみのインターフェース
public interface IExternalServiceAdapter
{
    void Method1();
    void Method2();
}
// クライアントが必要とする操作のアダプターの実装
public class ExternalServiceAdapter : IExternalServiceAdapter
{
    readonly IExternalService _service;
    public ExternalServiceAdapter(IExternalService service)
    {
        _service = service;
    }

    public void Method1()
    {
        _service.Method1();
    }
    public void Method2()
    {
        _service.Method2();
    }
}
// クライアント
public class Client
{
    readonly IExternalServiceAdapter _clientService;
    public Client(IExternalServiceAdapter clientService)
    {
        _clientService = clientService;
    }

    public void Foo()
    {
        _clientService.Method1();
        _clientService.Method2();
    }
}

特に ISP を意識した実装で避けたいのは IExternalServiceClient が直接使用するケースで、インターフェースがあるからそれつかえばいいじゃん、という流れで以下のように IExternalService をクライアントが直接受け取ってしまうと使用しないインターフェースへの依存が強制されてしまいます。

// クライアント
public class Client
{
    readonly IExternalService _service;
    public Client(IExternalService service)
    {
        _service = service; // 外部の大きなインターフェースに依存する
    }

    public void Foo()
    {
        _service.Method1();
        _service.Method2();
        // ここまではいいけどここで見えてない
        // ★Method3, Method4 の変更やそれ以外の変更にClientが巻き込まれる可能性がある
    }
}

「★Method3, Method4 の変更やそれ以外の変更にClient が巻き込まれる可能性がある」ですが例えば、Method99() が追加される、Method4 のパラメータの並びが変わって急にテストスタブでエラーが出るなどで、直接見えてないだけで依存してる状態から影響が出る可能性が潜在してます。例えばある日ライブラリを最新化したら急にエラーが出るみたいな状態ですね。

ちょっと説明が長くなってしまいましたが今回は以上です。

参考サイト

ロバート・C・マーティンのブログ

blog.cleancoder.com