序文
デザインパターンで最も難しいとされる Visitor Pattern を、2018年5月の初めにやっと理解できました。自分なりに理解した Visitor Pattern は、以下の通りです。
異なる要素が含まれるコレクションに対し、各要素ごとに if 文で分岐することなく処理を切り替えたい時に使われるパターン
未解決問題 (Expression Problem)
以下にリンクした記事で、Visitor Pattern に未解決問題 (Expression Problem) があると知りました。
Visitor パターン再考
Expression Problem の主眼は以下の点だと理解しました。
要素が追加された時、追加された要素のための処理を追加するには、既存クラスを書き替えなければならない。
「既存クラスを書き替えないで要素も処理も任意に追加できる Visitor Pattern を設計できたらすごいな」と少し考えてみたところ、いけてそうな設計を発見しました。それが、この記事で紹介する Dispatcher Pattern です。
Dispatcher Pattern の設計
Dispatcher Pattern の主役は、Dispatcher クラスです。全部の処理クラスは Expression インターフェイスを implements し、Dispatcher クラスに委譲されます。Dispatcher クラスは「委譲よるポリモーフィズム(後述)」を使って、クライアントから渡された全要素について処理を振り分けます。
クラスとインターフェイス
要素クラス (Item class)
- 任意のオブジェクトです。ライブラリで提供されたクラスなど、変更できない要素クラスでも対応できます。
class ItemA
{
String getAnimalName() {
return "cat";
}
}
class ItemB
{
String getColorName() {
return "purple";
}
}
class ItemC
{
}
class ItemD
{
}
class ItemE
{
}
処理クラス (Expression class)
- Dispatcher pattern で使う処理クラスは、Expression インターフェイスを実装します。全処理に共通するメソッド群は、Expression を継承したインターフェイスで定義します。この例では Exp を定義しています。
interface Expression<T>
{
}
interface Exp<T> extends Expression<T>
{
void print(T item);
}
class ExpA implements Exp<ItemA>
{
@Override
void print(ItemA item) {
Log.d(
"DEMO",
"I am ExpressionA, treat ItemA only. Animal name is " + item.getAnimalName()
);
}
}
class ExpB implements Exp<ItemB>
{
@Override
void print(ItemB item) {
Log.d(
"DEMO",
"I am ExpressionB, treat ItemB only. Color name is " + item.getColorName()
);
}
}
class ExpX<T> implements Exp<T>
{
@Override
void print(T item) {
Log.d(
"DEMO",
"I am ExpressionX, treated " + item.getClass().getSimpleName() + "."
);
}
}
振り分けクラス (Dispatcher class)
- AbstractDispatcher は Dispatcher pattern の汎用的なスーパークラスです。AbstractDispatcher を継承した Dispatcher は、Expression を継承した Exp を実装します。
abstract class AbstractDispatcher<X extends Expression>
{
private Map<Class<?>, X> expressions = new HashMap<>();
@SuppressWarnings("unchecked")
public <T> void set(Class<T> clazz, Expression<T> expression) {
expressions.put(clazz, (X) expression);
}
@Nullable
protected final X dispatch(@NonNull Object item) {
return expressions.get(item.getClass());
}
}
class Dispatcher<I, X extends Exp<I>>
extends AbstractDispatcher<X>
implements Exp<I>
{
@Override
void print(I item) {
X exp = dispatch(item);
if (exp == null) {
Log.d(
"DEMO",
"Unknown item: " + item.getClass().getSimpleName()
);
return;
}
exp.print(item);
}
}
実装例とログ
// setup expressions.
Dispatcher<Object, Exp<Object>> dispatcher = new Dispatcher<>();
{
dispatcher.set(ItemA.class, new ExpA());
dispatcher.set(ItemB.class, new ExpB());
dispatcher.set(ItemC.class, new ExpX<>());
dispatcher.set(ItemD.class, new ExpX<>());
// dispatcher.set(ItemA.class, new ExpB()); // error
}
// setup elements.
List<Object> list = new ArrayList<>();
{
list.add(new ItemB());
list.add(new ItemB());
list.add(new ItemC());
list.add(new ItemA());
list.add(new ItemB());
list.add(new ItemC());
list.add(new ItemA());
list.add(new ItemD());
list.add(new ItemE());
}
// execute.
for (Object it : list) {
dispatcher.print(it);
}
I am ExpressionB, treat ItemB only. Color name is purple
I am ExpressionB, treat ItemB only. Color name is purple
I am ExpressionX, treated ItemC.
I am ExpressionA, treat ItemA only. Animal name is cat
I am ExpressionB, treat ItemB only. Color name is purple
I am ExpressionX, treated ItemC.
I am ExpressionA, treat ItemA only. Animal name is cat
I am ExpressionX, treated ItemD.
Unknown item: ItemE
要素と処理の追加例
以下の例のように、要素とその要素に対する処理が任意に追加できます。既存のクラスを書き替える必要がないのが分るでしょう。
class ItemF
{
}
class ExpF implements Exp<ItemF>
{
@Override
void print(ItemF item) {
Log.d(
"DEMO",
"I am new ExpressionF, treat ItemF only."
);
}
}
// setup expressions.
Dispatcher<Object, Exp<Object>> dispatcher = new Dispatcher<>();
{
...
+++ dispatcher.set(ItemF.class, new ExpF());
}
// setup elements.
List<Object> list = new ArrayList<>();
{
...
+++ list.add(new ItemF());
}
Dispatcher Pattern の特徴
non-instusive (non-invasice)
Visitor パターンで使われる accept メソッドが不要なので、ライブラリで使われてるような変更できないクラスであっても要素クラスとして扱えます。
処理の共有
要素クラスと処理クラスは一対一の関係ではなく、複数の要素をひとつの処理クラスが担当することも可能です。その場合でも、既存のクラスを書き換える必要はありません。先の実装例の ExpX を参照して下さい。ExpX が担当する要素クラスが共通のスーパークラスを継承していれば、ExpX に処理を集約できます。
委譲によるポリモーフィズム
Dispatcher Pattern では、Map を使って要素と処理を紐付けます。key を「要素のクラス」、value を「対象要素をバインドした処理クラスのインスタンス」として Map に仕込んでおけば、任意の要素に対してどの処理を行うかを振り分けられるというわけです。これを「委譲によるポリモーフィズム」と表現してみました。
Map へ単純に仕込むだけだと「処理クラスにバインドされてない未知の要素クラス」が処理クラスに紐付けられてしまいます。そうすると実行時エラーとなり、type safety を満たさなくなります。それを解消するための工夫が Dispatcher#set() でした。「処理クラスにバインドされた要素クラス」のみを処理クラスの相棒として Dispatcher に登録できるように制限してます。
public <T> void set(Class<T> clazz, Expression<T> expression) {
expressions.put(clazz, (X) expression);
}
Visitor Pattern との比較
項目 | Visitor Pattern | Dispatcher Pattern |
---|---|---|
要素クラスの accept メソッド | 必要 | 不要(non-intusive) |
利用するポリモーフィズム | Overload によるポリモーフィズム | 委譲によるポリモーフィズム |
ディスパッチ | ダブル | シングル |
戻り値の多相化
Dispatcher から取得する戻り値は多相化できません。しかし、戻り値を新しい要素群と考えれば、戻り値に対して Dispatcher Pattern を適用できます。
ここでコード紹介すると長くなるため、別記事でコードだけ紹介します。
Dispatcher Pattern (戻り値の多相化)
今後の検証
自分では type safety に解決できていると考えてます。しかし、プログラマの猛者たちから見ると「だめだめ、穴だらけだよ。」という結果に終わるかもしれません。たとえ type safety が不成立だったとしても、ライブラリに含まれるクラスなど任意の要素を扱える点は Visitor Pattern よりも応用範囲が広いだろうと考えてます。
もしこの新しいパターンが type safety に成立しているなら、今後のデザインパターン関連の書籍では Visitor Pattern の代わりに Dispatcher Pattern が記載されていくでしょう。その時はぜひ、Stew Eucen の名を紹介してもらいたいものです(笑)