TypeScriptを使って学ぶSOLID原則4 インターフェイス分離原則(Interface Segregation Principle)
モチベーション
ソフトウェアを設計する際に重要な5つのガイドラインであるSOLID原則について学んでいます。
前回はリスコフの置換原則(Liskov Sabstitutional Principle)についてアウトプットを行いました。
今回はインターフェイス分離の原則(Interface Segregation Principle) について学んだので、アウトプットの一環で記事を執筆しました。
インターフェイス分離の原則(Interface Segregation Principle)とは
インターフェイス分離の原則とは下記のことを表します。
Do not force any client to implement an interface which is irrelevant to them.
出典:https://www.geeksforgeeks.org/solid-principle-in-programming-understand-with-real-life-examples/
クライアントに関係のないプロパティやメソッドの実装を強制してはならないという原則です。
1つのインターフェイスに何でもかんでも詰め込むのえはなくクライアントが必要とする最低限のプロパティやメソッドのみを実装すべきというものです。
これにより各インターフェイスがそれぞれの明確な責務を負うことでコードの保守性を高めることができます。
実装例
インターフェイス分離の原則をコードで理解するために以下の例を用います。
あなたはベジタリアンです。レストランにてウェイターから渡されたメニューには下記のものが含まれていました。
- ベジタリアン料理のメニュー
- ベジタリアン用でない料理のメニュー
- ドリンクメニュー
この場合、ベジタリアンのあなたが受け取るメニューにはベジタリアン用の料理とドリンクのみが記載されているべきです。
レストランとしてはベジタリアン用でないメニューとベジタリアン用のメニューを用意していることが望ましいです。
個人的に一番わかりやすい例はBird
クラスからEagle(飛ぶことができる)
とPenguin(飛ぶことができない)
のサブクラスを作る例ですが、よく見かけるため勉強も兼ねてレストランを例に取りました。
守られていない実装例
// 全てのメニューが1つのメニューインターフェイスに含まれている
interface IMenu {
name: string;
description: string;
price: number;
getMeatDish(): string;
getVegetarianDish(): string;
getDrink(): string;
}
// ベジタリアン用のメニュークラス
class VegetarianMenu implements IMenu {
constructor(
public name: string,
public description: string,
public price: number
){}
// 必要のないメソッドの実装を強制されている
getMeatDish(): string {
throw new Error("Vegetarian menu should not contain meat dish");
}
getVegetarianDish(): string {
return "Vegetarian Dish";
}
getDrink(): string {
return "Coke";
}
}
// ベジタリアン専用でないメニュー
class NonVegetarianMenu implements IMenu {
constructor(
public name: string,
public description: string,
public price: number
){}
getMeatDish(): string {
return "Meat Dish";
}
// 必要のないメソッドの実装を強制されている
getVegetarianDish(): string {
throw new Error("Non-Vegetarian menu should not contain vegetarian dish");
}
getDrink(): string {
return "Coke";
}
}
この例ではIMenu
というインターフェイスが getMeatDish
とgetVegetarianDish
を保有しているためそのサブクラスであるVegetarianMenu
とNonVegetarianMenu
は双方に必要ないメソッドの実装を強要されています。
VegetarianMenu
にgetMeatDish
は必要なくNonVegetarianMenu
にgetVegetarianDish
は必要ありません。
よってこの例はインターフェイス分離の原則に違反していることになります。
守られている実装例
先ほどの実装をインターフェイス分離の原則を満たすように修正を加えたものを紹介します。先ほどのImenu
インターフェイスが持っていたgetMeatDish
とgetVegetarianDish
をそれぞれ別のインターフェイスに分離します。(双方が持っていたgetDrink
メソッドは割愛しています)
interface IMenu {
name: string;
description: string;
price: number;
}
interface VegetarianItems {
getVegetarianDish(): string;
}
interface NonVegetarianItems {
getMeatDish(): string;
}
class VegetarianMenu implements IMenu, VegetarianItems{
constructor(
public name: string,
public description: string,
public price: number
) {}
getVegetarianDish(): string {
return "Vegetarian Dish";
}
}
class NonVegetarianMenu implements IMenu, NonVegetarianItems {
constructor(
public name: string,
public description: string,
public price: number
){}
getMeatDish(): string {
return "Meat Dish";
}
}
この例ではImenu
インターフェイスが持っていたgetMeatDish
とgetVegetarianDish
をそれぞれNonVegetarianItems
とVegetarianItems
に分離しています。VegetarianMenu
クラスはVegetarianItems
をNonVegetarianMenu
はNonVegetarianItems
のサブクラスとしているため不必要なメソッドの実装は強要されていません。
よって、この例はインターフェイス分離の原則を守れていることになります。
原則に違反した場合はどうなるか
インターフェイス分離の原則に違反した場合、以下のような影響が出ます。
インターフェイスに変更があった際に実装側で使用していないメソッドであっても修正しなければならなくなる
この修正の影響で新たなバグを生んでしまう可能性があ離、保守性が損なわれてしまいます。
単一責任の原則に反した実装になる
インターフェイスが多くのメソッドを含んでいる状態(Fat interface)で複数のアクターに利用されると実装が複雑になってしまいます。
リスコフの置換原則に反した実装になる
使用しないメソッドをエラーを発生させる処理にすることでリスコフの置換原則に違反した実装になってしまいます。
原則を守った場合のメリット
インターフェイス分離の原則を守った実装を心がけることで、サブクラスで不要な実装を行わずに済みます。これにより単一責任の原則やリスコフの置換原則といった他のSOLID原則も守れていることになります。
実装がよりシンプルになり保守性や拡張性の向上が見込まれます。
どうすれば違反しない設計にできるか
役割ごとにインターフェイスを適切に分離する
それぞれの役割ごとに適切にインターフェイスを定義し、1つのインターフェイスが複数の役割を持たなくても済むようにします。1つのインターフェイスに何でもかんでもメソッドを詰め込んでしまうとFat interfaceになってしまい責務の明確性が損なわれてしまいます。
まとめ
SOLID原則の一つであるインターフェイス分離の原則について、初心者なりにまとめました。
「単一責任の原則に似ているな」と思いながら勉強していると、やはりこの原則を守ることにより単一責任の原則も守れていることになるということでした。
これらの原則を守り綺麗なコードを書くことを心がけることで、新機能の実装やバグの解消に素早く対処することができ、ユーザーが欲する機能を即座に提供できるためプロダクトやビジネスへの満足度向上に繋がるとと感じています。
Discussion