依存性注入の基本、newは内部で書かず外から渡す
依存性注入(DI)とは、クラスが必要とする部品を内部でnewせず外から受け取る設計です。実装の差し替えやテストのしやすさを高める書き方です。
クラスの中でnewすると身動きが取れなくなる
DIという言葉だけ聞くと大げさなフレームワークの話に聞こえますが、要は「必要なものを自分で作らず、外から受け取る」というだけの話だと思っています。まずやりがちなのが、クラスの中で依存する部品を直接 new してしまうパターンですね。
class OrderService
{
public function place(array $items): void
{
$mailer = new SmtpMailer('smtp.example.com'); // 中で作っている
$mailer->send('注文を受け付けました');
}
}
これだと OrderService は SmtpMailer に密結合していて、テストのときに本物のSMTPへ接続しようとします。メール送信を差し替える隙間がどこにもないわけです。
コンストラクタで受け取る形に変える
依存を外から渡すようにすると、途端に扱いやすくなります。基本はコンストラクタで受け取る、いわゆるコンストラクタインジェクションですね。引数の型はインターフェースにしておくのがコツで、こうすると実装を後から自由に入れ替えられます。
interface Mailer
{
public function send(string $body): void;
}
class OrderService
{
public function __construct(private Mailer $mailer) {}
public function place(array $items): void
{
$this->mailer->send('注文を受け付けました');
}
}
呼び出し側で new OrderService(new SmtpMailer(…)) と組み立てるだけです。組み立てる場所が一箇所に寄るので、どの実装を使っているか追いやすくなるのも地味な利点だと思います。
テストではモックを差し込める
この形にしておくと、テストでは送信を記録するだけのダミー実装を渡せます。本物のメールサーバを一切叩かずに「送信が呼ばれたか」を検証できるわけですね。
class SpyMailer implements Mailer
{
public array $sent = [];
public function send(string $body): void
{
$this->sent[] = $body;
}
}
$spy = new SpyMailer();
$service = new OrderService($spy);
$service->place(['item']);
assert(count($spy->sent) === 1);
まとめ
DIコンテナを入れるかどうかは規模次第ですが、その手前の「newを外に出す」という習慣だけでも設計はかなり変わります。中で作らず引数で受け取る、型はインターフェースにする、この二つを守るだけでテストの書きやすさが段違いになるという実感があります。まずは一番差し替えたい依存から外に出してみるのがいいと思います。
よくある質問
Q. DIコンテナは必須ですか?
A. 必須ではありません。まずは、newを内部で書かず外から渡す習慣だけでも設計はかなり変わります。
Q. コンストラクタで受け取る引数の型は何にすべき?
A. インターフェースにしておくのがコツです。実装を後から自由に入れ替えられ、テストでダミーを渡せます。
Q. テストではどう差し込みますか?
A. 本物の代わりに、呼び出しを記録するだけのダミー実装を渡せば、外部リソースを叩かずに検証できます。