この記事について
この記事は YYPHPアドベントカレンダー22日目の記事となります。
内容としては私が主催しているPHPer向けの勉強会 ぺちオブにて開催した、初心者向けのオブジェクト指向勉強会にて説明した内容を加筆修正したものです。
勉強会自体はSOLID原則を数回に渡って解説したのですが、全て掲載するにはさすがに長過ぎるので、今回は依存性逆転の原則(Dependency inversion principle)について説明した会の内容に絞って記載させて頂いています。それでも少し長いですがお付き合いくださいませ。
本稿の目的
依存性逆転の原則(DIP)はオブジェクト指向設計において切っても切り離せない概念です。
ですが、初学時にはイメージし辛い部分もあったりしますので、本稿はまずそのDIPのイメージをプログラミング関係なく概念的なイメージを持ってもらい、その後そのイメージをプログラミングに落とし込んで頂くことで理解を深めて頂こうという狙いとなっています。
Dependency inversion principle
依存関係をコントロールしよう!
結論だけ先に書きます
まずは結論を先に。(詳細はこれから説明するので理解しなくても大丈夫です)
DIPとは具象に依存するのではなく抽象に依存する事により、依存の方向をコントロールする手法です。
ちょっとこれだけだと意味がわからないですよね。
ここから少しづつ言葉の意味を深掘りしながら紐解いて説明をしたいと思います。
概要を知ろう
依存性逆転の原則。
ソフトウェア開発において『依存』と言う言葉はよく耳にします。
言葉からするとその『依存』とやらを逆転するんだなーくらいしかわかりません。
逆転というと、『上下逆転』、『逆転ホームラン』といった使い方をしますよね。
つまり、前提として対象となる2つの何か
(上の例で言えば、上と下、自チームと相手チーム)が存在し、その関係性が反転する時に使用する言葉でしょう。
この『依存性逆転』も逆転というからには2つの何か
が存在していそうです。
そしてその2つの何かに存在する依存が逆転する原則なのだろうなという事が伺えます。
まずはそこから考えてみましょう。
飲食店を開きます
飲食店を開こうと思います。
目的としては、料理を提供する事によりお客様から代金を頂き収益を上げたいと思っています。
当たり前に感じるかもしれませんがこの飲食店はそれが目的です。
この場合、内装や料理の種類、食器等はあくまで主目的を達成する為のサブ要素と言えます。
ここではホールと厨房に注目してみましょう。
極端な言い方となりますが、先の前提を踏まえると厨房から来る料理はあくまでサブ要素となります。
料理を作る事が目的ではなく収益を上げる事が目的となります。
つまり、ホールにて料理をお金に変える事が最重要となり、厨房で料理を作る事は重要度は低いと言えます。
こうして同じ店内の中で
- お金を生むコアな層。ビジネスの中心(ホール)
- コアな層をサポートする層(厨房)
と2つの層に分けて考える事ができそうです。
2つの何か
が出てきました。
この2つの何か
を中心にもう少し見ていきましょう。
いよいよオープン
この2つの層からなるこのお店ですが、まずは厨房にシェフAを雇ってみました。このシェフはカツ丼を作るみたいです。シェフが作るカツ丼をホールで提供して収益を上げましょう。ホールにカツ丼用の丼を用意し、メニューを作ってさっそくオープンです。
最初のうちは順調に売上も上がっていい感じでした。メインの目的の『収益をあげる』が実現できています。
ところが、不幸な事にシェフAがぎっくり腰になって入院を余儀なくされてしまいました!大ピンチです。
しかたないので厨房にシェフBを雇う事にしました。
このシェフはざるそばを作るようです。
先程買った丼は使えなくなりましたが仕方ありません。ホールにザルとつゆ入れを買ってメニューを一新して営業再開です!
シェフBを雇った後も、シェフAがいた頃よりは少し落ちましたがざるそばも売れてくれています。カツ丼程ではないですが、『収益をあげる』がなんとか実現できています。
ところが・・・。
シェフBが突然海外で勝負したいと退職してしまいました。しかたないので今度は厨房にシェフCを雇うことにしました。
このシェフはカレーを作るみたいです。
ザルとつゆ入れは使えなくなりましたがカレー皿を買ってきてサイドメニューを入れ替えて、、、
ここでオーナーふと思いました。
達成したいメインの目的は収益を上げる事なのに、サブ要素である厨房のシェフが変わる度に、ホールの食器やらメニューを変更しています。
カツ丼時代は凄く繁盛していたのにざるそばになって収益も下がり、今度はカレーを売ることになったけどちゃんと収益はあがるのだろうか。。と。
この2つの層(ホール、厨房)は、厨房で変更がある度に、ホールで提供する料理等の変更が必要な状態となっています。
この状態は、ホール層が厨房層に依存していると言えます。
なぜならば、厨房層の変更がホール層に影響を及ぼしているからです。
あくまでこのお店の目的はホールでお金を頂き、収益を上げる事です。
あのままカツ丼を売っていれば安定して収益があがっていたかもしれないのに、雇ったシェフがその都度作る料理を決めている為、ホールで提供する料理が変わってしまっていメインの目的に影響がでてしまいました。
どうすれば良いのでしょう。
ここでひらめくのがオーナーの才能!
ホールにてオーナーが料理の種類を決める事にしました。
もちろんカツ丼です。
具体的に料理を作るのはシェフですが、何を作ってもらうかはホール側が決めました。『カツ丼を提供する』という契約を満たせるシェフだけを雇い、実際の調理はシェフにしてもらいましょう。
するとどうでしょう。
その後シェフD~シェフZまで様々なシェフに交代して行きましたが、ホール側には一切影響がでません。
それもそのはずです。ホールではカツ丼を売る準備がしてあり、各シェフ達とは『カツ丼を提供する』という契約を結んでいます。もうざるそばもカレーもホールには来ません。
つまり、ホール側はシェフがいくら変わろうとも何も変更する事なくひたすらカツ丼を売り続けていれば良いのです。
シェフの種類を気にする必要はなく、ただ一点『カツ丼を提供する』という契約が満たせているかだけがこの2層間での関心事となりました。
依存関係はどうなったでしょう。
先程までは厨房の変更がホールに影響してましたが、今は違います。
厨房の変更はホールに一切影響を与えていません。逆にホールで決めたメニュー内容がもし変わったら、厨房のシェフを変える必要があるでしょう。
つまり厨房層がホール層に依存している状態になりました。
最初とは真逆の状態になっています。
そう、依存関係が逆転しているのです!
そろそろプログラミングの話しに戻ります
依存関係が逆転する例をプログラミングを離れて説明してみました。
ではこの例をプログラミングに置き換えて見ましょう。
2つのホールレイヤーと厨房レイヤーというものが存在していました。
そしてメインの目的を果たすホールレイヤーが上位レイヤー、厨房レイヤーが下位と言えます。
そして、下位レイヤーをそのまま使用していた際は依存関係が 上位 → 下位の方向に存在してしまっていました。
ですが、上位の作った(カツ丼を提供する)という契約に従った下位を作成する事により、依存関係が逆転したのです。
カツ丼を提供するという振る舞いを持っている事を下位レイヤーに保証させたのです。
そうです。プログラミング的に言うとこれはまさに interface
の振る舞いでしょう。
もう少しプログラミングっぽく言い換えると
シェフインターフェースには、「cookメソッドの返り値がカツ丼クラス」という定義がされており、そしてそのシェフインターフェースを実装したシェフクラス達を入れ替えながら経営する事により依存関係を逆転させた
と言えます。
もう少しわかりやすくクラス図で依存関係の逆転前と逆転後を示してみます。
依存関係逆転前
依存関係逆転前は、ホールは厨房層のシェフAクラスやシェフBクラスに依存しています。
このシェフAクラスとは実際に実装があるクラスなので、具象クラスという呼び方をします。
つまり、ホールは厨房層の具象クラスに依存している状態です。
依存関係逆転後
それに対して依存関係逆転後は、ホールはホール層で定義した抽象(interface)に依存してるだけです。
それによりホールは厨房の具象クラス達の変更を知る必要がなくなりました。
逆に厨房層の具象クラスはホール層の抽象が変更される事があれば影響を受けるでしょう。
ホール層が厨房層とのやりとりをする際に必要な情報を抽象化した事により、この二層間の関心事は抽象のみとなりました。
冒頭に結論として記載した、具象に依存せず抽象に依存するとはこの事を指しています。
最後に具体的上記を具体的なソースコードに落としてみたいと思います。
依存関係逆転前
<?php
namespace 厨房 // 下位レイヤー
class シェフA
{
public function カツ丼を作る(): カツ丼
{
}
}
namespace ホール // 上位レイヤー
class ホール
{
private $シェフ;
public function __construct(シェフA $シェフA)
{
$this->シェフ = $シェフA;
}
public function 料理を提供する(): カツ丼
{
return $this->シェフ->カツ丼を作る();
}
}
// 料理のクラスは割愛。
// クライアントコード
$シェフ = new 厨房\シェフA();
$ホール = new ホール\ホール($シェフ)
$ホール->料理を提供する(); // シェフAが作ったカツ丼が提供される
// シェフがAからBに交代
class シェフB
{
public function ざるそばを作る(): ざるそば
{
}
}
// シェフの変更に合わせて上位レイヤーのホールも修正が必要となる
class ホール
{
private $シェフ;
public function __construct(シェフB $シェフB) //シェフBに修正
{
$this->シェフ = $シェフB;
}
public function 料理を提供する(): ざるそば
{
return $this->シェフ->ざるそばを作る(); // シェフBに合わせて修正
}
}
$シェフ = new 厨房\シェフB();
$ホール = new ホール\ホール($シェフ)
$ホール->料理を提供する(); // シェフBが作ったざるそばが提供される
// 下位レイヤーが変更される度、上位レイヤーの修正が必要となっている
依存関係逆転後
<?php
namespace ホール
Interface シェフ // 上位レイヤーでinterfaceを定義
{
public function 調理する(): カツ丼
{
}
}
class ホール
{
private $シェフ;
public function __construct(シェフ $シェフ) // 定義したinterface に依存した形で型宣言
{
$this->シェフ = $シェフ;
}
public function 料理を提供する(): カツ丼
{
return $this->シェフ->調理する();
}
}
namespace 厨房 // 下位レイヤー
// 上位レヤーにて定められた interface のっとって作成する
class シェフA implements ホール\シェフ
{
public function 調理する(): カツ丼
{
// シェフAなりのカツ丼を作る処理
}
}
class シェフB implements ホール\シェフ
{
public function 調理する(): カツ丼
{
// シェフBなりのカツ丼を作る処理
}
}
// クライアントコード
$シェフ = new 厨房\シェフA();
$ホール = new ホール\ホール($シェフ)
$ホール->料理を提供する(); // シェフAの作ったカツ丼が提供される
// 依存関係が逆転しているので、シェフBに変更してもホールは影響を受けない
$シェフ = new 厨房\シェフB();
$ホール = new ホール\ホール($シェフ)
$ホール->料理を提供する(); // シェフBの作ったカツ丼が提供される
このように上位レイヤーにて抽象化を行い、下位レイヤーがそれに従う事により、依存の方向を逆転する事ができます。
依存関係の方向が制御出来た事により、本来守られるべきコア部分の変更をせずにシェフの変更ができるようになりました。
上位概念から下位概念への依存を発生させない事が出来てはじめて、上位概念を変更しなくてはいけないリスクから遠ざける事ができ、変更容易性を担保する事ができるのです。
なぜなら上位概念はシステムの根幹となるため、変更される可能性が低い層だからです!
変更される可能性が高い下位概念(画面UIや、DBなど永続化など)が変更されたとしても、保護されるべき存在だからです。
DIPまとめ
DIPとは下位概念の具象に依存するのではなく(上位概念によって定義された)抽象に依存する事により、依存性を逆転される原則でした。
最後にもう一つ、より普段目にしやすい例のサンプルコードを記載してイメージをよりプログラミングに寄せて本稿を締めさせて頂こうと思います。
コアとなる層(コアドメイン)からするとサブ要素とも言えるデータの保存先をDIPを使って依存方向を逆転させる事によりコアドメインのソースを修正する事なく切り替えられるようになっています。
開発途中はin memory にデータを保存し、開発が進んだ段階でデータベースに保存するように差し替えられるようになっています。
<?php
class UserRegistrationController extends Controller
{
/**
* UserDB
*/
private $db;
public function __construct(UserDB $db)
{
$this->db = $db;
}
public function register(string $name, string $password): void
{
$this->db->create(new User($name, $password));
}
}
interface UserDB
{
public function create(User $user): void;
}
#-------- ↑ 上位
#-------- ↓ 下位
final class InMemoryDB implements UserDB
{
private $users = [];
public function create(User $user): void
{
$this->users[] = $user;
}
}
final class MySqlDB implements UserDB
{
public function create(User $user): void
{
$mysql = new MySQL();
$mysql->insert('インサート文');
}
}
こうして上位概念が下位概念に依存している際はDIPを使用して依存関係を逆転される事により、より大切な上位概念を守る事ができます。
上位概念が下位概念に依存しない関係を構築した上で、DIにより適切に依存を注入していく事ができるようになります。
まとめのまとめ
やっぱ10分じゃ読めないかも
ちょっと宣伝
2019年もぺちオブではPHPer向けのイベントを随時開催していく予定ですので、ご興味ある方はぜひ遊びにきてください!
YYPHPアドベントカレンダー明日は @AkiraTameto さんの投稿です。
アドベントカレンダーも残す所あと3日!よろしくお願いいたします!