TypeScriptを使って学ぶSOLID原則5 依存性逆転の原則(Dependency Inversion Principle)
モチベーション
ソフトウェアを設計する際に重要な5つのガイドラインであるSOLID原則について学んでいます。
前回はインターフェイス分離の原則(Interface Sefregation Principle)についてアウトプットを行いました。
今回は依存性逆転の原則(Dependency Inversion Principle) について学んだので、アウトプットの一環で記事を執筆しました。
依存性逆転の原則(Dependency Inversion Segregation Principle)とは
依存性逆転の原則とは下記の2つことを表します。
- High-level modules should not depend on low-level modules. Both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstractions.
出典:https://www.geeksforgeeks.org/solid-principle-in-programming-understand-with-real-life-examples/
- 上位モジュールは下位モジュールに依存してはならない。両者は抽象に依存すべきである
- 抽象は詳細に依存すべきでなく、詳細が抽象に依存すべきである
という原則です。
ここでいう依存というのはあるモジュールが別のモジュールの機能を利用することを意味します。この際、利用される側が上位モジュール、利用する側を下位モジュールと呼びます。
※モジュール:ソフトウェアにおけるひとまとまりの機能群のこと
実装例
依存性逆転の原則をコードで理解するために「レストランのオーダーサービスの処理」を例にして考えます。
守られていない実装例
class PizzaChef{
prepareDish(dish:string){
console.log(`Preparing ${dish}`);
}
}
class OrderService{
private chef:PizzaChef;
constructor(){
// 直接PizzaChefをインスタンス化しているため、依存している
this.chef = new PizzaChef();
}
orderPizza(){
this.chef.prepareDish("Pizza");
}
}
const orderService = new OrderService();
orderService.orderPizza();
下位モジュールのOrderService
クラスにて上位モジュールのPizzaChef
クラスを直接インスタンス化している為、両者が依存してしまっています。仮にレストランがピザのほかに寿司も提供したい場合はOrderService
クラスの実装を変更しなければなりません。
よってこの例は依存性逆転の原則に違反していることになります。
守られている実装例
先ほどの例を依存性逆転の原則を提供した実装に変更しました。抽象化されたIChefというインターフェイスを作成しOrderService
クラスとPizzaChef
クラスはそのインターフェイスに依存するように変更しています。
// 抽象化されたインターフェースを作成
interface IChef{
prepareDish(dish:string):void;
}
// クラスは抽象化されたインターフェイスIChefの詳細を実装している
class PizzaChef implements IChef{
prepareDish(dish: string): void {
console.log(`Preparing ${dish}`);
}
}
class OrderService{
// IChefに依存するようになる
constructor(private chef:IChef){
this.chef = chef;
}
placeOrder(dish:string){
this.chef.prepareDish(dish);
}
}
const pizzaChef = new PizzaChef();
const orderService = new OrderService(pizzaChef);
orderService.placeOrder("Pizza");
OrderService
クラスとPizzaChef
クラスはそれぞれに依存し合うのではなく抽象化されたインターフェースであるIChefに依存するようになりました。これにより異なるジャンルの料理を提供するようになってもOrderService
クラスに渡すインスタンスを変更するだけでよくなりOrderService
クラスの実装を変更する必要がなくなりました。
よって、この例は依存性逆転の原則を守れていることになります。
原則に違反した場合はどうなるか
依存性逆転の原則に違反した場合、以下のような影響が出ます。
- 下位モジュールの変更が上位モジュールにも影響する
- 下位モジュールがないと上位モジュールが開発できない
- モジュールの再利用性や拡張性が損なわれる
- ユニットテストが困難になる
このようにモジュール同士が依存していることにより、拡張性や保守性が損なわれた実装になってしまいます。
原則を守った場合のメリット
依存性逆転の原則を守ることで、モジュール間の依存を弱めることができます。すなわち下位モジュールを変更しても抽象が変更されない限り上位モジュールに影響が及ばなくなります。
さらに下位モジュールの切り替えが簡単に行えるようになり、ユニットテストも容易になります。
どうすれば違反しない設計にできるか
ユニットテストを書きやすいかを確認する
依存性逆転の原則を守れている設計はユニットテストが書きやすいことが特徴です。こまめに
テストコードを書くことで各モジュールの依存関係が適切かを確認することができます。
変化しやすいモジュールを参照しない
コード上で変化しやすいモジュールに依存していると、変更のたびにその下位モジュールを修正しなければなりません。(逆も然り)そのことで余分な工数がかかったり、バグを生みやすくなります。逆に言えば、変化する可能性が低いモジュールへの依存は許容しても問題にはなりにくいです。
Dependency Injection(依存性の注入)を行う
依存性の注入(以下: DI)ちはクラス間の依存関係を排除するために、コンストラクタやセッターを通して外部から受け取るようにすることを指します。内部で直接インスタンスを生成するのではなく、外部から受け取ることで依存度を弱めることができます。
先ほどのOrderService
の実装もDIの一例です。
class OrderService{
constructor(private chef:IChef){
this.chef = chef;
}
placeOrder(dish:string){
this.chef.prepareDish(dish);
}
}
この外部から渡すインスタンスの生成が複雑であることが多く、その場合はDIコンテナというものを使って生成の手間を省くことができます。
しかしTypeScriptでは別途ライブラリを使用する必要があります。
下記2つのライブラリが使われることが多いです。
まとめ
SOLID原則の一つである依存性逆転の原則について、初心者なりにまとめました。個人的にはSOLID原則の中で理解するまでに苦労した原則の1つです。
依存性逆転の原則を守ることで再利用性、拡張性が高い設計にすることができユーザーのニーズの素早い変化に対応することができます。また、ユニットテストもしやすくなり実装が複雑化することを防ぐことができます。
以前、英語でもこの依存性逆転の原則について記事を執筆したことがあるので興味のある方はこちらも是非読んでいただけると嬉しいです。
Discussion