この記事の目的
Dependency Injection(DI)の基本的な考え方と必要性を、具体例(決済処理)を通して理解することを目的としています。
「なぜDIが必要なのか」「使うと何が良くなるのか」 を、実務でイメージできるレベルで理解できる状態を目指します!
対象者
- DIという言葉は聞いたことがあるが、よく分かっていない方
- クラス設計で「とりあえず new している」状態の方
- 外部サービス(APIなど)を使った実装で設計に悩んでいる方
- テストしづらいコードに課題を感じている方
- オブジェクト指向や設計の理解を深めたいエンジニア
Dependency Injection(依存注入)とは?
設計手法の一つで、簡潔に言えば
クラスが依存するオブジェクトを自身で生成せずに、外部から注入する設計手法
です。(これだけじゃ分かりづらいので、具体例で説明します)
もうちょい詳細に!
ビジネスロジックがオブジェクトを呼び出す際に、自身で生成してそのまま使用するのではなく、外部から注入することでオブジェクトへの依存度を下げよう
ということ!
まだ全く意味がわからなくても問題ありません!
ある外部サービスを用いた決済の実装例
とある支払い処理を行うビジネスロジックがあったとします。
支払い方法は PayPay, Stripe, そのほか決済サービス どれを使うか悩んでいるし、簡単に切り替えられるようにしたい!
しかもテストの時にはモックを使用して支払えたことにしたい!
そんな時、ある外部サービスを用いてビジネスロジックでこんな感じで書いたとする。
class OrderService
{
public function checkout()
{
$payment = new KessaiClient();
$payment->kessai(1000);
}
}
なにも問題ないはず。
この外部サービスのドキュメントには
KessaiClientクラスのkessaiメソッドで支払い処理を行う
この実装でできるって書いてたんだから…。
問題点
しかしこれは「テスト」「PayPay」の際にはどうでしょう?
少なくともkessai()なんてメソッドは用意されていないだろうし、呼び出すクラス名も全く違うはずです。
これではテストや支払い方法変更の度にビジネスロジックを書き換える必要がある。
ビジネスロジックを書き換えたらそれはテストとして成立しているとは言えないですね…。
依存関係
この状態は図で表すと以下のようにビジネスロジックが外部サービスに強く依存している形となります。
OrderService → KessaiClient
DIを意識した実装
今回のDIを意識して実装してみましょう!
支払いのビジネスロジックと外部サービスの橋渡しを行うInterfaceを作成して…
◼︎ビジネスロジック
class OrderService
{
public function __construct(PaymentGateway $payment)
{
$this->payment = $payment;
}
public function checkout()
{
$this->payment->pay(1000);
}
}
■ Interface … 外部サービスの橋渡し
interface PaymentGateway
{
public function pay(int $amount): bool;
}
◼︎Stripe … 実際に決済を行うクラス
class StripePayment implements PaymentGateway
{
public function pay(int $amount): bool
{
// Stripe SDKを使って決済
$stripe = new StripeClient('API_KEY');
$charge = $stripe->charges->create([
'amount' => $amount,
'currency' => 'jpy',
'source' => 'tok_sample',
]);
return $charge->status === 'succeeded';
}
}
■コンテナ …「PaymentGatewayが必要なときにStripePaymentを生成して渡す」というルールを定義
$this->app->bind(
PaymentGateway::class,
StripePayment::class
);
このようにコードは少し増えますが、これにより「変更に強い設計」になっています!
では、何が変わったのでしょうか?
- 外部サービスを抽象化したInterfaceを定義する
- ビジネスロジックはそのInterfaceを通して処理を呼び出す
各決済サービスごとの違いをInterfaceが吸収することで、ビジネスロジックは常にpay()を呼べば支払いをできるようにしてくれたよ!
ということです。
ビジネスロジック側では「支払いできる」という契約だけ知っているが何を使用しているのか知らないし、関係ない
これがDIによって実現される設計です。
依存関係は以下のようにビジネスロジックに外部サービスのオブジェクトが依存するようになります!
OrderService → PaymentGateway ← StripePayment
OrderServiceはPaymentGatewayに依存し、StripePaymentはその実装として紐づいています。
これならビジネスロジックの変更を行わずに外部サービスを切り替えられるし、テストも行える!
他のサービスで支払いたい時はどうするの?
同じように外部サービスを抽象化したオブジェクトを作成し、コンテナで紐づけるだけです!
■ PayPay
class PayPayPayment implements PaymentGateway
{
public function pay(int $amount): bool
{
// PayPay APIを呼び出し
$client = new PayPayClient('API_KEY');
$response = $client->createPayment([
'amount' => $amount,
'currency' => 'JPY',
]);
return $response->isSuccessful();
}
}
◼︎コンテナ
$this->app->bind(
PaymentGateway::class,
PayPayPayment::class
);
まとめ
DIとは「依存を外から渡す設計」であり、その目的は「変更に強いコードを作ること」にあります。
例えば決済システムを利用する場合、ビジネスロジックの中で直接オブジェクトを生成してしまうと、そのクラスは特定の外部サービスの実装に依存してしまいます。
その結果、テストを行う際や別の決済サービスへ変更する際に、ビジネスロジック側のコードまで書き換える必要が出てしまいます。
DIを採用することで、ビジネスロジックは具体的な実装ではなく interface(抽象)に依存する設計にできます。
そしてDIコンテナなどで interface と実装クラスを紐付けることで、実装を差し替えてもビジネスロジックを変更する必要がなくなります。
DIのメリット
- クラス同士の結合度を下げられる(疎結合になる)
- 実装を差し替えてもビジネスロジックを変更しなくてよい
- テストがしやすくなる(モックに置き換え可能)
- 変更に強い設計となる(拡張しやすい)
- 依存関係が明確になり、コードの見通しが良くなる
DIで「やらないこと」
- クラスの中で勝手に依存を生成しない(newしない)
- 「Stripe専用」など特定の実装にベタ依存しない
- APIの呼び出し方法などの詳細をビジネスロジックに書かない
- テストのたびに実装を書き換えるような設計にしない
- 依存関係が見えないブラックボックスなクラスにしない
補足
この設計は SOLID 原則の一つである
Dependency Inversion Principle(依存関係逆転の原則)
にも基づいています。
DIはあくまで「依存を外から渡すための手段」であり、その目的は「変更に強い設計(疎結合)」を実現することにあります。