はじめに
2018年の PHPerKaigi で、「SOLIDの原則ってどんなふうに使うの?」 というタイトルで hidenorigoto さんによるセッションがありました。そのセッションの話の中で Resolverのパターン
が取り上げられていました(Resolver はGoFデザインパターンではありませんが、よくある設計の問題とその解決の1つなのでここではパターンと呼びます)。この記事では、私がふだん使っている BEAR.Sunday で Resolver のパターンを実装するにはどうすれば良いかについて考えたことを書き、FormalBears 拡張で試作したコードを紹介します。
やりたいことのイメージ
「続・SOLIDの原則ってどんなふうに使うの?」発表スライドの 130ページ目
上記の図のような設計を BEAR で実装したいと思います。
Resolver クラスの add
メソッドによるフォーマッター登録の箇所をどうするのかという点が重要です。DIなどの コンフィグレーション
で行う必要があります。
※Guice ではマルチバインディングで実現されます。また、私は書き方を知らないのですが、 Symfony、Laravel で同等の機能があると聞いたことがあります。
方法の検討
次の2つの機構が必要です。
- 1つのインターフェイスに対して複数の実装をバインド登録できること
- 登録したバインド選択肢のうちどれを使うのかを、実行時に動的に決まるキー値によって都度指定できること
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 パターン)を知ったことにより理解することができました。