ISP(Interface Segregation Principle)はSOLIDの第4の原則であり、クライアントは使用しないメソッドに依存すべきではないと定めています。この原則は、ロバート・マーティンによってオブジェクト指向システムのインターフェース設計の文脈で提唱されました。書籍 Clean Architecture(2017) で説明されているように、インターフェース分離の原則は、1つの汎用的なインターフェースではなく、狭く専門化されたインターフェースを作成することを要求し、結合度を減らして変更を容易にします。
重要なポイント
ISP(Interface Segregation Principle)は、すべてのクライアントが使用しないメソッドを含む“太った”インターフェースの作成を禁止するインターフェース分離の原則です。1つのインターフェースに多数のメソッドを置く代わりに、クライアントグループごとに複数の小さなインターフェースが設計されます。
この原則は、クラスが共通のインターフェースで宣言されているという理由だけで、必要のないメソッドを実装せざるを得ない“インターフェース汚染”の問題に対する解決策として、ロバート・マーティンによって導入されました。静的型付け言語では、これにより空の実装や例外のスローが発生し、ISP違反の直接的な兆候となります。
ISPとSRPは互いに補完し合います。SRPはクラスの責任に関するもので、ISPはインターフェースの契約に関するものです。SRPは“1つのクラス — 変更する理由は1つ”と言い、ISPは“1つのインターフェース — 1つのクライアントシナリオ”と言います。これらは一緒になって、システムの各要素が明確な境界を持つモジュラーアーキテクチャを形成します。
Fat Interface — 特定のクライアントが必要とするよりも多くのメソッドを含むインターフェース。たとえば、work、eat、sleepメソッドを持つWorkerインターフェース。ロボット労働者はeatやsleepを実装すべきではありませんが、強制的に実装することになります。解決策は、Workable、Eatable、Sleepableに分割することです。 各クライアントは必要なものを正確に得ることができます。
モバイル開発では、Fat InterfaceはデリゲートプロトコルやDataSourceに見られます。1つのプロトコルに2つの異なるシナリオ(編集と表示)のメソッドが含まれることがありますが、特定の画面はそのうちの1つしか使用しません。
ISPの実装は、各インターフェースのクライアントを分析することから始まります。2つのクライアントが1つのインターフェースの異なるメソッドセットを使用する場合、そのインターフェースは分割されるべきです。各新しいインターフェースは、1つのシナリオ内で一緒に呼び出されるメソッドをグループ化します。
分割のメカニズム:元のインターフェースは複数の狭いインターフェースに分割され、それぞれが共通部分(ある場合)を継承します。クライアントは一般的なインターフェースの代わりに、必要な狭いインターフェースに依存するように切り替わります。元のインターフェースを実装していたクラスは、実際に必要とする狭いインターフェースのみを実装します。
重要な注意点:分割の程度は、クライアントの数とそのシナリオによって決定されます。ISPは最大限の分割(各メソッドに1つのマイクロインターフェース)を要求しません。これにより過度の複雑さが生じるからです。目標は、クライアントの不要なメソッドへの依存を排除することであり、各インターフェースのサイズを最小化することではありません。
ISP違反の主な兆候には、空のメソッド(ダミー実装)でインターフェースを実装するクラス、実装でのUnsupportedOperationExceptionのスロー、一部のクライアントが使用しない多数のパラメータや戻り値の型、一部のクライアントのみに影響するインターフェースの頻繁な変更が含まれます。
Android開発におけるISP違反の典型的な例は、クリック、長押し、スワイプのメソッドを含むOnItemClickListenerインターフェースです。特定の画面がクリックのみを使用する場合、残りのメソッドは空のままです。解決策は、OnItemClickListener、OnItemLongClickListener、OnItemSwipeListenerに分割することです。
iOS開発では、ISP違反はUIKitデリゲートに現れます。1つのプロトコルに異なるコンポーネント状態のメソッドが含まれます。UITableViewDelegateには、表示、選択、編集、スワイプアクションのメソッドが含まれています。開発者はしばしば、多数の空メソッドを持つプロトコル全体を実装します。責任グループごとに複数のプロトコルに分割することで問題が解決します。
問題はコードの美学だけではありません。インターフェースが変更される(新しいメソッドが追加される)と、すべての実装クラスを更新する必要があります — 新しいメソッドを必要としないクラスでも同様です。多数の画面を持つモバイル開発では、これにより連鎖的な変更が発生します。ISPは各クライアントを、そのクライアントに関係のない変更から隔離します。
暗黙的なISP違反は、設定パラメータを通じて発生します。メソッドが多数のフィールドを持つオブジェクトを受け入れ、クライアントがそのうちの2〜3しか使用しない場合、これは分割の合図です。代替案:最小限のパラメータセットを持つ複数の特殊化されたメソッド。
Android開発では、すべてのアプリケーション設定の読み取りと書き込みに単一のSharedPreferencesManagerを使用する際にISPが違反されます。テーマを読むだけのFragmentが、異なるデータ型のための多数のメソッドを持つグローバルマネージャーに依存することになります。ThemePreferenceProvider、AuthPreferenceProvider、FeatureFlagProviderへの分割 — 設定サービスレベルでのISPの適用です。各プロバイダーは、そのクライアントが必要とするメソッドのみを正確に含みます。
データを扱うためのインターフェースを使用したAndroidの例を考えてみましょう。ISP違反 — すべてのCRUD操作のための1つのインターフェースですが、すべてのクライアントがすべての操作を必要とするわけではありません。
// ISP違反:Fat Interface
interface UserRepository {
fun getAll(): List<User>
fun getById(id: Int): User
fun save(user: User)
fun delete(id: Int)
}
// ISP適用後:狭いインターフェース
interface UserReader {
fun getAll(): List<User>
fun getById(id: Int): User
}
interface UserWriter {
fun save(user: User)
fun delete(id: Int)
}
// ReadOnlyViewModelは書き込みメソッドに依存しない
class ReadOnlyViewModel(
private val reader: UserReader
)
メディアを扱うためのプロトコル分割を使用したiOSの例:
// ISP違反:すべてのメディア作業に1つのプロトコル
protocol MediaService {
func play(url: URL)
func pause()
func stop()
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// ISP後:責任によるプロトコルへの分離
protocol MediaPlayer {
func play(url: URL)
func pause()
func stop()
}
protocol MediaTransfer {
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// PlayerViewModelはダウンロードメソッドに依存しない
class PlayerViewModel {
private let player: MediaPlayer
}
実践的な結論:ISPは、インターフェースの無関係な部分の変更からクライアントを保護します。UserRepositoryをUserReaderとUserWriterに分割することは、saveの変更がReadOnlyViewModelに影響を与えず、その逆も同様であることを意味します。各クライアントは使用しない機能から隔離され、システムの他の部分が変更されても変更を必要としません。
ISPとSRP — 自然な組み合わせです。SRPはクラスが変更する理由を1つ持つべきだと定義します。ISPは同じ論理をインターフェースに適用します:インターフェースは1つのクライアントシナリオにサービスを提供すべきです。クラスは複数の狭いインターフェース(それぞれが1つの責任に対応)を実装でき、これは複数の責任を持つ1つのFat Interfaceよりもクリーンです。
ISPとOCPも関連しています:狭いインターフェースは拡張が容易です。狭いインターフェースに新しいメソッドを追加しても、そのクライアントのみに影響します。Fat Interfaceにメソッドを追加すると、すべてのクライアントに影響し — クライアントが実装を変更せざるを得ない場合、OCPに違反する可能性があります。
ISPとDIPは連携して機能します:DIPは抽象化への依存を要求します。ISPはこれらの抽象化を狭く焦点を絞ったものにします。広いインターフェースへの依存は依然として抽象化への依存ですが、ISPの観点からは“悪い”抽象化です。4つの原則(SRP、OCP、ISP、DIP)は“モジュール性のピラミッド”を形成します:SRPとISPは境界を定義し、OCPとDIPは拡張と結合の方法を定義します。
モバイルプロジェクト(モジュール、機能、レイヤー)におけるコンポーネントアーキテクチャは、パブリックAPIレベルでISPから恩恵を受けます。各モジュールは、1つの共通ファサードではなく、消費者向けに狭いインターフェースをエクスポートします。これにより、モジュールの機能の一部のみを使用する消費者に影響を与えることなく、モジュールの内部実装を変更できます。
Clean Architectureを使用するAndroidプロジェクトでは、ISPがUseCaseに適用されます:各UseCaseは、単一のinvokeまたはexecuteメソッドを持つ個別のインターフェースです。クライアント(ViewModel)は、リポジトリ全体ではなく、必要なUseCaseにのみ依存します。これにより、依存関係が透過的でテスト可能になります。
よくある質問
はい、過度の分割は可能です。ISPはメソッドごとに1つのインターフェースを要求しません。基準となるのは:インターフェースのメソッドの一部のみを必要とするクライアントが存在するかどうかです。すべてのクライアントがすべてのメソッドを使用する場合、インターフェースを分割する必要はありません。最適な分割レベルは、実際の使用シナリオによって決定されます。
パラメータレベルでのISPとは:関数が多数のフィールドを持つオブジェクトを受け入れるべきではなく、その一部しか使用しない場合です。代わりに、必要なデータのみを渡すか、特化されたインターフェース(たとえば、完全なUserではなくRenderableインターフェース)を使用すべきです。
LSPは正しい継承と行動的サブタイプの互換性に関するものです。ISPはインターフェース設計に関するものです:クライアントは使用しないメソッドに依存すべきではありません。LSPは“サブクラスをベースクラスの代わりに使用できるか?”という質問に答え、ISPは“クライアントはインターフェース全体を必要とするか?”という質問に答えます。
狭いインターフェースはモックオブジェクトの作成を簡素化します:テストは1つか2つのメソッドを持つモックを作成し、多数のメソッドを持つモックは作成しません。インターフェースのメソッドが少なければ少ないほど、その振る舞いをスタブ化するのが容易になります。これにより、テスト開発者の認知負荷が軽減され、モックロジックのエラーの可能性が減少します。
インターフェースが安定しており、すべてのクライアントがすべてのメソッドを使用する場合、分割は冗長です。典型的な例:Appleが設計したUIKitプロトコル。これらを分割するのはリスクが伴います。なぜならUIKitはデリゲートの完全な実装を期待するからです。このような場合、ISP違反はAPIの安定性によって正当化されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。