SOLIDの原則を心で理解する - インターフェース分離の原則
さて、SOLIDの原則を心で理解することの探求を続けましょう。その第四の原則は「インターフェース分離の原則」(ISP)であり、Wikipedia(英語版)では以下のようにまとめられています。
以前のSOLID原則に関する投稿、特に単一責任の原則(SRP)に関するものを読んだことがあるなら、その目的にすでに気付いているかもしれません。
ここでも同様に、疎結合のシステムを構築することを目指しています。これは、強い凝集性、低い結合性を達成するために、クラスとインターフェースを一貫性があり、モジュール化され、保守が容易な形で設計することを通じて実現します。
原則の理解
今回は乗り物(車・バイク)を参考例として作成しました。
まず、すべての種類の車両に対してメソッドを定義するインターフェースVehicle
をモデル化することで始めました。そのメソッドには、startEngine()
、stopEngine()
、accelerate()
、openDoors()
などがあります。
次に、2種類の車両、車(Car
)とバイク(Motorbike
)の実装しました。
車とバイクはstartEngine()
やstopEngine()
などの機能を共通して持っていますが、バイクにはドアがないため、openDoors()
メソッドはバイクには適用できません。
interface Vehicle {
startEngine(): void
stopEngine(): void
accelerate(): void
openDoors(): void
}
class Car implements Vehicle {
startEngine() {
// 🏎️💥
}
stopEngine() {
// 🏎️✋
}
accelerate() {
// 🏎️📈
}
openDoors() {
// 🔑🚪
}
}
class Motorbike implements Vehicle {
startEngine() {
// 🏍️💥
}
stopEngine() {
// 🏍️✋
}
accelerate() {
// 🏍️📈
}
openDoors() {
// どうやって... !?
}
}
クラスMotorbike
は、openDoors()
メソッドを実装する必要がありますが、これは意味がありません。これは「インターフェース分離の原則(ISP)」の違反です(🚨👮✋)
このクラスは使用しないメソッドに依存しています。
この問題を解決するために、インターフェースVehicle
をより小さくて具体的なインターフェースに分割する必要があります。
しかしその前に、この「インターフェース分離の原則(ISP)」違反が引き起こす問題は何でしょうか?
なぜこのコードが問題なのか
複雑さの増大
「インターフェース分離の原則(ISP)」の違反は、クラスが必要としない機能をサポートしなければならないため、コードの複雑さを増大させることがあります。これにより、コードの理解や保守が難しくなります。TypeScriptのような型付き言語の場合、不要なメソッドは「未実装のメソッド」というタイプのエラーをスローすることを余儀なくされることがあります。
コードの脆弱性
クラスが無関係な機能を持つインターフェースに依存する場合、それらのインターフェースに変更が加えられると、それを使用するクラスに意図しない影響を与えることがあります。これにより、コードがより脆弱になり、システムの維持や進化の過程でバグが導入されるリスクが増します。
不要な依存関係
この違反は、システムのコンポーネント間に不必要な依存関係を導入します。これにより、システムのテスト、デバッグ、リファクタリングが難しくなり、一部分の変更が他の部分に予期せぬ影響を与える可能性があります。
再利用の難しさ
モノリシックなインターフェースや過剰に複雑なインターフェースは、クラスの他のコンテキストでの再利用を難しくします。不要な機能を提供するクラスは、過剰な依存関係があるため、システムの他の部分に統合するのが難しいかもしれません。
インターフェース分離の原則(ISP)の適用
この例にインターフェース分離の原則(ISP)を適用しましょう。インターフェースVehicle
をMotorized
とDoorControllable
という二つの具体的なインターフェースに分割します。
interface Motorized {
startEngine(): void
stopEngine(): void
accelerate(): void
}
interface DoorControllable {
openDoors(): void
}
class Car implements Motorized, DoorControllable {
startEngine() {
// 🏎️💥
}
stopEngine() {
// 🏎️✋
}
accelerate() {
// 🏎️📈
}
openDoors() {
// 🔑🚪
}
}
class Motorbike implements Motorized {
startEngine() {
// 🏍️💥
}
stopEngine() {
// 🏍️✋
}
accelerate() {
// 🏍️📈
}
}
これにより、各クラスはインターフェース分離の原則(ISP)に従って、自分に関連するメソッドのみを実装するようになります。クラMotorbike
はopenDoors()
メソッドの実装を提供する必要がなくなり、これによりより一貫性があり、保守しやすいものとなります。
インターフェース分離の原則(ISP)の利点・メリット
この原則は、具体的で一貫性のあるインターフェースの設計を促し、それによってモジュール性が高く保守しやすいシステムを構築することができます。これにはいくつかの利点が含まれます:
保守の容易さ
クライアントが実際に使用するメソッドのみに依存するようにすることで、コードの保守と理解が容易になります。
柔軟性
インターフェース分離の原則は、ターゲットを絞ったモジュール化されたインターフェースの設計を促し、進化するニーズに適応できる、より汎用性の高いコードを実現します。
テストの容易さ
インターフェース分離の原則(ISP)に従うことで、テストスイートを簡素化でき、クライアントは実際に使用するメソッドのみをテストすればよいため、テストが容易になります。
再利用性
インターフェース分離の原則(ISP)は、制限されたターゲットを絞ったインターフェースの作成を促進し、さまざまな文脈で容易に再利用することができます。
まとめ
インターフェースを設計する際には、強い凝集性と低い結合性の両方を目指すべきです。
つまり、インターフェースは明確で一貫した目的を持ち、その目的に関連するメソッドだけを公開する必要があります。また、大きな汎用インターフェースを継承することは避けるべきです。これは不要な依存関係を生み出し、インターフェース分離の原則(ISP)を違反することになります。代わりに、継承ではなく合成を優先し、複数の小さくて具体的なインターフェースを使用してクラスの振る舞いを定義するべきです。
Discussion