3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

FormalBears マルチバインディングで Resolver のパターンを実装してみる(β版)

Posted at

はじめに

2018年の PHPerKaigi で、「SOLIDの原則ってどんなふうに使うの?」 というタイトルで hidenorigoto さんによるセッションがありました。そのセッションの話の中で Resolverのパターン が取り上げられていました(Resolver はGoFデザインパターンではありませんが、よくある設計の問題とその解決の1つなのでここではパターンと呼びます)。この記事では、私がふだん使っている BEAR.Sunday で Resolver のパターンを実装するにはどうすれば良いかについて考えたことを書き、FormalBears 拡張で試作したコードを紹介します。

やりたいことのイメージ

「続・SOLIDの原則ってどんなふうに使うの?」発表スライドの 130ページ目

上記の図のような設計を BEAR で実装したいと思います。
Resolver クラスの add メソッドによるフォーマッター登録の箇所をどうするのかという点が重要です。DIなどの コンフィグレーション で行う必要があります。

※Guice ではマルチバインディングで実現されます。また、私は書き方を知らないのですが、 Symfony、Laravel で同等の機能があると聞いたことがあります。

方法の検討

次の2つの機構が必要です。

  1. 1つのインターフェイスに対して複数の実装をバインド登録できること
  2. 登録したバインド選択肢のうちどれを使うのかを、実行時に動的に決まるキー値によって都度指定できること

FormalBears 拡張を導入してマルチバインディング機能を使うことによりこれらを用意することにしました。

実装

1つのインターフェイスに対して複数の実装をバインド登録

モジュール設定:

class TodoListModule extends AbstractConfigAwareModule
{
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        // 拡張ポイントを定義
        $this->defineExtensionPoint(TodoFormatterInterface::class, TodoFormatterProvider::class);

        // 複数の実装の登録
        // 「通常のTODO」のフォーマッター
        $this->registerExtension(TodoFormatterInterface::class, NormalTodoFormatter::class, NormalTodo::class);
        // 「GoogleカレンダーのTODO」のフォーマッター
        $this->registerExtension(TodoFormatterInterface::class, CalendarTodoFormatter::class, CalendarTodo::class);
    }
    // ...

実行時に動的に決まるキー値によって都度指定

利用例:

class Todolist extends ResourceObject
{
    /**
     * @var TodoFormatterProvider
     */
    private $todoFormatterProvider;

    /**
     * @var TodoListQueryRepository
     */
    private $todoRepository;

    /**
     * @Inject
     *
     * @param TodoFormatterProvider $todoFormatterProvider
     * @param TodoListQueryRepository $repo
     */
    public function __construct(TodoFormatterProvider $todoFormatterProvider, TodoListQueryRepository $repo)
    {
        $this->todoFormatterProvider = $todoFormatterProvider;
        $this->todoRepository = $repo;
    }

    public function onGet(): self
    {
        $todoList = $this->todoRepository->findAll();
        $formatterProvider = $this->todoFormatterProvider;

        $this->body['list'] = array_map(function (TodoInterface $todo) use ($formatterProvider) {
            // TODOの種類によってどのフォーマッター実装クラスを使うのかを都度動的に決定
            $formatterProvider->setContext(get_class($todo));
            /** @var TodoFormatterInterface $formatter */
            $formatter = $formatterProvider->get();

            return $formatter->format($todo);
        }, $todoList);

        return $this;
    }
}

Gitリポジトリ

FormalBearsDemo に動作するコードを置きました。

セッションスライドの図と異なっているところ

  • モジュール設定で代替しているので、Resolver クラスの add メソッドとフォーマッター の supports メソッドはありません

メリット

  • 可変点がモジュール定義に示されているので明らかです。
  • TODOの種類が増えた場合は最小限の安全な変更で対応できます。

※通常は下記のようにエンティティごとにフォーマッターのクラスをどれにするか判定するロジックを条件分岐で用意します。TODOの種類が増えたらこの箇所も併せて修正してやる必要があります。

            /** @var TodoFormatterInterface $formatter */
            $formatter = null;
            switch (get_class($todo)) {
                case NormalTodo::class:
                    $formatter = $this->normalFormatter;
                    break;
                case CalendarTodo::class:
                    $formatter = $this->calendarFormatter;
                    break;
                default :
                    throw new \LogicException();
            }
            return $formatter->format($todo);

デメリット

  • FormalBears 拡張が必要です。
  • プロバイダ を直接インジェクションしており、プロバイダのメソッドをアプリケーション内部で直接利用しています(記事タイトルに "β版" と付けたのはこのためです)。

編集後記

Guice のマルチバインディングのマニュアルを読んだ当初は、何が嬉しいのか私には分からなかったのですが、実務でよくある使用例(Resolver パターン)を知ったことにより理解することができました。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?