Dependency Inversion Principle
上位モジュールは下位モジュールに依存してはならない。両者は抽象に依存すべき。
抽象は詳細に依存してはならない。詳細は抽象に依存すべき。
ソフトウェアの構造を強固にする「レイヤー」の概念。これを支える重要原則です。
SOLID原則と呼ばれる、主要5原則のひとつ。
- Single Responsibility Principle 単一責任原則
- Open Closed Principle 開放閉鎖原則
- Liskov Substitution Principle リスコフ置換原則
- Interface Segregation Principle インターフェース分離原則
- Dependency Inversion Principle 依存性逆転原則
どんなときに使える?
共通モジュールに振り回されるとき
データ送信メソッド、引数追加しましたー
ファイル出力メソッド、戻り値修正しましたー
もう、呼び出してるとこ、また修正しないといけない…
モジュールには、使う側と、使われる側があります。使う側が使われる側に依存している。その時の関係がどうあるべきか、という話です。
上位モジュールと下位モジュールの関係
上位モジュールは使う側、下位モジュールは使われる側
例として、RPGゲームでのセーブで考えます。
ゲームコントローラクラス。ゲームの流れをコントロールするクラスです。
ゲームをいろいろコントロールします
アクセスコントロールクラス。ゲーム進行状況のセーブを行うクラスです。
ゲームの進行状況をセーブします
ゲームコントローラクラスは、アクセスコントロールクラスを呼んでセーブします、このとき、
- 呼ぶ側(ゲームコントローラ):上位モジュール
- 呼ばれる側(アクセスコントロール):下位モジュール
と呼びます。
上位モジュールは下位モジュールに依存する
ゲームコントローラクラスは、アクセスコントロールクラスでセーブします。なのでアクセスコントロールクラスは必要なわけです。
この、相手がないと動かない状態を、依存している、といいます。
上位モジュールは、下位モジュールがないと動かないので、上位モジュールが下位モジュールに依存している、という状態です。
インターフェースをどちらの都合にあわせるか
セーブする、を考えるとき、上位モジュールと下位モジュールの間のインターフェース(なにをやり取りするか)を決める必要があります。
上位モジュールのやりたいことは、とてもシンプル。
セーブがしたい
対して下位モジュールは、いろいろ細かいことをやる必要あります。
○○フォルダの△△ファイルに□□形式でセーブする
どちらの都合に合わせましょうか?
上位モジュールが、下位モジュールに合わせるとします。下位モジュールに必要な情報を渡します。
○○フォルダの、△△ファイルに、□□形式で、セーブする
○○フォルダの△△ファイルに□□フォーマット形式でセーブします
これをいちいちしていると、たいへんなことになります。
上位モジュールは、ほかにもいろいろ下位モジュールを使うはず。下位モジュールにいちいち合わせてインターフェースを決めていたら、きりがないわけです。
インタフェースは抽象にもとづいて決める
この両者の都合は、それぞれ「抽象」「詳細」と言い換えることができます。
- 抽象 ほんとうにやりたいこと。「セーブする」
- 詳細 実際やらないといけないこと。「○○フォルダの△△ファイルに□□フォーマット形式でセーブする」
そして
抽象は、変わりにくい。セーブしたいという要求は、変わらないでしょう。
詳細は、変わりやすい。フォルダの場所やファイル名や形式は、実装にもとづき見直すことが多いでしょう。
であれば、
詳細にインターフェースを合わせるのでなく、ほんとうにやりたいこと=抽象にもとづいて、インターフェースを決める。そうすると、変更の影響を受けにくくなります。
セーブがしたい
セーブします
(○○フォルダの△△ファイルに□□フォーマット形式でセーブしよう)
上位モジュールは、自分のやりたいことに注力できます。
下位モジュールは、抽象にもとづいたインターフェースに従い、自分自身で実際やらないといけないことを判断します。
両者は、抽象に依存する
新たな登場人物「インターフェース」
インターフェースは、抽象に基づき決めたほうが、変更の影響を受けにくい。
でも、相変わらず、上位モジュールが下位モジュールに依存している、のは変わりません。
ここで、「インターフェース」と名乗る登場人物を、新たに登場させます。
セーブします(でも自分ではやりません)
アクセスコントロールクラスの代わりとなる人です。セーブというインターフェースだけ持っているが、実際なにもしない。
そうすると、
- 上位モジュールは、「インターフェース」に「セーブする」をお願いします。本当は誰がやっているか分かりません
- 下位モジュールは、「インターフェース」に「セーブ」の依頼が来たら、代わりにセーブ作業します。本当は誰からお願いされたか分かりません。
こうすると、上位モジュール、下位モジュール、それぞれが「インターフェース」、つまり抽象に依存している、という状態となります。
上位モジュールと下位モジュールは、直接の依存関係がなくなります。
Javaでの実装イメージ。AccessControlInterface が、自分ではなにもしない「インターフェース」。
GameControllerとAccessControlのいずれも、他方を直接使っていません。
import java.util.*;
// インターフェース
interface AccessControlInterface {
public void save();
}
// アクセスコントロール(下位モジュール)
class AccessControl implements AccessControlInterface {
// インターフェースsaveを実装
public void save() {
System.out.println("save");
}
}
// ゲームコントローラ(上位モジュール)
class GameController {
// セーブする
public void save(AccessControlInterface ac) {
// インターフェースに対してセーブを要求
ac.save();
}
}
public class Main {
public static void main(String[] args) throws Exception {
GameController gc = new GameController();
AccessControl ac = new AccessControl();
gc.save(ac);
}
}
依存関係の逆転
さらに、「インターフェース」つまり抽象の部分を、上位パッケージに含めるとします。
上位モジュールが、ほんとうにやりたいことを基準としてインタフェースを決めます。下位モジュールは、インターフェースに従った実際の作業を行います。
これで依存の向きは、下位パッケージから上位パッケージの方向に変わります。依存関係が逆転しました。安定する方向に依存するようになるわけです。
現実にはどうするか?
じゃあ、下位モジュールの分だけ、インターフェースを全部作ればいいんですね!
それは、やりすぎ!
依存関係が逆転できるからといって、下位モジュールのインターフェースをなんでもかんでも用意する…
というのも、現実的にはあまり好ましくありません。
このインターフェースのしかけは、上位モジュールがどの下位モジュールを呼んでいるのか直接分からなくなります。なので処理が追いづらくなるという欠点があるのです。
今後、詳細の変更が見込まれるような場合に限定し、ほどほどに使うのがよいでしょう。
まとめ
依存関係逆転の登場は、下位モジュールに依存するのが当然といった考えを覆す、驚きの考えでした。
ただそれにこだわりすぎると、逆に分かりにくいプログラムとなってしまいます。
大事なのは、この考えです。
ほんとうにやりたいこと=抽象にもとづいて、インターフェースを決めること
この考えでインターフェースを決めたのであれば、無理に仕掛けに拘る必要はないでしょう。
- Single Responsibility Principle 単一責任原則
- Open Closed Principle 開放閉鎖原則
- Liskov Substitution Principle リスコフ置換原則
- Interface Segregation Principle インターフェース分離原則
- Dependency Inversion Principle 依存性逆転原則
コメント