DIとDIコンテナ、Laravelでの適用

まずはDIとDIコンテナについて。

DI(Dependency injection)とは

  • あるオブジェクトが依存している他のオブジェクトを受け取るデザインパターン
  • DIを使うことで、クライアントはオブジェクトがどう作られるのか、どのクラスが具象化されるのかを気にせずに済む。

DIを使わない例

class Client
{
    /**
     * @var ServiceA
     */
    private $serviceA;
    /**
     * @var ServiceB
     */
    private $serviceB;

    public function __construct()
    {
        // ServiceA、ServiceBの生成方法を知らなければならない
        $this->serviceA = new ServiceA();
        $this->serviceB = new ServiceB();
    }

    public function doSomething(): void
    {
        $this->serviceA->doSomethingA();
        $this->serviceB->doSomethingB();
    }
}

$client = new Client();
$client->doSomething();

Clientクラスは依存するServiceA, ServiceB を自身で生成している状態で、以下のような問題が生じる。

  • 別のサービスに置き換えたい時に、コンストラクタを修正しなければならない
  • ServiceA, もしくはServiceBの生成方法が複雑になった時、コンストラクタも修正しなければならない
  • テストを書く時にモックに差し替えられない

DIを使った例

class Client
{
    /**
     * @var ServiceAInterface
     */
    private $serviceA;
    /**
     * @var ServiceBInterface
     */
    private $serviceB;

    // 渡されたServiceA、ServiceBの具象クラスを使用する。
    public function __construct(ServiceAInterface $serviceA, ServiceBInterface $serviceB)
    {
        $this->serviceA = $serviceA;
        $this->serviceB = $serviceB;
    }

    public function doSomething(): void
    {
        $this->serviceA->doSomethingA();
        $this->serviceB->doSomethingB();
    }
}

$client = new Client(new ServiceA(), new ServiceB());
$client->doSomething();

利用者側で具体的なServiceA、ServiceBクラスを生成して渡すようにしているため、Clientクラスは抽象に依存するだけで済み、上記の問題を解消できた。
しかし、このような手動DIは利用するたびに具象クラスを生成しているため、依存箇所が増えメンテが大変になりスケールしない。
この問題を解決するのがDIコンテナ。

DIコンテナとは

  • DIの機能を提供するフレームワーク
  • 具象クラスの生成をDIコンテナ側の1箇所に集約することで、DIのメンテをしやすくする。

DIコンテナを使った例

シンプルなDIフレームワークであるPimpleをDIコンテナとして使用。

use Pimple\Container;

$container = new Container();

// Clientクラスの生成方法を定義
$container['service_a'] = function ($c) {
    return new ServiceA();
};
$container['service_b'] = function ($c) {
    return new ServiceB();
};
$container['client'] = function ($c) {
    return new Client($c['service_a'], $c['service_b']);
};

$client = $container['client'];  // Clientクラスの利用者側は生成方法を気にせず使える
$client->doSomething();

DIコンテナを使えば、利用者側が使いたいクラスの生成方法を意識せずに済む。

LaravelではDIをどうやるか

LaravelではサービスコンテナがDIコンテナに当たり、Illiminate\Foundation\Application クラスで実装されていて、コンテナへの登録は、サービスプロバイダの中で行う。

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(ServiceAInterface::class, ServiceA::class);
        $this->app->bind(ServiceBInterface::class, ServiceB::class);
        $this->app->bind(
            Client::class,
            function ($app) {
                return new Client($app->make(ServiceAInterface::class), $app->make(ServiceBInterface::class));
            }
        );
    }
}

$client = App::make(Client::class);  // サービスコンテナに登録されたClientインスタンスを返す
$client->doSomething();

App::make(Client::class) でDIコンテナにて登録方法を定義したClientインスタンスを取得できる。
これがLaravelでDIコンテナを使う分かりやすい例だが、Laravelの場合はControllerやCommandクラスのオブジェクトがサービスコンテナにより生成されるため、サービスコンテナを明示的に使ってインスタンスを取得するよりも、タイプヒントを指定することで取得するケースが多い。

class ClientController extends Illuminate\Routing\Controller
{
    private $client;

    public function __construct(Client $client)  // Clientクラスがサービスコンテナにより自動で解決されてインスタンスが渡される
    {
        $this->client = $client;
    }

    public function doSomething()
    {
        $this->client->doSomething();
    }
}

サービスプロバイダで依存関係を登録しておけば、使う側はタイプヒントで指定するだけで済むため非常に楽になる。