突然ですが 、DI(Dependency Injection)ってご存知ですか?

最近は常識になりつつある感じがしますが、実際に有効活用したことはないという方や、そもそも何?という方も多いのではないでしょうか?

今回の記事では決済の実例を元に「DIを有効活用する方法」についてご紹介しようと思います。

決済に関しては、以前ご紹介したPAY.JPの決済サービスを元に紹介していきます。
(PAY.JPの実装に関しては別の記事を参考にしてください。)

使用するLaravelの機能

  • サービスコンテナ
  • サービスプロバイダー

そもそもDIって何?

DIとはよく「依存性の注入」と言われます。

これだけでは絶対分かりませんよね。。

別の言葉を使って説明すると、「クラス内で別クラスを利用するときにクラス内で直接newせず、外部から引数で渡してあげて利用する」ということです。

実際にコードをみてみましょう。

/**
 * 決済関連のクラス
 */
class Payment
{
    // 金額を返すメソッド
    public function amount()
    {
        return 1000;
    }
}

/**
 * 請求書生成のクラス
 */
class Invoice
{
    // 依存注入していない例
    public function notUseDi()
    {
        $payment = new Payment();
        return '今月の請求金額は' . $payment->amount() . '円です';
    }

    // 依存注入している例
    public function useDi(Payment $payment)
    {
        return '今月の請求金額は' . $payment->amount() . '円です';
    }
}

$invoice = new Invoice();
$invoice->notUseDi();           // 今月の請求金額は1000円です
$invoice->useDi(new Payment()); // 今月の請求金額は1000円です

簡易的ですが、Paymentクラスのamountは決済金額を計算して返すクラスです。

一方で、InvoiceクラスはPaymentクラスを使って請求書を作成するクラスです。

依存注入していないnotUseDiメソッドでは、メソッド内でPaymentクラスをnewしています。

依存注入しているuseDiメソッドでは、引数でPaymentクラスのインスタンスを渡しています。

結果はどちらも同じになります。

「結果が同じならどっちでもいいじゃん?」となりそうですが、場合によって依存注入していない方では不便な時があります。

それは、「Paymentクラスではなく別のクラスを使いたい」場合です。

例えば単体テストを書きたい場合などです。まだ決済機能がなかったり、実際の決済データがない場合、けどテストはしたい!というときにダミーのデータを使うとします。

ダミーデータはPaymentクラスを継承したStubPaymentクラスを利用します。

/**
 * 決済関連のクラス
 */
class StubPayment extends Payment
{
    // 金額を返すメソッドをオーバーライド
    public function amount()
    {
        return 2000;
    }
}

依存注入していないnotUseDiメソッドではメソッド内で直接Paymentクラスを利用しています。これを「InvoiceクラスはPaymentクラスに依存している」と言います。

Paymentクラスに依存しているため、StubPaymentを利用したい場合はPaymentを書き換えるしかありません。メソッド内を書き換えてしまうとテストの意味がありません。

一方で、依存注入しているuseDiメソッドでは外部からPaymentオブジェクト型のインスタンスを渡しています。

この場合Paymentクラスだけでなく、Paymentクラスを継承した子クラス、Payment抽象クラスやPaymentインターフェースを実装したクラスも外部から渡すことができます。

つまり以下のように記述が可能になります。

$invoice = new Invoice();
$invoice->useDi(new StubPayment()); // 今月の請求金額は2000円です

これで、メソッド内を書き換えることなくuseDiの単体テストを実行することができます。

DIを利用することによって、他のクラスに依存せず変更に強いプログラムになります。

決済機能での実例

では実際にPAY.JP決済機能の実装において、DIを活用してみたいと思います。

今回の想定として、決済サービスの本番申請中だけど実装を進めたい!という場面で、PAY.JPでの実装は審査通過後にするとして、まずはダミーを作って実装を進めようと思います。

まずは、決済サービス用のPaymentインターフェースを作成します。

<?php

namespace App\Libraries\Payment;

/**
 * 決済の実装
 */
interface PaymentInterface
{
    /**
     * 決済の実行
     * @param string $token トークン
     * @param int $amount 金額
     * @param string $currency 決済通貨(jpyなど)
     * @return string charge_id 決済ID
     */
    public function createCharge(string $token, int $amount, string $currency): string;
}

ここのcreateChargeは決済を実行するメソッドです。ダミーの決済クラスやPAY.JP決済クラスにこのインターフェースを実装します。

まずは実際のPAY.JPで決済をするクラスを見てみましょう。

<?php

namespace App\Libraries\Payment;

use Payjp\Charge;
use Payjp\Payjp;

class PayjpPayment implements PaymentInterface
{
    public function __construct()
    {
        Payjp::setApiKey(config('payjp.secret_key'));
    }

    /**
     * 決済の実行
     * @param string $token
     * @param int $amount
     * @param string $currency
     * @return string charge_id
     */
    public function createCharge(string $token, int $amount, string $currency = 'jpy'): string
    {
        $charge = Charge::create(array(
            "card"     => $token,
            "amount"   => $amount,
            "currency" => $currency,
        ));
        return $charge->id;
    }
}

インターフェースで宣言されているメソッドが実装されており、ここで実際に決済を実行しています。

では次に、ダミーの決済クラスStubPaymentを作成します。

<?php

namespace App\Libraries\Payment;

/**
 * ダミーの決済クラス
 */
class StubPayment implements PaymentInterface
{
    /**
     * 決済の実行
     * @param string $token
     * @param int $amount
     * @param string $currency
     * @return string charge_id
     */
    public function createCharge(string $token = '', int $amount = 1000, string $currency = ''): string
    {
        return 'ch_' . uniqid();
    }
}

StubPaymentも同様にインターフェースのcreateChargeを実装していますが、実際に決済を行うわけではなく、単にランダムな文字列を返しているだけです。

ではビジネスロジックを実装してみましょう。

<?php

namespace App\Services;

use App\Libraries\Payment\PaymentInterface;
use App\Models\Payment;

class PaymentService
{
    public function __construct(private readonly PaymentInterface $payment_interface) {}

    /**
     * 決済の実行
     * ※注意:エラーハンドリングは省略しています。
     * @param array $params
     * @return Payment
     */
    public function createCharge(array $params): Payment
    {
        $charge_id = $this->payment_interface->createCharge($params['token'], 1000);
        // DBに決済履歴を保存
        return Payment::create([
            'user_id'   => auth()->id(),
            'charge_id' => $charge_id,
        ]);
    }
}

コンストラクタでPaymentInterface型のオブジェクトを受け取って利用しています。ここが今回の肝になります。仮実装段階ではDIを使って、StubPaymentを依存注入するわけです。

Controllerは下記のような感じです。

<?php

namespace App\Http\Controllers\Sample;

use App\Http\Controllers\Controller;
use App\Services\PaymentService;
use Illuminate\Http\Request;

/**
 * 決済のサンプルコード
 */
class PaymentController extends Controller
{
    public function __construct(private readonly PaymentService $paymentService) {}

    /**
     * 決済の実行
     * @param Request $request
     * @return void
     */
    public function store(Request $request)
    {
        $this->paymentService->createCharge($request->all());
    }
}

あれ?StubPaymentはどこで引数に渡しているの?と思われる方もいるかと思いますが、これはLaravelのサービスコンテナという機能で設定しています。

<?php

namespace App\Providers;

use App\Libraries\Payment\PayjpPayment;
use App\Libraries\Payment\PaymentInterface;
use App\Libraries\Payment\StubPayment;
use Illuminate\Support\ServiceProvider;

class PaymentServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        // envにpayjpの設定の有無によってインスタンスを変更
        if(config('payjp.secret_key')){
            // PayjpPaymentを注入
            $this->app->bind(PaymentInterface::class, function ($app) {
                return new PayjpPayment();
            });
        } else {
            // StubPaymentを注入
            $this->app->singleton(PaymentInterface::class, function ($app) {
                return new StubPayment();
            });
        }

    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

もしも.envにPAY.JPの設定がされていたら、PayjpPaymentがインジェクションされて、設定がない場合はStubPaymentが注入されます。

しかし、どちらにしろ決済クラスを利用しているPaymentServiceはそれぞれのクラスに依存していないため、共通の書き方となります。

ここでもしもDIされていない場合、PaymentServiceのメソッド内に直接new PayjpPayment()やnew StubPayment()をしなければならず、変わった場合に書き換えが必要になってしまいます。

しかしDIを使えば、仮にPAY.JPの審査が落ちたとしても、PaymentInterfaceを実装した新しい決済クラス(例えばStripePayment)を作るだけでよく、PaymentServiceを書き換える必要はありません。

これがDIの利点となります。もちろんテストでも大活躍です。

DIのデメリット

決済での実例を見てきましたが、上記の例ではデメリットもあります。

一番のデメリットは、エディタでPaymentServiceから$this->payment_interface->createCharge()のクラス元に飛ぼうとするとインターフェースのファイルに飛んでしまうため、実際の処理でどのクラスが使われているのかすぐに判断できないというのがあります。

インターフェースやサービスコンテナの知識のないメンバーが多い組織で利用するのは多少リスクになりそうです。

まとめ

さて、今回は依存性の注入(DI)について解説してみました。

おさらいですが、DIとは「クラス内で別クラスを利用するときにクラス内で直接newせず、外部から引数で渡してあげて利用する」ということです。

DIの利点は以下の点になります。

  • 他のクラスに依存しないため、再利用しやすくなる。
  • 単体テストで難しいテストも簡単にできるようになる
  • 未実装のクラスの代わりを作って実装を進められる(チーム開発でなどで便利)

ここで紹介したこと以外にも活用事例はあると思うので、ぜひDIを活用してみてください。

参考

DI (依存性注入) って何のためにするのかわからない人向けに頑張って説明してみる

この記事をシェアする