OpenWork Tech Blog

社員クチコミサービスを運営しているオープンワークエンジニアによるテックブログです。

DIって、本当にいいものですね(SymfonyのAutowiringについて)

はじめまして、Webアプリチームの永田です。

今回はSymfonyでDIを行うための機能、Autowiringについてお話ししようと思います。

DI, Dependency Injectionって何?という方は、まずそちらをググってみてください。分かりやすい記事がたくさんあります。

この記事では、SymfonyのAutowiringのような機能を使った場合に得られるメリットにフォーカスしたいと思います。

言語問わず他の多くのアプリケーションフレームワークでも同様の機能は存在するので、Symfonyを使っていない方にも参考になるかなと思います。

Autowiringとは?

ドキュメントを見るのが早いですが、一応説明いたします。

要は、クラスの依存クラスを勝手に作ってくれる凄まじく便利な機能です。唐突ですが、以下のようなメロンパン工場クラスがあったとしましょう。

<?php

class MelonPanFactory
{
    private Mixer $mixer;
    private Oven $oven;
    private ManufacturingLogger $logger;

    public function __construct(Mixer $mixer, Oven $oven, ManufacturingLogger $logger) {
        $this->mixer = $mixer;
        $this->oven = $oven;
        $this->logger = $logger;
    }

    public function create(Flour $flour, Water $water, Sugar $sugar): MelonPan {
        $dough = $this->mixer->mix($flour, $water, $sugar);
        $melonPan = $this->oven->bake($dough);
        $this->logger->log('メロンパンをひとつ作ったよ');

        return $melonPan;
    }
}

これをメロンパン作成コントローラーから呼び出す場合、Autowiringを利用すればこのようにできます。

<?php

class CreateMelonPanController extends AppController
{
    public function createAction(Request $request, MelonPanFactory $melonPanFactory): Response {
        $melonPan = $melonPanFactory->create(new Flour(), new Water(), new Sugar());
        ~省略~
        return $response;
    }
}

不思議なことに、リクエストが飛んできた時点で工場ができています。工場とその依存クラスのコンストラクタを呼ぶ必要がないので、すごくスッキリしています。(Autowiringの設定方法は上記ドキュメントをご覧ください。)

Autowiringをすることで得られるメリット

メロンパン工場クラスの例を使い説明します。

コンストラクタの呼び出しがなくなり、コードが綺麗になる

そのままなので省略します。

Autowiringを意識することで、設計が改善する

例えば、オーブンクラスの設計が以下のようになっていたらどうでしょう?

<?php

class Oven
{
    public function __construct(Dough $dough) {
        $this->dough = $dough;
    }

    public function bake(): MelonPan {
        new MelonPan($this->dough->baked());
    }
}

これではAutowiringが機能しなくなってしまいます。

リクエストが飛んできた時点でSymfonyはメロンパン工場クラス、及びその依存クラスを作ろうとします。しかし、その段階ではまだ生地(Dough)ができていないため、オーブンクラスを作ることができません!

上記の設計は意味を考えてもおかしいです。生地は、オーブンに最初から入っているものでしょうか?違います。生地を焼こうと思った時に、オーブンに生地を入れるのが普通です。

Autowiringを意識することで、メソッドの引数に渡すべきものはメソッドの引数に、コンストラクタでDIすべきものはコンストラクタの引数に、という設計に自然となります。

ファンクショナルテストの際に、サービスをテストダブルに置き換えられる

メロンパンの製造ログを出力していることをテストしたいとしましょう。以下のように、製造ロガークラスをMockにしてサービス定義を上書きすることができます。便利ですね。

<?php

class CreateMelonPanControllerTest extends WebTestCase
{
    public function testCreateAction_製造ログを出力する(): void {
        $loggerMock = $this->createMock(ManufacturingLogger::class);
        $loggerMock
            ->expects(self::once())
            ->method('log')
            ->with('メロンパンをひとつ作ったよ')
        ;
        self::$container->set(ManufacturingLogger::class, $loggerMock);

        $client = self::createClient();
        $client->request('POST', '/melon_pan');
    }
}

もしAutowiringを使っていなかった場合(メロンパン作成コントローラーの中で製造ロガークラスをnewしていた場合)、テストができません!

他にも例えば、外部APIへのリクエストを行なっているクラスなんかをStubに置き換えたいときも同様のことができます。

最後に

SymfonyのAutowiringは、便利なだけではなく設計や自動テストの改善にもつながります。ぜひ活用していきましょう。

また、弊社ではエンジニアを募集しています。メロンパン、ではなく設計やリファクタリングに関心のある方大歓迎です。是非ご検討ください。

www.openwork.co.jp