依存性逆転の実践
依存性逆転でアダプターをコアに接続します。
「依存性逆転の実践」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。
依存性逆転と依存性注入
この2つの異なる考え方は、混同されがちです。依存性注入は技法です。依存するオブジェクトを自分で生成するのではなく、外部から渡します。依存性逆転(SOLIDのD)は原則です。高レベルの方針と低レベルの詳細がともに抽象に依存し、その抽象を高レベルのモジュールが所有します。このレッスンでは、コアがインターフェースを定義し、具体的なアダプターをそこに接続することで、後者を実現します。
原則を正確に理解する
DIPは次のように定めています。
- 高レベルモジュールは低レベルモジュールに依存してはいけません。両者は抽象に依存します。
- 抽象は詳細に依存してはいけません。詳細が抽象に依存します。
重要な点は、インターフェースを実装者ではなく利用者が所有することです。ドメインがPaymentGatewayを宣言し、Stripeアダプターがそれに準拠します。逆ではありません。
コアで抽象を定義する
インターフェースは、それを必要とするコードの近くに置き、ドメインの用語で表現します。Stripeの型を漏れ込ませてはいけません。
<?php
// src/Billing/Application/Port/PaymentGateway.php
interface PaymentGateway
{
public function charge(Money $amount, CardToken $token): ChargeId;
}
final class Money {
public function __construct(public readonly int $cents, public readonly string $currency) {}
}
final class CardToken { public function __construct(public readonly string $value) {} }
final class ChargeId { public function __construct(public readonly string $value) {} }アダプターを抽象に準拠させる
インフラストラクチャのアダプターはコアのインターフェースを実装し、ベンダーSDKへの変換を行います。依存関係の矢印は、アダプター(詳細)から抽象(方針)に向かいます。これで逆転が実現します。
<?php
final class StripePaymentGateway implements PaymentGateway
{
public function __construct(private \Stripe\StripeClient $stripe) {}
public function charge(Money $amount, CardToken $token): ChargeId {
$intent = $this->stripe->paymentIntents->create([
'amount' => $amount->cents,
'currency' => strtolower($amount->currency),
'payment_method' => $token->value,
'confirm' => true,
]);
return new ChargeId($intent->id);
}
}コンストラクター注入をデフォルトにする
コンストラクター経由で注入し、抽象を型ヒントに指定してください。依存関係が明示的かつ不変になり、渡し忘れも起こりません。必須の依存オブジェクトには、セッター注入やプロパティ注入を避けてください。構築途中の不完全なオブジェクトを許してしまうためです。
<?php
final class CheckoutService
{
public function __construct(
private PaymentGateway $payments, // abstraction, not StripeClient
private OrderRepository $orders,
) {}
public function pay(OrderId $id, CardToken $token): ChargeId {
$order = $this->orders->get($id);
$charge = $this->payments->charge($order->total(), $token);
$order->markPaid($charge);
$this->orders->save($order);
return $charge;
}
}コンポジションルート
具体的な接続はすべて、main()やエントリーポイントにできるだけ近いコンポジションルートという1か所で行います。それ以外の場所でインフラストラクチャをnewしてはいけません。Stripeの存在を知っているのはこの場所だけです。
<?php
// public/index.php — composition root
$stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET'));
$gateway = new StripePaymentGateway($stripe);
$orders = new PdoOrderRepository(new PDO(getenv('DB_DSN')));
$checkout = new CheckoutService($gateway, $orders);
// Everything below depends only on abstractions
$controller = new CheckoutController($checkout);DIコンテナで接続する
複雑なアプリケーションでは、コンテナ(PHP-DI、Symfony)を使って接続を自動化します。重要なのは、インターフェースを実装にバインドすることです。オートワイヤリングは型に基づいてコンストラクターを解決するため、インターフェースとクラスの対応表を宣言するだけで済みます。
<?php
use function DI\autowire;
use function DI\get;
return [
PaymentGateway::class => autowire(StripePaymentGateway::class),
OrderRepository::class => autowire(PdoOrderRepository::class),
\Stripe\StripeClient::class => fn() => new \Stripe\StripeClient(getenv('STRIPE_SECRET')),
PDO::class => fn() => new PDO(getenv('DB_DSN')),
];アダプターの交換で原則を確かめる
コアがPaymentGatewayだけに依存しているため、プロバイダーの切り替えやオフラインでのテストは、接続を1行変更するだけで行えます。これはユニットテストで使うフェイクの例です。CheckoutServiceは変更されず、Stripeをインポートすることもありません。
<?php
final class FakeGateway implements PaymentGateway {
public array $charges = [];
public function charge(Money $a, CardToken $t): ChargeId {
$this->charges[] = $a;
return new ChargeId('ch_test_' . count($this->charges));
}
}
$fake = new FakeGateway();
$id = $fake->charge(new Money(2500, 'EUR'), new CardToken('tok_visa'));
echo $id->value, ' charges=', count($fake->charges), PHP_EOL; // ch_test_1 charges=1サービスロケーターの罠を避ける
コンテナ自体を注入し、メソッドの内部で依存関係を取り出すのは、サービスロケーターアンチパターンです。依存関係を隠し、型チェックを無効にし、コードをコンテナに再び結合させてしまいます。必要なものを明示的に注入してください。
<?php
// ANTI-PATTERN: hidden dependencies, container leaks everywhere
final class BadCheckout {
public function __construct(private ContainerInterface $c) {}
public function pay($id, $token) {
$gateway = $this->c->get(PaymentGateway::class); // hidden!
// ...
}
}コンテナを置いてよい場所
コンテナを置いてよい層は1つだけです。それはコンポジションルートと、その周辺のフレームワーク連携部分(例:コントローラーファクトリー)です。ドメインとアプリケーションのクラスはコンテナから独立させ、コンストラクター経由で通常のオブジェクトを受け取り、手動でもインスタンス化できるようにしてください。よい確認方法は、コンテナなしで1つのPHPファイルにアプリケーション全体を接続できるかどうかです。できるなら、依存関係を正直に表現できています。
サービスロケーションを使わずに遅延接続する
依存関係の構築にコストがかかる場合や、条件によってのみ必要になる場合があります。コンテナを注入したくなっても、代わりにファクトリークロージャを注入してください。依存関係は明示的かつ型付きのまま、実際に使うまで構築を遅らせられます。
<?php
final class ReportService
{
/** @param Closure():PaymentGateway $gatewayFactory */
public function __construct(private Closure $gatewayFactory) {}
public function refundIfNeeded(bool $needed): void {
if (!$needed) return;
$gateway = ($this->gatewayFactory)(); // built only when required
// $gateway->charge(...) etc.
}
}
// Composition root supplies the factory, not the container
$svc = new ReportService(fn() => new StripePaymentGateway($stripe ?? null));
echo 'lazy dependency wired', PHP_EOL;簡単な確認
PaymentGatewayインターフェースは誰が所有すべきですか?
まとめ
アダプターを正しい方法でコアに接続できました。
- 依存性逆転と依存性注入は異なります。注入は仕組みであり、逆転とは利用者が抽象を所有することです。
- コアがインターフェースを定義し、インフラストラクチャのアダプターがそれに準拠します。
- 抽象をコンストラクター注入し、具体的な接続は1つのコンポジションルート(またはインターフェースとクラスをバインドするコンテナ設定)でのみ行います。
- アダプターの交換やテストでのフェイクへの切り替えを、1行の変更で行えるようになります。
- サービスロケーターアンチパターンを避け、コンテナはエッジにだけ置いてください。
よくある質問
「依存性逆転の実践」レッスンは無料ですか?
はい。「依存性逆転の実践」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。
「依存性逆転の実践」で何を学びますか?
依存性逆転でアダプターをコアに接続します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
PHP Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。
「依存性逆転の実践」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このPHP Academyレッスンでコードを書いて実行できますか?
はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。