[JAVA] Dispatcher Pattern (a new design pattern that solves the Visitor Pattern's Expression Problem)

preface

At the beginning of May 2018, I finally understood the Visitor Pattern, which is considered to be the most difficult design pattern. The Visitor Pattern that I understand is as follows.

A pattern used when you want to switch processing for a collection containing different elements without branching with an if statement for each element.

Expression Problem

In the article linked below, I learned that the Visitor Pattern has an Expression Problem. Visitor pattern reconsideration

I understand that the main points of the Expression Problem are as follows.

When an element is added, the existing class must be rewritten to add processing for the added element.

After thinking for a moment, "It would be great if we could design a Visitor Pattern that can add elements and processes arbitrarily without rewriting the existing class," I found a design that seems to be good. That is the Dispatcher Pattern introduced in this article.

Dispatcher Pattern design

The protagonist of the Dispatcher Pattern is the Dispatcher class. All processing classes implements the Expression interface and are delegated to the Dispatcher class. The Dispatcher class uses "polymorphism by delegation (discussed below)" to distribute processing for all elements passed by the client.

Classes and interfaces

Item class

Element classes


class ItemA
{
    String getAnimalName() {
        return "cat";
    }
}

class ItemB
{
    String getColorName() {
        return "purple";
    }
}

class ItemC
{

}

class ItemD
{

}

class ItemE
{

}

Processing class (Expression class)

Processing classes


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

Sorting class group


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);
    }
}

Implementation example and log

Implementation example


// 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);
}

log


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

Additional examples of elements and processing

You can optionally add an element and processing for that element, as in the example below. You will find that you don't have to rewrite the existing class.

Class to add


class ItemF
{

}

class ExpF implements Exp<ItemF>
{
    @Override
    void print(ItemF item) {
        Log.d(
                "DEMO",
                "I am new ExpressionF, treat ItemF only."
        );
    }
}

Difference to add


// 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());
}

Features of Dispatcher Pattern

non-instusive (non-invasice)

Since the accept method used in the Visitor pattern is not required, even an immutable class used in the library can be treated as an element class.

Processing sharing

There is no one-to-one relationship between the element class and the processing class, and it is possible for one processing class to handle multiple elements. Even in that case, there is no need to rewrite the existing class. See ExpX in the previous implementation example. If the element classes that ExpX is in charge of inherit from a common superclass, processing can be aggregated in ExpX.

Polymorphism by delegation

Dispatcher Pattern uses Map to associate elements with processes. If key is set as "element class" and value is set as "instance of processing class to which target element is bound" in Map, it is possible to assign which processing is to be performed for any element. I tried to express this as "polymorphism by delegation".

If you simply add it to the Map, the "unknown element class that is not bound to the processing class" will be linked to the processing class. Doing so will result in a run-time error and will not meet type safety. Dispatcher # set () was a device to solve this problem. Only "element class bound to processing class" is restricted so that it can be registered in Dispatcher as a partner of processing class.

Dispatcher#set()


public <T> void set(Class<T> clazz, Expression<T> expression) {
    expressions.put(clazz, (X) expression);
}

Comparison with Visitor Pattern

item Visitor Pattern Dispatcher Pattern
Element class accept method necessary Unnecessary (non-intusiveļ¼‰
Polymorphism to use Polymorphism with Overload Polymorphism by delegation
dispatch double single

Multiphase return value

The return value obtained from Dispatcher cannot be polymorphic. However, if you think of the return value as a new set of elements, you can apply the Dispatcher Pattern to the return value.

Since it will be long to introduce the code here, I will introduce only the code in another article. Dispatcher Pattern (multi-phase return value)

Future verification

I think I have solved it to type safety. However, from the point of view of the programmer's heroes, the result may be "No, it's full of holes." Even if type safety is not established, I think that it will have a wider range of applications than the Visitor Pattern in that it can handle arbitrary elements such as classes included in the library.

If this new pattern holds for type safety, future design pattern books will include the Dispatcher Pattern instead of the Visitor Pattern. At that time, I would like you to introduce the name of Stew Eucen (laughs).

Recommended Posts

Dispatcher Pattern (a new design pattern that solves the Visitor Pattern's Expression Problem)
Design pattern ~ Visitor ~
I made a program in Java that solves the traveling salesman problem with a genetic algorithm
Rubocop-daemon as a countermeasure for the problem that RuboCop starts slowly