【SOLID原則】依存性逆転の原則 - DIP
SOLID原則とは、ソフトウェア設計の5つの原則の頭字語を取ったものです。ソフトウェアをより理解しやすく、より柔軟に、よりメンテナナンス性の高いものにするために考案されました。
- 単一責任の原則(Single Responsibility Principle)
- オープン・クローズドの原則(Open/closed principle)
- リスコフの置換原則(Liskov substitution principle)
- インターフェース分離の原則(Interface segregation principle)
- 依存性逆転の原則(Dependency inversion principle)
今回はSOLID原則のひとつ、依存性逆転の原則についてです。
TSで書いたサンプルコードを載せますが、あくまで依存性逆転の原則を理解するためのサンプルとして見て頂ければと思います。
依存性逆転の原則
依存性逆転の原則とは簡単にいうと、抽象に依存させることで依存関係を逆転させることです。
書籍「アジャイルソフトウェア開発の奥義」では、このように書かれています。
a.上位のモジュールは下位のモジュールに依存してはならない。どちらのモジュールも「抽象」に依存すべきである。
b.「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべきである。[1]
importやuseしてモジュールを使う側が、依存する側です。依存する側がモジュールを直接呼び出すのではなく、抽象クラスやインターフェースを使い「抽象」に依存しましょうというのが、この原則です。モジュールというのは、クラスや関数のことです。
次のようにレイヤーを分割した場合を考えます。
表示における関心事を扱うPresentation層は、UIの変更などによりコードの変更頻度が多くなるため、安定性が低く柔軟性が求められます。逆にドメイン知識(ルール・制約)を扱うドメイン層の柔軟性はあまり求められません。
例えば、じゃんけんの勝敗(グーより、パーが強いなど)のルールは、じゃんけんである限り変わらないです。これはじゃんけんにおいて最も本質的なドメイン知識であり、ルールが変わるとじゃんけんではなくなるためです。
このようにシステムの本質である、ドメイン知識(Domain層)は、最も安定性が高くなければなりません。
そしてモジュール間で依存関係が成立する場合は、依存する側は安定性が低く、柔軟性が高くなります。これはモジュールをimportするほど、コードが複雑になるからです。一方で、依存される側は安定性が高く、柔軟性は低くなります。
つまり、Domain層でモジュールをimportして使うことは、安定度を低下させる要因になるため、なるべく避けるべきです。
依存性逆転の原則に違反しているコード
次のコードは、依存性逆転の原則に違反しています。Domain層は、Infrastructure層に依存しています。そしてDomain層からモジュールをimportして、AnimalRepositoryクラスを使っているため、安定度が低下しています。
// Domain/Animal.ts
import { AnimalRepository } from "../Infrastructure/AnimalRepository"
class Animal {
private animalRepository: AnimalRepository
constructor(private id: number) {
this.animalRepository = new AnimalRepository(id)
}
get() {
this.animalRepository.findById()
}
}
const animal = new Animal(1)
animal.get()
// Infrastructure/AnimalRepository.ts
export class AnimalRepository {
constructor(private id: number) {}
findById() {}
}
依存性の注入(DI)
AnimalRepositoryをDIします。それにより、1クラス単位で単体テストができるようになりました。
ただし、この時点では依存関係はまだ変わっていません。
// Domain/Animal.ts
import { AnimalRepository } from "../Infrastructure/AnimalRepository"
class Animal {
constructor(private animalRepository: AnimalRepository) {
this.animalRepository = animalRepository
}
get() {
this.animalRepository.findById()
}
}
const animalRepository = new AnimalRepository(1)
const animal = new Animal(animalRepository)
animal.get()
インターフェースの実装
AnimalRepositoryにインタフェースを実装します。これでどちらのモジュールも「抽象」に依存しました。
しかし、クラス図を見ると依存関係はまだ逆転していません。
// Domain/Animal.ts
import {
AnimalRepository,
IAnimalRepository
} from "../Infrastructure/AnimalRepository"
class Animal {
constructor(private animalRepository: IAnimalRepository) {
this.animalRepository = animalRepository
}
get() {
this.animalRepository.findById()
}
}
const animalRepository = new AnimalRepository(1)
const animal = new Animal(animalRepository)
animal.get()
// Infrastructure/AnimalRepository.ts
export interface IAnimalRepository {
findById(): void
}
export class AnimalRepository implements IAnimalRepository {
constructor(private id: number) {}
findById() {}
}
インターフェースの移動
Infrastructure層に定義していたインターフェースをDomain層に移動しました。クラス図を見ると依存性が逆転している事が分かります。
// Domain/Animal.ts
import { AnimalRepository } from "../Infrastructure/AnimalRepository"
export interface IAnimalRepository {
findById(): void
}
class Animal {
constructor(private animalRepository: IAnimalRepository) {
this.animalRepository = animalRepository
}
get() {
this.animalRepository.findById()
}
}
const animalRepository = new AnimalRepository(1)
const animal = new Animal(animalRepository)
animal.get()
// Infrastructure/AnimalRepository.ts
import { IAnimalRepository } from "../Domain/Animal"
export class AnimalRepository implements IAnimalRepository {
constructor(private id: number) {}
findById() {}
}
Domain層はInfrastructure層の実装に依存しなくなり、寧ろInfrastructure層がDomain層の抽象(インターフェース)に依存するようになりました。これを依存性逆転の原則と言います。
このようにすることでInfrastructure層に変更があっても、Domain層はあまり影響を受けないようになっています。業務ロジックが表示や技術選択といった関心事に依存しないため、強固なアプリケーションを作ることができます。
参考
-
Robert C. Martin. アジャイルソフトウェア開発の奥義 ↩︎
Discussion