プログラミング - THIS IS IT !

より良い開発をすべく日々奮闘しているプログラマーのブログです。設計に興味があります。主にPHPネタを書いてます 日本Symfonyユーザー会

Symfonyフレームワークにおけるデザインパターン活用

この記事はSymfony Advent Calender 2018 5日目の記事です。

はじめに

今回はSymfonyフレームワークにおいて、どのようにデザインパターンが使われているかについてのお話します。 Symfonyは既に4系リリースされてますが、ここではSymfony3.4系 Standard Editionをベースに解説しています。

Symfony4系でもコードベースはそれほど変わっていないと思います。

これまでにデザインパターンをなんとなく勉強したことはあるけど、実務で活かせた経験がないない。メリットがいまいち分からなかった。

といった方が「なるほど。そういう使い方をするのか」と気づいてもらえればうれしいです。

それでは本題

Strategyパターン

Strategyパターンはオープンクローズドの原則に則っていて、ロジック(ここではStragetyと呼んでいます)の切り替えや追加を簡単に行えるようするデザインパターンです。

GofのStrategyパターンは、最も有名なデザインパターンがの1つではないかと思います。

通常フレームワークでは利用する「ロジックを簡単に切り替え」たり、「拡張点があらかじめ提供」してくれています。

「ロジックの切り替えとは?」と思った方もいるかもしれませんが、例えばMySQLPostgreSQLといった自分が使いたいデータベースを自由に選択することができます。

他にもキャッシュの保存先、セッションの保存先など実装者はFWが用意してくれた多くの選択肢の中から自由に選ぶことができます。

1つの選択肢しか取れないとなると非常に困りますよね。

Symfonyにも勿論このような機能が提供されています。

configファイルに記述することで簡単にロジック(ストラテジー)を選択できるようになっています。

そういった、ロジックの切り替えを行う機能を提供するのにうってつけのパターンが「ストラテジーパターン」になります。

また、ここでは直接触れませんが自分の要望に合うものがなければ自作のストラテジーを用意して追加することもできます。

説明が長すぎても伝わらないと思うので、実際にコードを見ていきましょう。

例に取り上げるのはSessionHandlerです。

SessionHandlerInterface 自体はPHPで標準に提供されています。

PHP: SessionHandlerInterface - Manual

そしてSymfonyにおける抽象クラスの定義がこちら。

<?php

abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
{
    /**
     * @param string $sessionId
     *
     * @return string
     */
    abstract protected function doRead($sessionId);

    /**
     * @param string $sessionId
     * @param string $data
     *
     * @return bool
     */
    abstract protected function doWrite($sessionId, $data);

    /**
     * @param string $sessionId
     *
     * @return bool
     */
    abstract protected function doDestroy($sessionId);
}

各サブクラスではこれらの抽象メソッドが実装していくこととなります。(この部分だけ見ると)

サブクラスにはsessionをDBに保存する PdoSessionHnadlerMemcachedに保存する MemcachedSessionHandler 等があります。

愚直にコードを書くと以下のような利用イメージとなります。

<?php

// DBに保存したい場合
$pdo = new \PDO(...);
$sessionStorage = new NativeSessionStorage([], new PdoSessionHandler($pdo));
$session = new Session($sessionStorage);

// Memcachedに保存したい場合
$memcached = new \Memcached();
$memcached-> addServers(...);
$sessionStorage = new NativeSessionStorage([], new MemcachedSessionHandler($memcached)));
$session = new Session($sessionStorage);

PdoSessionHnadler, MemcachedSessionHandler が各ストラテジーとなっており実装者は自由に選択することができます。

以下は PdoSessionHnadler を用いた例ですが、Symfonyでは下記の通りconfigを設定することで利用できます。

config.yml

framework:
    session:
        handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler
services:
     Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler:
        arguments:
            - 'mysql:dbname=mydatabase, host=myhost'
            - { db_table: 'sessions', db_username: 'myuser', db_password: 'mypassword' }

ここまで見てきて分かることは、実際のところこのSessionクラスにおいては1つのストラテジーのみが利用できることとなっています。

Symofnyにはコンパイルフェイズ(Bundleの起動)があり、選択された1つのSessionHandlerがDIコンテナによって注入されることになります。

このSessionHandlerはアプリケーションの実行時に変える必要がないため、configの設定に応じた唯一のSessionHnaderだけが注入されているのは理にかなっていると思います。

余談: 一方、実務で書くStrategyパターンは実行時(ランタイム)で決まることが多いと思います。

例)

  • ロイヤルカスタマーであれば割引率が大きくなる
  • Adminユーザーの場合だけ出力フォーマットを変える など

実行時に使うストラテジーを変更したい場合には、下記記事が参考になりますので見てみるとよいと思います。

medium.com

余談2: Template Methodパターン

https://github.com/symfony/symfony/blob/3.4/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/AbstractSessionHandlerTest.php

<?php

abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface
{
    abstract protected function doRead($sessionId);

    abstract protected function doWrite($sessionId, $data);

    abstract protected function doDestroy($sessionId);
}

先程出てきた AbstractSessionHandler ですが、これはTemplate Methodパターンと見ることもできます。

wikipediaによると Template Method パターンとは以下のようなものです、

ある処理のおおまかなアルゴリズムをあらかじめ決めておいて、そのアルゴリズムの具体的な設計をサブクラスに任せること

処理の流れはAbstractSessionHandlerにかかれていて、具体的な処理は PdoSessionHnadlerMemcachedSessionHandler といったサブクラスで実装されています。

Template Method パターンは単に継承を活用しただけなので、それほど難しいパターンではなく実は無意識のうちにわりと書いていたりするのではないかと思います。

Decoratorパターン

既存のクラス/インターフェースを変更することなく元のクラスに機能追加 できるようにする設計パターンです。

個人的にとても好きなデザインパターンの1つです。

以前、別の記事でSymofnyのコントローラーの実行時間の計測にはDecoratorパターンが利用されていると紹介したことがあります。

okapon-pon.hatenablog.com

↑ 余談を項を参照

今回は別の箇所で、Decoratorが利用されている部分を見てみましょう。

例として取り挙げるのは HttpKernel です。

https://github.com/symfony/symfony-standard/blob/3.4/web/app.php#L14

$kernel = new AppKernel('prod', false);
$kernel = new AppCache($kernel); // AppCacheがデコレーター

$response = $kernel->handle($request);
$response->send();

AppKernelに対しリバースプロキシのように動作する Cache機能を付け加えたのがAppCacheクラスです。

同一インタフェースである handle() メソッドがそのまま使え既存のコードには影響がありません。単にCacheの機能が追加されただけとなっています。

Cacheの仕組み自体はここでは解説しませんので、以下ご参照ください。

symfony.com

Decoratorパターンが素晴らしいのは、もし仮に独自拡張のMyKernelクラスを作りたくなったとしても、MyKernelがKernelインタフェースさえ実装していれば、いくらでもクラスを重ねることができ、機能の追加だけを行うことができます。

$kernel = new AppKernel();
$kernel = new AppCache($kernel);
$kernel = new MyKernel($kernel);
$response = $kernel->handle($request);

非常にエレガントな設計だと思いませんか?

Null Objectパターン

Null ObjectパターンはStrategyパターンの応用と考えてよいと思います。

同一インタフェースを実装した何も仕事をしないオブジェクトに差し替えることで、利用側のコードを変えることなく機能を無効化することができるデザインパターンとなります。

例えば、Symfony Standard Editionのテスト実行時には Swift_NullTransport が使われていたりします。

他にもPsr\Log\NullLogger というものもありますね。

このようなクラスに差し替えることでtest環境ではメールを送信しない、ログを記録しないといったことが行なえます。

Null Objectパターンを利用しない場合には、

if ($strategy === null) {
    return;
}

とガード節を書かなけらばならずあまりいい感じはしないですね。

では実際に Null Objectパターンが利用されているコードを紹介したいと思います。

Swift_Mailerクラスをみてください。このクラスはでは Swift_Transport というインタフェースにのみ依存しています。

https://github.com/swiftmailer/swiftmailer/blob/master/lib/classes/Swift/Mailer.php

<?php

class Swift_Mailer
{
    private $transport;

    public function __construct(Swift_Transport $transport)
    {
        $this->transport = $transport;
    }

    public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
    {
        // ....略
            $sent = $this->transport->send($message, $failedRecipients); // interfaceに定義されているsendメソッドに依存
     }

一方でNullObjectパターンが適用された Swift_Transport_NullTransport クラスをみてください。

sendメソッドは実際のメール送信は行わないダミーメソッドとなっています。

https://github.com/swiftmailer/swiftmailer/blob/master/lib/classes/Swift/Transport/NullTransport.php

<?php

class Swift_Transport_NullTransport implements Swift_Transport
{
    public function send(Swift_Mime_Message $message, &$failedRecipients = null)
    {
        // メール送信処理は実際には行わず、ただ送信数をカウントして返却するだけのダミーメソッド
        $count = (
            count((array) $message->getTo())
            + count((array) $message->getCc())
            + count((array) $message->getBcc())
        );

        return $count;
    }
}

SwiftMailerBundel内で行われている、SymfonyのDIの仕組みについてはここでは詳しく触れませんが、下記のymlファイルへの記述だけで Swift_NullTransport が使われるようになります。

Swift_NullTransportSwift_Transport_NullTransport を継承したクラスです。

# config_test.yml
swiftmailer:
    disable_delivery: true

Commandパターン

Command オブジェクトは、動作とそれに伴うパラメータをカプセル化したパターン

Commandパターンのメリットは2点あります。

  • 実際の処理を細かく分離できること
  • 命令を別で行えること(個別実行もできるし Unixのパイプのように連結して実行することもできる)

Commandパターンの実装面での1つの見方としては、Template Methodパターンの派生でより目的特化したものと考えることもできます。

Template Methodパターンでは複数のabstractメソッドが定義されている場合がありますが、Commandパターンとして定義されているのは execute() のみとなります。

開発者はCommandクラスのサブクラスで必要な処理を execute() メソッドに実装します。

ではSymofnyのどこで実際に使われているかですが、もちろんCommadクラス(Consoleコンポーネント)ですね。

symfony.com

ドキュメントにさらっとexecute() を実装するように書かれているのであまり意識したことはないとは思います。

Commandクラスのコードを見てみましょう。

namespace Symfony\Component\Console\Command;

class Command
{
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        throw new LogicException('You must override the execute() method in the concrete command class.');
    }
}

実はexecute()メソッドはabstractメソッドではないので、厳密にはCommandパターンとは呼べないかもしれません。

ですが、Commandコンポーネント開発者の意図としてはこのデザインパターン通りだと思います。

※注 Symofny Commandコンポーネントの場合、実質configure()メソッドの実装もほぼ必須だと思います。

Commandはそれぞれが独立しているので、単独でコマンド実行することができますし

$ php app/console demo:greet Fabien
$ php app/console my:greet okapon

コマンド自体が分離しているので、以下のドキュメントのようにあるコマンドから別のコマンドを呼び出すというようなことも簡単に行なえます。

How to Call Other Commands (Symfony Docs)

その他

ここでは簡単に紹介

Compositeパターン

Formコンポーネントで実際に使われています。

よくあるCompositeパターンの例では、ディレクトリ構造たどっていくサンプルコードがありますが、このFormObjectもまさにそのようなTree構造で問題を扱っています。

まとめ

ぜひSymfonyのコードリーディングにチャレンジしてみてください。