OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

SymfonyのService Containerについて(後編)

こんにちは。@mrtry_です。
最近ずっとサンダルで出社していたら、日焼けでサンダルの紐の跡ができました。

さて、今回は前回に引き続き、DIについて書こうと思います。 前回でDIとはなにか、という話をしたので、今回はSymfonyでDIする際に利用するService Containerについて書きたいと思います

以下、今回の記事の目次になります。

  • Service Contaier とは
  • DIするまでの手順
  • サービスを作る
  • サービスの設定をする
  • コントローラでサービスを呼び出してみる

Service Container とは

Service Containerとは、Symfonyで提供されているDIコンテナです。 Symfonyでは、ビジネスロジックをまとめたクラスをサービスと呼んでいます。 Service Containerは、サービスをインスタンス化するときに、必要とする定数や他のサービスのオブジェクトを注入したりと、依存関係を解決する役割を担ってくれます。 前回の記事で言うところの「インジェクタ」の役割にあたります。

今回は、実際に簡単なサービスを作ってみて、Service Containerを利用して依存する定数やサービスを注入していきたいと思います。

DIするまでの手順

実際にサービスを作成し、依存する他のサービスを注入してみたいと思います。 以下のような手順で進めていきたいと思います。

  • サービスを作る
  • サービスの設定をする
    • ローダーの作成
    • 設定ファイルの作成
    • コマンドラインからの確認

サービスを作る

はじめに、元となるビジネスロジックをまとめたクラス、サービスを作ろうと思います。

まず、サービスのソースコードを配置するディレクトリを作っておきます。

$ mkdir src/AppBundle/Service

次に、サービスクラスを作ります。

今回は、GreetingServiceというサービスを作成しました。 sayGreeting()すると、時間帯にあった挨拶を返してくれるサービスです。 このサービスには、TimeSlotInterfaceを継承したクラスと、VariousGreetingsInterfaceを継承したクラスが注入されています。 時間帯の判定をTimeSlotInterfaceを継承したクラス、時間帯に依ったあいさつをVariousGreetingsInterfaceを継承したクラスにまかせるようになっています。

ちなみに、今回の例では、DIをコンストラクタで依存オブジェクトを受け取るConstruct Injectionで行っていますが、Setter InjectionProperty Injectionも行うことができます。

src/AppBundle/Service/GreetingService.php

<?php

namespace AppBundle\Service;

use AppBundle\Service\VariousGreetingsInterface;
use AppBundle\Service\TimeSlotInterface;

class GreetingService
{
    private $variousGreetings;
    private $timeSlot;

    public function __construct(TimeSlotInterface $timeslot, VariousGreetingsInterface $variousGreetings)
    {
        $this->timeSlot = $timeSlot;
        $this->variousGreetings = $variousGreetings;
    }

    public function sayGreeting()
    {
        $greeting = '';

        switch ($this->timeSlot->getCurrentTimeSlot()) {
            case TimeSlotInterface::MORNING:
                $greeting = $this->variousGreetings->sayMorningGreeting();
                break;

            case TimeSlotInterface::NOON:
                $greeting = $this->variousGreetings->sayNoonGreeting();
                break;

            case TimeSlotInterface::NIGHT:
                $greeting = $this->variousGreetings->sayNightGreeting();
                break;
        }

        return $greeting;
    }
}

続いて、注入しているクラスのインターフェースと実際のクラスを見ていきます。

src/AppBundle/Service/TimeSlotInterface.php

TimeSlotInterfaceは以下のようなコードになっています。 時間帯を表す定数と、現在の時間帯を表す値を返すメソッドgetCurrentTimeSlot()が定義されています。

<?php

namespace AppBundle\Service;

interface TimeSlotInterface
{
    const MORNING = 0;
    const NOON = 1;
    const NIGHT = 2;

    /**
     * 現在の時間帯を表す値を返す
     *
     * @return int
     */
    public function getCurrentTimeSlot();
}

src/AppBundle/Service/TimeSlotService.php

TimeSlotInterfaceを継承し、TimeSlotServiceを作成しました。 GreetingService に注入する実際のクラスはこれになります。 以下のようなコードになっています。

<?php

namespace AppBundle\Service;

use AppBundle\Service\TimeSlotInterface;

class TimeSlotService implements TimeSlotInterface
{
    public function getCurrentTimeSlot()
    {
        $now = new \DateTime('now');

        $morningOfTheDay = clone $now;
        $morningOfTheDay->setTime(6, 0);

        $noonOfTheDay = clone $now;
        $noonOfTheDay->setTime(11, 0);

        $nightOfTheDay = clone $now;
        $nightOfTheDay->setTime(18, 0);

        if ($morningOfTheDay <= $now && $now < $noonOfTheDay) {
            return TimeSlotInterface::MORNING;
        } else if ($noonOfTheDay <= $now && $now < $nightOfTheDay) {
            return TimeSlotInterface::NOON;
        } else {
            return TimeSlotInterface::NIGHT;
        }
    }
}

src/AppBundle/Service/VariousGreetingsInterface.php

VariousGreetingsInterfaceは以下のようなコードになっています。 朝昼夜、それぞれにあたるあいさつを返すメソッドが定義されています。

<?php

namespace AppBundle\Service;

interface VariousGreetingsInterface
{
    /**
     * 朝のあいさつを返す
     *
     * @return string
     */
    public function sayMorningGreeting();

    /**
     * 昼のあいさつを返す
     *
     * @return string
     */
    public function sayNoonGreeting();

    /**
     * 夜のあいさつを返す
     *
     * @return string
     */
    public function sayNightGreeting();
}

src/AppBundle/Service/JapaneseGreetingService.php

VariousGreetingsInterfaceを継承し、JapaneseGreetingServiceを作成しました。 GreetingServiceに注入する実際のクラスはこれになります。 以下のようなコードになっています。

<?php

namespace AppBundle\Service;

use AppBundle\Service\VariousGreetingsInterface;

class JapaneseGreetingService implements VariousGreetingsInterface
{
    public function sayMorningGreeting()
    {
        return 'おはようございます';
    }

    public function sayNoonGreeting()
    {
        return 'こんにちわ';
    }

    public function sayNightGreeting()
    {
        return 'こんばんわ';
    }
}

DIすることで得られた利点

このように実装したことで、以下の利点があります。

結合度の低下

依存するクラスをInterfaceで実装を統一しているため、結合度が低くなっています。 今回の例で言うと、多言語対応が必要となった時に嬉しいです。 VariousGreetingsInterfaceを継承したクラスを新しく作成し、GreetingServiceに注入すれば、それだけで別言語でのあいさつを返すサービスを実装することができます。 英語のあいさつを返すサービスにしたければ、VariousGreetingsInterfaceを継承したEnglishGreetingServiceを作成し、注入するだけで対応できます。

※ なお、今回の実装での多言語化はDIの例で挙げているだけで、通常は以下のような手法を使います。

テストのしやすさ

スタブやモックを用いたテストが状態になっています。 今回の例で言うと、時間帯ごとに意図したあいさつが返ってくるかを確認したい時に嬉しいです。 TimeSlotInterfaceを継承したクラスを作成し、getCurrentTimeSlot()で確認したい時間帯の定数を返すことで、時間帯ごとに意図したあいさつが返ってくるかを確認することができます。

サービスの設定をする

次に、サービスに注入する依存オブジェクトを定義したりする設定ファイルを作成していきます。

この記事では、サービスの設定ファイルをyamlで作成します。 作成した設定ファイルの配置場所として、以下の2箇所があります。

  • app/config
    • composerなどで導入したBundleをService Containerに登録したいときはここに置く
  • Bundle内のResouces/config
    • 自身で定義したサービスをService Containerに登録するときは、ここに置く

今回は、自身で定義したサービスをService Containerに登録したいので、Bundle内のResouces/configに配置したいと思います。

ただ、$ app/console generate:bundleでBundleを生成した際は、 設定ファイルを配置するディレクトリや、設定ファイルを読み込むためのローダーが準備されているのですが、プロジェクトを立ち上げたときに生成されるAppBundleでは準備がされていません。 ローダーの準備と設定ファイルの配置場所の作成、設定ファイルの作成をしていきます。

ローダーの作成

まず、ローダーのソースコードを配置するディレクトリを作成します。

$ mkdir src/AppBundle/DependencyInjection

その後、以下のようにローダーを作成します。

src/AppBundle/DependencyInjection/AppExtension.php

<?php

namespace AppBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;

class AppExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(__DIR__.'/../Resources/config')
        );
        $loader->load('services.yml');
    }

    public function getAlias()
    {
        return 'app';
    }
}

継承しているExtensionというクラスは、サービスコンテナに対して、何かしらの拡張を行うためのクラスです。 YAMLなどの設定ファイルからサービスの定義を読み取ったり、設定に基づいてサービスの構成を行ったりします。

今回は、AppBundle/Resources/configに配置した設定ファイルを読み込むためにYamlFileLoaderを用意し、services.ymlを読み込む処理をしています。

設定ファイルの作成

先程Resources/configに配置したservices.ymlを読み込むようにローダーの準備をしたので、ディレクトリを作成し、設定を配置します。

$ mkdir -p src/AppBundle/Resources/config

src/AppBundle/Resources/config/services.yml

services:
    # サービスID
    app.japanese_greeting_service:
        # 実際のクラス
        class: AppBundle\Service\JapaneseGreetingService

    app.timeslot_service:
        class: AppBundle\Service\TimeSlotService

    app.greeting_service:
        class: AppBundle\Service\GreetingService
        # 注入するサービスIDを渡す (サービスIDの頭には`@`をつける)    
        arguments:
            - @app.timeslot_service
            - @app.japanese_greeting_service

servicesのキー以下では、サービスの定義をすることができます。 任意の名前のサービスIDを設定し、実際のクラス名指定することで、フレームワーク側にサービスを登録することができます。 また、依存する定数や他のクラスを注入することもできます。

今回の例だと、以下のように定義しています。

クラス名 サービスID 注入したもの
JapaneseGreetingService japanese_greeting_service なし
TimeSlotService timeslot_service なし
GreetingService greeting_service japanese_greeting_service, timeslot_service

コマンドラインからの確認

一通りの設定ができたら、$ php app/console debug:containerを叩いてサービスがSymfony側に登録されたことを確認します。 きちんと設定ができていれば、以下のようにサービスIDを確認することができます。

$ app/console debug:container | grep app
  app.greeting_service                                                 AppBundle\Service\GreetingService
  app.japanese_service                                                 AppBundle\Service\Japanese
  app.timeslot_service                                                 AppBundle\Service\TimeSlotService
  doctrine.orm.default_entity_listener_resolver                        Doctrine\ORM\Mapping\DefaultEntityListenerResolver

これで、サービスを利用するまでの準備は一通り終わりました。

コントローラでサービスを呼び出してみる

では、定義したサービスをコントローラで利用してみます。 サービスを利用する際は、$this->get(サービスID)を実行するとサービスのインスタンスが取得できます。

今回の例では、MessageServiceを取得し、getMessage()した内容をJsonで返すコントローラを書きました。

<?php

namespace AppBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(Request $request)
    {
        $greetingService = $this->get('app.greeting_service');
        $message = [
            'message' => $greetingService->sayGreeting(),
        ];

        return new JsonResponse($message);
    }
}

app/console server:startをし、http://127.0.0.1:8000/へアクセスしてみると、時間帯に合ったあいさつが表示されていることを確認できたかと思います。

おわりに

今回は、SymfonyのService Containerを利用してDIする例を紹介しました。 Service Containerは、実際に開発でも高頻度で触る箇所なので、きちんと理解しておきたい所ですね!!

さて、今回をもちまして、「Symfony2入門」の投稿は、一旦終えたいと思います。 いろいろ紹介していない内容は多々あるのですが、全部紹介していたら永遠と続くので今回を区切りにしたいと思います。笑 (最近、私の開発担当がモバイルアプリになって、Symfonyを全然さわっていないというのも、あります。)

今まで書いた記事が、私と同じような初心者の方々の参考になったなら嬉しく思います。 ありがとうございました!

参考

重い腰上げDay(仮)を開催しました

ペンギン村からおはこんばんちは。今年のフジロックは3日通しで参加予定の @kalibora です。久しぶり過ぎてどんなテンションでブログ書けばいいのか忘れました。

さて、今日は技術的な話題というよりも、先日社内で開催した 重い腰上げDay(仮) というイベントについて書きたいと思います。

重い腰上げDay(仮)とは

結論から申しますと、メルカリ の Be Professional Day(BPD) のパクリです。

定義については t-wada 先生の

“Be Professional Day: 1ヶ月に1日、普段の業務を一旦ストップさせて普段手を付けていないことをやる日” 7つの習慣でいうと緊急度が低く重要度が高い象限にフォーカスする日

http://b.hatena.ne.jp/entry/277751164/comment/t-wada

これが一番わかりやすく、しっくり来たので、これを採用してます。

ちなみに7つの習慣は弊社サービスのFeBe↓でも購入できますので、耳で聴きたい方は是非。

www.febe.jp

まー、アレですよ。普段は目の前にやらなきゃいけないことが山積みであるじゃないですか。 で、それはそれで必要なんだけどもでもちょっと待って、それやって仕事した気になってるけど、それで本当にいいんだっけ?そのうち緩やかに死んでいかないんだっけ? みたいなやつを考えてやったりなんだりする日です。

あと名前については言い出しっぺの僕が付けた仮称のままでとりあえず動いてます。

1回目の成果

さぁ、そんなわけで大上段に構えつつ実際は緩やかに1回目を開催したわけですが、 成果としてはこんな感じでした。

  1. 古いシステムで動き続けて使い辛いまま放置されていた管理画面の移植&機能改善
  2. 古くて保守しづらかったり機能がイマイチなライブラリの置換
  3. アプリのビルド周りのアップデート

エンドユーザーに直接的にメリットがあるものもあれば、そうでないものもあり。 成果として上がったかどうかは、一概には判断し辛いと思ってます。

でもそれで良いのです。そういうものこそを進めるための時間なので。

やる意味あんのか?あんのかバカヤロー

いわゆる技術的負債のようなものをどのように返していくか?ということにも近いと思うのですが、 こういう類のものは往々にして後回しにされます。(直近で直接的なメリットが薄いので。)

しかし放置しておくとにっちもさっちもいかなくなる(あとは後任に丸投げ? or 行き着く先はサービス終了?)ので、それをどう処理していくのか?

scrumでやってるなら、バックログ的なものにエンドユーザー目線でのメリットがどうか?ということを加味して同じ枠内で優先度を付けて混ぜてやる方法もあると思いますし、 負債処理班、みたいに専任で人で分けちゃうっていうやり方もあるだろうし。 今回みたいに、期間を設けて対応するというやり方もあるだろうし。

何が言いたいのかよくわからなくなってきましたが、、 ともかく、普段とは別の角度から考える時間っていうのは必要だと思うので、この取り組みは今後も続けて行こうと思っています。

SymfonyのService Containerについて(前編)

こんにちは。@mrtry_です。 最近、低温調理機を自作しまして、毎週ハナマサで肉塊を買って、調理する週末を過ごしています。

さて、今回から2回ほどService Containerについて紹介したいと思います。 初回のこの記事では、Service Containerの仕組みの元となるデザインパターンDependency injection(DI)について紹介したいと思います。

以下、今回の記事の目次になります。

  • Dependency injection(DI)とは
  • 実際のコード例
  • DIすると嬉しいこと
  • SymfonyでDIを行うにはSymfonyでDIを行うには

Dependency injection(DI)とは

Dependency injection(DI)とは、デザインパターンの一種です。 よく「依存性の注入」とよく言われています。 和訳だけ見てもどんなものなのか想像しにくいですね…。

英語のwikipediaを見てみると、以下のように説明されています。

In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object.A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client’s state.[1] Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern. Dependency injection - Wikipedia

意訳すると、だいたい以下のようなことが書いてあります。

  • Dependency injectionは、オブジェクト間の依存関係を解決するデザインパターンのひとつ
  • 「Dependency」は、利用しているオブジェクト(サービス)とのことを指す
  • 「Iinjection」は、「クライアント」に「依存オブジェクト(サービス)」を渡すこと
  • 「依存するオブジェクトをクライアントに渡す」という振る舞いが、DIパターンの基本

また、以下のような説明もあります。

As with other forms of inversion of control, dependency injection supports the dependency inversion principle. The client delegates the responsibility of providing its dependencies to external code (the injector). The client is not allowed to call the injector code.[2] It is the injecting code that constructs the services and calls the client to inject them. This means the client code does not need to know about the injecting code. The client does not need to know how to construct the services. The client does not need to know which actual services it is using. The client only needs to know about the intrinsic interfaces of the services because these define how the client may use the services. This separates the responsibilities of use and construction. Dependency injection - Wikipedia

意訳すると、だいたい以下のようなことが書いてあります。

  • DIには「依存性反転の原理」がある
  • クライアントは、自身に注入される依存オブジェクトの準備を全て「インジェクタ」に任せる
  • クライアントは、依存オブジェクトの生成方法、そのオブジェクトがどんなものであるか知る必要はない
  • クライアントは、注入される依存オブジェクトのInterfaceだけわかれば良い
  • こうすることで、「依存オブジェクトを準備する責任」と「依存オブジェクトを使用する責任」に分けることができる

というわけで、DIをまとめると以下のようになります。

  • 「依存するオブジェクトを外から渡してあげる」という「デザインパターン」のこと
  • 要素として、クライアントとインジェクタというものがある
    • クライアント
      • 依存するオブジェクトを外から受け取り、そのオブジェクトを使用する責任を持つ
      • 渡されるオブジェクトのInterfaceだけ知っている
    • インジェクタ
      • 依存オブジェクトを用意する責任を持つ
      • クライアントが知っているInterfaceを持つクラスを用意する

というものだとわかりました。

実際のコード例

実際のコードでDIしたもの、していないもので比較してみます。 掲載されているソースコードは、やはりあなた方のDependency Injectionはまちがっている。 — A Day in Serenity (Reloaded) — PHP, FuelPHP, Linux or somethingを参考にしています。

ClientServiceというクラスを作成します。 また、ServiceClientの依存オブジェクトとします。

DIしていないコード

Clientのconstruct時に、Serviceもnewし、内部で保持するような実装になっています。

<?php

class Client
{
    private $service;

    public function __construct()
    {
        $this->service = new Service();
    }

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

    ...
}

class Service
{
    public function doSomething()
    {
        ...
    }

    ...
}

DI しているコード

Clientのconstruct時に、$serviceを外部から受け取るようになっています。 $serviceは、既に準備されたものを受け取るようになっています (この$serviceのオブジェクトの準備は、何かしらで実装されたインジェクタが用意してくれます)。 Clientがどんな$serviceを受け取るかはServiceInterfaceで決定しています。

<?php

class Client
{
    private $service;

    public function __construct(ServiceInterface $service)
    {
        $this->service = $service;
    }

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

    ...
}

class Service implements ServiceInterface
{
    public function doSomething()
    {
        ...
    }

    ...
}

DIすると嬉しいこと

先程のコード例を見比べて、DIした時にどんな嬉しいことがあるのでしょう?

DIしていないと困ること

  • 結合度が高い
    • 依存オブジェクトの置換や更新をする時、依存オブジェクトを利用しているクラスのソースコードを修正する必要がある
    • 例: ServiceクラスのAPIに変更があったとき、Clientクラスも合わせて修正しなければならない
  • テストがしにくい
    • 依存オブジェクトを直接参照しているので、テスト時にスタブやモックに置換することができない
    • 例: Serviceが本番DBを扱っているオブジェクトとする。Clientのテストをするとき、Serviceに依存しているためモックなどに差し替えができず、DBにテスト用のデータを準備したりする必要がある

DIすると嬉しいこと

  • 結合度の低下
    • 依存オブジェクトを利用しているクラスのソースコードに修正を加えず、依存オブジェクトの置換や更新を行える
    • 例: ServiceクラスのAPIに変更があったとしても、Interfaceで実装を統一しているので、変更する必要はない
  • テストがしやすくなる
    • スタブやモックを準備することで、そのクラスを単体テストすることができる
    • 例: Clientのテストをするとき、Serviceと同様のInterfaceを持つオブジェクトを作成すれば、テスト時は開発サーバに接続したりすることができる

SymfonyでDIを行うには

DIの仕組みを提供するフレームワークのことを、DIコンテナと言います。 Symfonyには、Service ContainerというDIコンテナが初めから導入されて、とても簡単にDIをすることができます。

実際の使い方に関しては、次回説明したいと思います。

おわり

今回は、Dependency injectionについて紹介しました。 次回は、Service Containerの利用方法について紹介したいと思います。

また、この記事は、@mrtry_の勉強の一環で書いていますので、 お気づきの点などがありましたら、コメント等でご指摘いただければ幸いです!

参考