DIとは何か?
DIとはなにか?
先日のEC-CUBEのイベントに出てから、自分の発表はDIを解っている前提だったなぁーという反省もあり、DIとは何かを改めて考えていきたいと思いました。
DIとは何かをwikipediaから見てみる
日本語でいうと「依存性の注入」。
Wikiに書いてある説明は以下の通り。
コンポーネント間の依存関係をプログラムのソースコードから排除し、外部の設定ファイルなどで注入できるようにするソフトウェアパターンである
DIを利用したプログラムを作成する場合、コンポーネント間の関係はインタフェースを用いて記述し、具体的なコンポーネントを指定しない。具体的にどのコンポーネントを利用するかは別のコンポーネントや外部ファイル等を利用することで、コンポーネント間の依存関係を薄くすることができる。
分かる人にはわかるけど、多分最初にこれを読んでもさっぱりわからない気がする。
「なるほど、よくわからん」となってしまう気がする。
自分の中での「DIとはなにか?」を考えてみる
「AというオブジェクトがBというオブジェクトを利用する際に、AはBを利用する事(Bが保持しているメソッドやその引数、戻り値など)しか関心がない。
だからBをどのように生成するかに関しては別の部分へ移譲し、生成したBをAに渡す」という感覚で考えています。
つまり「AはBの使い方だけ知っていればよくて、生成に関しては関心を持たない」って事ですね。
Aは機能を使いたいだけで、Bは実はCやDのオブジェクトが必要だとしてもしらねーよって話で、AはBの使い方にしか関心がなくて、Bの生成に関しては他でやってよみたいな感じなのかなぁーと。
この説明もやっぱ何って言っているのかちょっとわからないですよね…。
まあめっちゃ簡単にいうと「俺のほしいオブジェクトよこせ」だけな気もしますが…。
あるオブジェクトから違うオブジェクトを使う時を想像してみよう
具体的なコードを書いて考えて見ましょう。
ユースケースとしては
「ユーザーに通知を送って、送った送らないのトラブルにならないのようにログを残す。」
がベースです。ただ通知はメールやSMSがあって、更に将来的には増える可能性もあります。
SendNotificationというオブジェクトがあるとして、SendNotification は内部にSenderとLoggerというオブジェクトを利用する必要があります。
さらにSenderはMailer,Smsのオブジェクトを利用する必要があります。
コードで書くと以下の通り。
<?php
class SendNotification
{
/**
* @var Sender
*/
private $sender;
/**
* @var Logger
*/
private $logger;
/**
* @var string
*/
private $subject;
public function __constractor(Sender $sender, Logger $logger, string $subject)
{
$this->sender = $sender;
$this->logger = $logger;
$this->subject = $subject;
}
public function run(User $user, string $message)
{
try {
$this->logger->info([
'name' => $user->getName(),
'message' => $message,
'status' => 'before send'
])
$this->sender->send($user, $this->subject, $message);
$this->logger->info([
'name' => $user->getName(),
'message' => $message,
'status' => 'after send'
])
} catch (\Exception $e) {
$this->logger->fail([
'name' => $user->getName(),
'message' => $message,
'status' => 'fail send'
])
throw $e;
}
}
}
class Sender
{
/**
* @var Mailer
*/
private $mailer;
/**
* @var Sms
*/
private $sms;
public function __constractor(Mailer $mailer, Sms $sms)
{
$this->mailer = $mailer;
$this->sms = $sms;
}
public function send(User $user, string $subject, string $message)
{
$this->mailer->send($user->getEmail(), $subject, $message);
if ($user->isSms()) {
$this->sms->send($user->getSmsNumber(), $subject + "\n\n" + $message)
}
}
}
class Logger
{
public function info(array $data)
{
// code...
}
public function fail(array $data)
{
// code...
}
}
class Mailer
{
public function send(string $to, string $subject, string $message)
{
# code...
}
}
class Sms
{
public function send(string $number, string $subject, string $message)
{
# code...
}
}
class User
{
/**
* @var string
*/
private $name;
/**
* @var string
*/
private $email;
/**
* @var string
*/
private $smsNumber = null;
public function getName() :string
{
return $this->name;
}
public function getEmail() :string
{
return $this->email;
}
public function getSmsNumber() :string
{
return $this->smsNumber;
}
public function isSms() :bool
{
return $this->sms === null;
}
}
このSendNotificationを利用する場合に単純にコードを書いたらこんな感じになります。
<?php
class UserRegisteredController
{
public function save()
{
$user = \App\User::findOrFail(1);
// Send Notification
$sms = new Sms();
$mailer = new Mailer();
$sender = new Sender($mailer, $sms);
$logger = new Logger();
$notification = new SendNotification($sender, $logger);
$notification->run($user, '登録が完了しました');
}
}
単純なオブジェクトの構造なのでこれぐらいになりますが、これがもっと複雑なオブジェクトになると考えると。。。
依存性の複雑なオブジェクトの生成をコードで書くのってかなり辛いんですよね。
本来はこんな感じが一番いいわけですよ。
<?php
class UserRegisteredController
{
/**
* @var SendNotification
*/
private $sendNotification;
public function __constractor(SendNotification $sendNotification)
{
$this->sendNotification = $sendNotification;
}
public function save()
{
$user = \App\User::findOrFail(1);
$this->sendNotification->run($user, '登録が完了しました');
}
}
このコードを実現するためには2つ必要になります。
- オブジェクトの生成に関する問題を外部で解決する
- 生成したオブジェクトをコンストラクタでSendNotificationを渡す
ここはLaravelやSymfonyではフレームワークでいい感じに解決できるDIの機構が備わっているわけです。
「オブジェクトの生成に複雑性をコードから排除し、外の世界に任せる」それがDIな気がします。
結果的にテストが書きやすいコードになる気がします。
それは結果的なだけであって目的ではないと思います。
あくまで目的は「生成に関する複雑でめんどくさいアレを外部に任せて、コードは使うことに注力する」ということではないのでしょうか。
DIコンテナについて
DIコンテナはオブジェクトを管理する目的で作られています。
生成のルールから生成されたオブジェクトを管理しする役割がDIコンテナだと思います。
ただし、本来であれば、利用者は意識する必要がない部分だと考えています。
例えば以下のようなコードは間違いだと思います。
<?php
class UserRegisteredController extends Controller
{
/**
* @var SendNotification
*/
private $sendNotification;
public function __constractor(SendNotification $sendNotification)
{
$this->sendNotification = $sendNotification;
}
public function save()
{
$user = \App\User::findOrFail(1);
$container = Container::getInstance();
$notification = $container->get(SendNotification::class)
$notification->run($user, '登録が完了しました');
}
}
これはサービスコンテナから直接呼び出しているコードです。いわいるサービスロケータパターンと言われるものです。
生成に関する関心はありませんが、オブジェクトを利用する側で取得するコードを書いてしまっています。
どんなところからでも$container->get(SendNotification::class)が実行されてしまい、参照されるかわからない状態になってしって非常に怖いですね。
これは$_GLOBALに値を参照しているのと何ら変わりはありません。
利用する側のコードではDIに関する知識が一切ないことが望ましいです。
まとめ
DI関してポリモーフィズムやテストと一緒に語られていることが多い気がします。
以下の点がDIが解決する領域かと今の所自分は考えています。
- オブジェクトの生成に関する問題を解決する仕組みを提供する
- オブジェクトを必要とするオブジェクトに渡す仕組みを提供する
この2つの機能を提供する仕組みがDIと言えるのではないでしょうか?
朝早くに起きてしまって眠れないテンションで書いているので間違っているかもしれませんが、DIがよくわからない人へちょっとでもヒントになったら幸いです。
おやすみなさい。