Web制作、Web開発の歩き方

保守性と拡張性を高めるためのSOLID原則とシステム設計

第7話:インターフェース分離の原則(1)設計と分離の方法

(最終更新日:2024.10.02)

SOLID原則とシステム設計
この記事は7分で読めます!
(絵が小さい場合はスマホを横に)

役割ごとにインターフェースを分けよう!

インターフェース分離の原則(Interface Segregation Principle, ISP)は、 クライアントが使わないメソッドに依存しないようにすることで、システムの柔軟性と保守性を向上させるオブジェクト指向設計の重要な原則だ。 ISPを守ることで、必要な機能だけを持つ小さなインターフェースを作成し、不必要な依存からクラスが解放される。 今回は、ISPの具体例を通じて、無駄のないインターフェース設計を行う方法を解説し、効率的なシステム構築のメリットを紹介する。


1.インターフェース分離の原則とは

インターフェース分離の原則(ISP)は、オブジェクト指向設計におけるSOLID原則の1つであり、 特に大規模なシステムにおいてのインターフェース設計指針を提供する。 この原則は、クライアントは、必要としないメソッドに依存してはいけないという考え方に基づいている。 つまり、インターフェースは、クラスが実際に使用する機能だけを提供するように分離されるべきという考えに基づく。

1.1 インターフェース分離の原則とは
ISPの基本的な定義は次の通りである。
特定のクライアント(利用者)に合わせた小さなインターフェースを設計し、大きなインターフェースを持つことを避ける
これは、インターフェースが大きくなりすぎて、クライアントが不要なメソッドを実装したり、依存したりするのを防ぐための設計原則だ。 大きなインターフェースは複数の異なる機能を1つにまとめた結果、必要のない機能も実装しなければならない状況が発生する。 ISPを守ることで、システムはより柔軟で保守しやすくなる。

ISPに違反した例を考えてみる。以下はWorkerというインターフェースが大きくなりすぎて、不要なメソッドを持ってしまっている。 このインターフェースはworkとeatの機能を持っている。しかし、実装で必要なのはworkメソッドだけである。

ISP違反の例

問題なのは、不要な依存関係(eatメソッドの実装)ができてしまっていることである。 不要な機能は、生成するクラスが本来必要のない機能に依存してしまうことになり、保守が難しくなる。

1.2 インターフェースを分割する(ISPの実践)
インターフェース分離の原則に従い、Workerインターフェースを必要な機能に応じて小さなインターフェースに分割しよう。 例えば、WorkableとEatableという2つのインターフェースを定義する。 下記の実装では、ロボットはWorkableインターフェースだけを実装し、人間クラスがEatableとworkインターフェースの2つを実装することで、 クライアントにとって必要な機能のみを持つように設計できる。

ISPを守った例

柔軟性の向上:Robotクラスはworkメソッドのみを実装するため、eatのような余計な依存がなくなる。 同様に、人間クラスはeatメソッドも実装できる。追加で動物クラスを作った場合はeatメソッドのみを実装できる。

保守性の向上: インターフェースが小さくシンプルになり、それぞれが明確な責任を持つため、将来的な変更や拡張がしやすくなる。

再利用性の向上: 各クラスは必要な機能だけを実装することで、他の機能に縛られず、異なるコンテキストでの再利用が容易になる。

1.3 インターフェース分離の原則を守る理由
クラス間の結合度を下げる:ISPを守ると、クライアントは使わないメソッドに依存することがなくなり、 システムのモジュール同士の結合度が低くなる。これにより、システムの拡張や修正が容易になる。

無駄な実装を避ける:クライアントが必要のないメソッドを実装することがなくなるため、 コードの無駄が減り、設計がよりクリーンになる。

1.4 まとめ
インターフェース分離の原則は、クライアントに必要な機能だけを提供することで、クラス設計を柔軟で保守性の高いものにする。 大きなインターフェースを小さなインターフェースに分割し、各クラスが不要なメソッドに依存しないようにすることで、 システム全体がシンプルで理解しやすくなる。

2.インターフェースの設計と分割の方法

インターフェースの設計と分割の方法は、ISPを実践するために、インターフェースをどのように設計し、適切に分割するかに焦点を当てた内容だ。 インターフェースを大きくしてしまうと、必要ない機能にも依存しなければならない問題が生じるため、 クライアントの必要に応じて細かく分割することが重要になる。

2.1 大きなインターフェースの問題点
大きなインターフェースは、複数の機能を含むため、それを実装するクライアントに不必要なメソッドが強制される。 例えば、1つのインターフェースがメンテナンス機能や読み取り機能、書き込み機能などを持っていると、 そのインターフェースを使うクラスは全ての機能を実装する必要が出てくる。 これにより、クラスの設計が複雑化し、保守性や拡張性が損なわれる。

下記のMachineインターフェースには、、機械の操作(start()、stop())と、メンテナンス関連の機能(repair()、clean())が含まれている。 しかし、全ての機械がメンテナンス機能を必要とするわけではない。 例えば、自動的にメンテナンスを行うロボットはrepair()やclean()メソッドを実装する必要がなく、不適切だ。

大きすぎるインターフェース

2.2 インターフェース分離の基本的な考え方
インターフェース分離の原則を適用するための基本的な考え方は、1つのクライアントが使う機能だけを提供するようにインターフェースを分割することだ。 これにより、インターフェースがシンプルになり、必要な機能だけを実装すれば良いという柔軟性が得られる。

2.3 インターフェースの分割方法
2.3.1 機能ごとにインターフェースを分割
インターフェースの分割は、クライアントごとのニーズに基づいて行う。 1つの大きなインターフェースを複数の小さなインターフェースに分けることで、クライアントが不要な機能に依存せずに済むようになる。 例えば、先ほどのMachineインターフェースを分割すると、次のような形になる。

このように、Operableインターフェースには操作に関する機能だけが、Maintainableインターフェースにはメンテナンス機能だけが含まれる。 これにより、各クラスは必要なインターフェースのみを実装することができる。

機能ごとに分けたインターフェース

2.3.2 クライアントごとに異なるインターフェースを提供
クライアントが異なる機能を必要とする場合、そのクライアントの要件に応じた専用のインターフェースを設計する。 これにより、インターフェースはよりターゲットに応じた形で提供されるため、使いやすくなる。 例えば、HumanクラスとRobotクラスがそれぞれ異なるニーズを持っている場合、それに応じたインターフェースを作成する。

この分割によって、Humanはeat()メソッドを持つインターフェースを実装し、Robotはrecharge()メソッドを持つインターフェースを実装することができる。 これにより、クラスごとに適切なメソッドを実装し、不必要なメソッドを持つことがなくなる。

機能ごとに分けたインターフェース

2.3.3 デフォルト実装を用いた柔軟なインターフェース
Java 8以降では、インターフェースにデフォルト実装を持たせることが可能だ。 これを利用することで、特定の機能を共有したいクライアントにはその機能を提供し、 他のクライアントはデフォルトの実装を使うか、オーバーライドすることでインターフェースをさらに柔軟に利用できる。 これにより、基本的な修理や掃除の機能を持つクラスはそのままデフォルトの実装を使用でき、 特別な処理が必要なクラスはこれをオーバーライドして独自のメソッドを提供することができる。

default実装を用いたインターフェース

2.4 まとめ
インターフェース分離の原則に基づく設計と分割は、システムの柔軟性と保守性を高めるために不可欠だ。 クライアントに必要な機能だけを持つ小さなインターフェースを設計することで、複雑さを抑え、 必要な機能に依存したシンプルで効率的なクラス設計が可能になる。

3.役割ごとのインターフェース設計

役割ごとのインターフェース設計は、ISPを効果的に適用するために、 クライアント(利用者)の役割に応じてインターフェースを設計することを意味する。 クラスが特定の役割を持っている場合、その役割に応じたインターフェースのみを提供することで、 無駄な機能の実装を避け、柔軟性と保守性を高めることができる。

3.1 役割ごとのインターフェース設計の目的
大規模なシステムでは、異なる役割(例えば、作業者、メンテナンス担当者、管理者など)がそれぞれ特定の機能を必要とする。 しかし、1つの大きなインターフェースに全ての役割の機能を詰め込んでしまうと、役割ごとに不要な機能が含まれ、 冗長な実装や無駄な依存が発生してする。 これを防ぐために、ISPでは役割ごとに適したインターフェースを設計し、必要な機能だけを提供することを推奨する。

3.2 役割ごとにインターフェースを分離する考え方
異なる役割に応じて、クライアントが利用する機能が異なるため、それぞれの役割に適したインターフェースを設計する。 この設計により、各クラスは自分の役割に必要なインターフェースのみを実装し、不要な機能に依存することがなくなる。 自動車工場を例に考えてみる。工場内では、次のような役割が存在する。

操作者(Operator): 機械を操作し、開始や停止を行う。
メンテナンス担当者(MaintenanceWorker): 機械の修理やメンテナンスを行う。
管理者(Administrator): 操作やメンテナンスの状況を監視し、記録を管理する。

このような異なる役割を持つクライアントには、それぞれ適したインターフェースを設計する。

3.3 役割ごとのインターフェースの設計例
A. 操作者(Operator)用のインターフェース
操作者は機械の操作だけが必要なので、操作に関するメソッドのみを持つインターフェースを提供する。 このような異なる役割を持つクライアントには、それぞれ適したインターフェースを設計する。 Operableインターフェースには、機械の開始と停止に関するメソッドだけが含まれる。 これにより、操作者はメンテナンスや監視に関する機能を実装せずに済む。

B. メンテナンス担当者(MaintenanceWorker)用のインターフェース
メンテナンス担当者には、機械の修理やメンテナンスに関するメソッドを提供する。 Maintainableインターフェースには、機械の修理や掃除など、メンテナンスに関連する機能だけが含まれる。 これにより、メンテナンス担当者は機械の操作には関与せず、役割に応じた機能のみを実装する。

C. 管理者(Administrator)用のインターフェース
管理者には、システム全体の監視や記録管理に関するメソッドを提供する。 Manageableインターフェースは、管理者が機械の動作状況を監視したり、システムの記録を管理するための機能を提供する。 これにより、管理者は操作やメンテナンスに関する具体的なメソッドを実装せずに、システムの全体状況に集中できる。

役割ごとのインターフェース

3.4 インターフェースの実装例
これらのインターフェースを使って、クラスごとに必要なインターフェースのみを実装した例を以下に示す。 それぞれの役割に合わせた実装ができていることが分かる。

役割ごとのインターフェース

3.5 役割ごとのインターフェース設計の利点
A. クラスの依存を最小限に抑える:各クラスは必要な機能にしか依存しなくなるため、 他の不要なメソッドや機能に縛られることがない。これにより、クラスの設計がシンプルになり、必要な変更が局所化される。

B. クラス設計の柔軟性が向上:役割ごとに分離されたインターフェースにより、異なるクラスが異なる役割を容易に実装できる。 これにより、システムの変更や拡張に対する柔軟性が大幅に向上する。

C. 保守性が向上:各クライアントに適したインターフェースのみを実装するため、 将来的な変更が必要になった際も、特定のクライアントに影響を与えることなくインターフェースの修正や拡張ができる。

3.6 まとめ
役割ごとのインターフェース設計は、ISPを効果的に実践するための重要な手法だ。 クライアントの役割ごとに適したインターフェースを設計することで、システムの柔軟性が向上し、不要な依存を避けることができる。 これにより、保守性と拡張性が高く、変更に強いシステム設計が可能になる。