依存関係逆転の原則とは
SOLIDの原則の1つで、次の2つの原則からなります。
A. 上位レベルのモジュールは下位レベルのモジュールに依存すべきではない. 両方とも抽象に依存すべきである.
B. 抽象は詳細に依存してはならない. 詳細が抽象に依存すべきである. (wikipedia)
うーん、わからん
実際にコードで書いてみよう
下記のプログラムは「アジャイルソフトウェア開発の奥義」に出てくるコピープログラムです。
実装
class Copy
{
/**
* @var string
*/
private $data;
public function read(): void
{
$reader = new KeybordReader();
$this->data = $reader->read();
}
public function output(): void
{
$console = new ConsoleOutput();
$console->output($this->data);
}
}
このプログラムは、キーボードからの入力をコンソールに出力します。
Copyクラスからは、KeybordReaderクラスと ConsoleOutputクラスを使っています。(ここ重要)
使っているということはCopyクラスの方が2つのクラスより上位にいるということです。
ただしこの場合、CopyクラスはKeybordReaderクラスとConsoleOutputクラスに依存しています。
どう依存しているか
まず、2つのクラスのメソッド名が変更された時点で、Copyクラスで書かれているメソッド名も変更しなければいけません。
また、KeybordReaderクラスの返り値の型や、ConsoleOutputクラスの引数の型が変われば
それに合わせてCopyクラスの処理も変更する羽目になります。
こんな感じ
class Copy
{
/**
- * @var string
+ * @var array
*/
private $data;
public function read(): void
{
$reader = new KeybordReader();
//文字列ではなく配列で返してくるようになった
$this->data = $reader->read();
}
public function output(): void
{
$console = new ConsoleOutput();
- $console->output($this->data);
//outputの引数はstrのままなので配列から文字列に変換
+ $str = //変換処理
//outputからechoに変更
+ $console->echo($str);
}
}
KeybordReaderクラスやConsoleOutputクラスの変更、つまり下位クラスの変更で上位クラスであるCopyも変更しなければならなくなっています。
A. 上位レベルのモジュールは下位レベルのモジュールに依存すべきではない.
に反しています。
依存をなくすために矢印の向きを逆にする
ここからが本題です。
現状だとこう
Copyから他のクラスに対して矢印が向かっています。
なら、矢印を逆にすればCopyは依存しなくなる...?
こうしたい
ただこうしたら、今度はCopyに依存しています。
どうしよう
ここで原則を見てみます
A. 上位レベルのモジュールは下位レベルのモジュールに依存すべきではない. 両方とも抽象に依存すべきである.
B. 抽象は詳細に依存してはならない. 詳細が抽象に依存すべきである. (wikipedia)
抽象に依存すべきと書いています。
抽象といえばインターフェースですね。
原則に則り、Copyをインターフェースに依存させるようにしましょう。
インターフェース
Copyはインターフェースに依存し、2つのクラスはそれぞれReaderInterfaceとOutputInterfaceを実装しています。
interface ReaderInterface
{
public function read(): string;
}
interface OutputInterface
{
public function output(string $str): void;
}
この時に重要なのは2つのインターフェースの所有者がCopyということです。
インターフェースを指定しているということは、引数や返り値をCopyクラスが指定しているということです。
つまり上位クラスが宣言したインターフェースに対して下位クラスが実装するようになります。
class Copy
{
/**
* @var string
*/
private $data;
public function read(ReaderInterface $reader): void
{
$this->data = $reader->read();
}
public function output(OutputInterface $outputter): void
{
$outputter->output($this->data);
}
}
インターフェースを使うことで、下位クラスへの依存がなくなりました。
ただし、インターフェースには依存しているのでインターフェースが変更されればCopyクラスの変更は必要になります。
ですが、インターフェースを変更する時は、Copyクラスを変更する時のはずです。
上位クラスでの変更が下位クラスに及ぶのは、別に原則には違反していません。
現在の実装はこうなっています。
ReaderInterfaceとOutputInterfaceはCopyクラスによって宣言(所有)されています。
矢印の向きも変わり、依存関係が「逆転」しました。
モノで例えると
Macとマウスがあります。
マウスにはインターフェースとして USB Type Aがついています。MacとマウスはUSB Type Aで繋がっています
ある日、MacがインターフェースをUSB Type AからUSB Type Cに変えると言いだしました。
この時、当然マウスはType Aでは使ってもらえないのでAからCへ変更
することになります。
インターフェースが変更されれば、そのインターフェースを使っているマウスはそれに合わせて変更しなければいけません。
ですが、マウスがインターフェースを変える(USBを三角の形にする!)と言いだしても
Macからすれば自分に合わせられないなら使わんからいいよというだけなのです。
まとめ
依存関係逆転の原則の「逆転」とは、依存の向きと所有権を逆にすること。
色々書いてわかりにくくなった感があるので、また修正していきます...
参考
アジャイルソフトウェア開発の奥義