保守性と拡張性を高めるためのSOLID原則とシステム設計
第8話:インターフェース分離の原則(2)具体例
(最終更新日:2024.10.06)
(絵が小さい場合はスマホを横に)
具体例を通して、ISPを身に着けよう!
インターフェース分離の原則(Interface Segregation Principle, ISP)は、
クライアントが使わないメソッドに依存しないようにすることで、システムの柔軟性と保守性を向上させるオブジェクト指向設計の重要な原則だ。
今回は、良い例、悪い例、リファクタリングの具体例を通して、ISPに沿った設計、コーディングの方法を学ぶ。
[目次]
1.良い例と悪い例
本項では、インターフェース分離の原則(Interface Segregation Principle, ISP)を守る設計と、
それに違反した設計を比較して、どのようにシステムの柔軟性と保守性が影響を受けるかを具体的に説明する。
インターフェース分離の原則を守ることで、クラスが不必要な機能に依存せず、必要な機能だけを持つ小さなインターフェースを設計できる。
1.1 悪い例: 大きなインターフェースに依存する設計
悪い例では、クライアントが使わないメソッドを含む大きなインターフェースを実装してしまうことで、無駄な依存や実装の複雑化が生じる。
これにより、システムの保守性や柔軟性が損なわれるリスクがある。
大きすぎるインターフェース
このMachineインターフェースは、機械の操作(start()、stop())と、メンテナンス(repair()、clean())を含む大きなインターフェースだ。 全ての機械がメンテナンス機能を必要とするわけではありません。 例えば、ロボットがこのインターフェースを実装する場合、メンテナンス機能が不要である。 実装する際も、機能を使うことがない。
大きすぎるインターフェースの実装
問題点としては、ロボットはrepair()やclean()メソッドを必要としないにもかかわらず、それらのメソッドを実装する必要がある。 これはクラス設計を複雑にし、無駄な依存関係を生み出している。 加えて、ロボットが実装すべきではないメソッドを持っているため、将来的な修正や拡張時に不整合が生じる可能性がある。
1.2 良い例: 役割ごとに分割されたインターフェースを持つ設計
インターフェース分離の原則に従い、クライアントが必要とする機能だけを持つインターフェースを分割して設計することにより、
柔軟で保守しやすいシステムが構築できる。下記は必要に応じて分割されたインターフェースだ。
分割されたインターフェース
これにより、ロボットはOperableインターフェースだけを実装し、不要なメソッドに依存することなく、必要な機能だけを提供できる(上)。 そして、メンテナンス担当者や機械メンテナンスが必要なクラスにはMaintainableインターフェースを実装させることができる(下)。
ロボットに必要な実装(上)、メンテナンス担当者に必要な実装(下)
1.3 良い例と悪い例の比較
最後に良い例と悪い例の特徴をまとめる。インターフェースを適切な機能に絞ることで、多くのメリットが生まれる。
項目 | 悪い例 | 良い例 |
---|---|---|
依存関係 | Machineインターフェースは多くの機能を持ち、ロボットなど不必要な機能にも依存してしまう。 | OperableとMaintainableインターフェースに分離することで、各クラスは必要な機能にのみ依存する。 |
柔軟性 | 大きなインターフェースが変更されると、全てのクラスに影響が出るため、柔軟性が低い。 | 小さなインターフェースに分割することで、クラスに影響を与えずに変更や拡張ができ、システムが柔軟に対応できる。 |
保守性 | クラスが不要なメソッドに依存しているため、修正や拡張時に多くの影響を受け、保守性が低下する。 | 必要な機能だけを実装するため、保守や修正が局所化され、システムの保守性が高まる。 |
可読性 | クラスが実装しない機能や無効化されたメソッドを持つことで、コードの可読性が低下し、理解が難しくなる。 | 各クラスはシンプルなインターフェースのみを実装するため、コードが明確で理解しやすい。 |
再利用性 | 機能が密結合しているため、別のシステムで再利用する際に不必要な機能が含まれてしまう。 | 分離されたインターフェースは、異なるシステムでも容易に再利用できる。 |
1.4 まとめ
ISPを守ることで、システム全体の柔軟性、保守性、可読性が大幅に向上する。
大きなインターフェースに依存する設計では、クライアントが不必要な機能に依存することになり、システムが複雑化する。
対照的に、役割ごとにインターフェースを分割する設計では、必要な機能だけを提供するため、
クラスがシンプルかつ効率的に機能する。このアプローチにより、システムの変更に強く、将来的な拡張が容易な設計が実現できる。
2.リファクタリングの具体例
リファクタリングの具体例では、既存の設計がインターフェース分離の原則(ISP)に違反している場合に、 そのコードを改善するためにどのようにリファクタリングを行うかを解説する。 インターフェースを適切に分割することで、無駄な依存を排除し、クラス設計の保守性や柔軟性を向上させることが目的だ。
2.1 リファクタリング前の問題点
まず、リファクタリング前の設計を見てみよう。
下記は、大きすぎるインターフェースが原因で、複数のクライアントが不要なメソッドに依存してしまっている例だ。
このWorkerインターフェースには、work()とeat()という2つのメソッドが含まれている。
これは、人間の作業者には適していますが、ロボットの作業者には不適切だ。
ロボットはwork()メソッドのみを必要としますが、eat()メソッドを無効化するか、例外をスローしなければならない状況が発生する。
リファクタリング前
2.2 リファクタリングによる改善
インターフェース分離の原則を適用するための基本的な考え方は、1つのクライアントが使う機能だけを提供するようにインターフェースを分割することだ。
これにより、インターフェースがシンプルになり、必要な機能だけを実装すれば良いという柔軟性が得られる。
下記のリファクタリング後の設計では、
1つの大きなWorkerインターフェースを、WorkableとEatableという2つのインターフェースに分割し、それぞれの役割に適した機能だけを持たせている。
そして、ロボットクラスには、Workableインターフェースだけを実装し、不要なeat()メソッドに依存しないようにしている。
また、ヒューマンクラスには、WorkableインターフェースとEatableインターフェースの両方を実装し、必要な機能を持つようにしている。
リファクタリング後
2.3 リファクタリングの効果
リファクタリング後のとしては、以下の3点が挙げられる。
A. 依存関係の最小化
リファクタリング後、ロボットはWorkableインターフェースだけに依存し、不要なeat()メソッドに依存する必要がなくなる。
これにより、クラスが不必要な機能に縛られない。
B. コードの一貫性とシンプルさ
リファクタリング前のコードでは、ロボットクラスがUnsupportedOperationExceptionをスローするなど、不整合なコードが含まれていた。
リファクタリング後は、各クラスが自分の役割に応じたインターフェースのみを実装するため、コードがシンプルで一貫性がある。
C. 拡張性と保守性の向上
インターフェースが分割されていることで、新しい作業者や新しい機能を追加する際に、既存のインターフェースやクラスに変更を加えず対応できる。
これにより、システム全体が変更に強くなり、保守が容易になる。例えば、新しい作業者として「動物」を追加する場合、Eatableインターフェースだけを実装し、
Workableインターフェースは実装しないことで、自然な形で役割分担を表現できる。
機能ごとに分けたインターフェース
2.4 ISPを活用した設計のベストプラクティス
ISPを活用した設計のベストプラクティスとしては、
インターフェースを小さく、シンプルに保つ(インターフェースは1つの責任に集中させ、特定の役割に必要な機能だけを持たせる)、
クライアントごとに必要なインターフェースを設計する(クライアントが実装する必要のないメソッドを含む大きなインターフェースは避け、クライアントごとにインターフェースを分割する)、
既存のインターフェースを変更せずに拡張できるようにする(既存のインターフェースに変更を加えずに、新しいインターフェースやクラスを追加できる柔軟性を持たせる)
という3点が重要になる。
2.4 まとめ
リファクタリングによって、ISPを適用し、役割ごとに適切なインターフェースを提供することで、
クラスの依存関係が最小化され、保守性や拡張性が向上する。
大きなインターフェースに依存している場合、不要な機能を実装しなければならない問題が発生するが、
リファクタリングを通じて小さなインターフェースに分割することで、クラスは自分の役割に応じた機能だけを持つようになり、
シンプルで一貫性のあるコード設計が実現する。