Skip to main content

The Match Maker Design Pattern - a New Place for the Actions

April 27, 2010



Software systems often deal with similar concepts, whose behavior differs only slightly. Classic Object-Oriented design deals with such cases using inheritance; overriding the calculateSalary() method in different Employee subclasses allows the rest of the application to remain oblivious to the subtle differences between the salary algorithms of Manager, Engineer, and AnnoyingCeoNephew.

Sadly, inheritance doesn't scale very well. You might get away with adding a toXml() method to the business model objects, but when the customer requires data export support for Excel, OpenOffice, CSV, JSON and Lotus 1-2-3 - you know inheritance just ain't gonna cut it. Another issue with inheritance is that when one holds an Employee reference to an object and needs to know the its actual type, one has to downcast, which probably means one is going to get some runtime exceptions.

Modern Object-Oriented design sometimes separates the actions from the objects, using mainly the Visitor pattern. Sadly, this solution is not applicable in many common situations - the classes must implement the accept() method, so if you're using a third party API - you're out of luck. If you wrote that API, you would be reluctant to use the visitor pattern, as adding new visitable objects in later releases would require adding methods to the visitor interface, thus breaking existing client code that implements it.

Another approach is using the instanceof operator all over the code, which normally creates huge if-else chains, is hard to test and debug, and is prone to logic errors as the order of the if checks has to be done up the type hierarchy - if you check for Employee at line 100, the check for Manager at line 300 might never be executed.

The "where does the action go" dilemma is actually an age-old problem, normally referred to as "The Expression Problem" (See the Resources section for some links). It boils down to "it is either easy to add types, or it is easy to add operations". This article is (yet another) poke on the problem - we will see how one can add actions to the system without modifying the business objects, add objects without changing the actions, and still keep things reusable, testable, and clear. As an aside, this pattern allows us to change the application behavior on the fly, lends itself to using closures, and allows Java to implement Scala-style case class pattern matching.

Unfortunately, this is not a silver bullet - static type safety is somewhat compromised. Later in the article we will offer an effectively type-safe solution.

Making Matches

As the astute reader might have expected, the match maker pattern involves an object that makes matches between business objects to objects that act on them. The usage, shown below, is simple. At the setup, the client code registers class-handler pairs. In our example, the handler classes implement EmployeeHandler, an interface with a method handle(Employee e).

// Setup
MatchMaker<class<? extends Employee>, EmployeeHandler> m = new MatchMaker();
m.registerHandler( Manager.class, new ManagerHandler() );
m.registerHandler( Engineer.class, new EngineerHandler() );
....
// Usage
for ( Employee emp : staff ) {
EmployeeHandler h = m.match(emp);
h.handle(emp);  // line X
}

Below is the code that deals with the managers:

class ManagerPrinter implements EmployeeHandler {
public void handle(Employee emp) {
System.out.println( emp.getGivenName() + " " + emp.getFamilyName() +
" manages the " + ((Manager)emp).getDepartmentName() + " dept." );
}
}

At a glance, it looks like we could get away with implementing a match maker using a simple Map. We can't.

Sooner or later the organization would hire an employee who deserves a class of his own (say, an AnnoyingCeoNephew, who also implements the infamous YouTubeUploader interface). When the above loop would get to that employee, no handler would be found, and so we will get a NullPointerException on line X. What's more, a movie documenting the embarrassment would probably be featured in YouTube.

Dealing With New Classes On The Fly

A MatchMaker instance does hold a Map mapping classes to handlers, but with a twist. When it is given an instance of a class for which a handler was not explicitly specified, it embarks on a breadth-first search (BFS) up the class hierarchy, starting from that object's class, looking for a class for which a handler was explicitly specified. A property of BFS is that it ensures that the class found is the closest one to the original object's class. Note that there might be a few classes with the same distance. Below is the BFS code; the rest of the class is removed for brevity. Full implementations and examples are available in the Resources section.

protected Handler getExplicitHandler(Class<? extends BaseClass> valClass) {
    // the BFS' "to be visited" queue
    Queue<Class<?>> queue = new LinkedList<Class<?>>();
    // the class objects we have visited
    Set<Class<?>> visited = new HashSet<Class<?>>();   
   
    queue.add(valClass);
    visited.add(valClass);

    while (!queue.isEmpty()) {
        Class<?> curClass = queue.remove();

        // get the super types to visit.
        List<Class<?>> supers =
                        new LinkedList<Class<?>>();
        for (Class<?> itrfce : curClass.getInterfaces()) {
            supers.add(itrfce);
        }
         // this would be null for interfaces.
        Class<?> superClass = curClass.getSuperclass();
        if (superClass != null) {
            supers.add(superClass);
        }

        for (Class<?> ifs : supers) {
            if (explicitHandlers.containsKey(ifs)) {
                return explicitHandlers.get(ifs);
            }
            if (!visited.contains(ifs)) {
                queue.add(ifs);
                visited.add(ifs);
            }
        }
    }
    return explicitHandlers.get(Object.class);
}

The BFS looks at implemented interfaces before looking at the superclass of the object. This behavior normally creates better matches, especially in shallow class hierarchies where most of the classes extend Object directly, and implement a few interfaces (a similar approach is taken by Javadoc's {@inheritDoc} attribute). Considering the superclass last also allows for a convenient way for specifying a default handler - just register a handler for the base class, and it will by returned if no other handler is found. The found handler is cached (this time on a simple Map); so next time, finding the handler would be an O(1) issue.

For instance, in Figure 1, SwingEngineer instances would be handled by H1, J2meEngineer instances by H2, and JeeEngineer instances by H3. When the company would hire a JTableMaster (blue) it will automatically be handled by the H1, which knows how to handle Swing Engineers and is probably the most appropriate handler for such an expert. H4 is a bit of a "catch-all", handling all employees for which no better matching handler was found.

Class hierarchy and handlers example
Figure 1. Class hierarchy and handlers example.

Making it (Almost) Statically Type-Safe

Seasoned developers would probably frown at the type safety of this pattern. The handlers have to downcast the object they get, and there's no guarantee that the handler registered to handle a class can actually handle it. It's time for some wildcards.

First, we need to get the handlers to declare what class they handle. The HandlerOf<ClassToHandle> interface does just that - it is a designator interface (i.e. no methods, like java.io.Serializable).

public interface HandlerOf<ClassToHandle> {}

Suppose we need to print a list of Employees to System.out. We define a base class to handle the base Employee class, both for code reuse and for having a default handler for any employee type in the list. Then we subclass it to handle subclasses (full code is available in the Resources section):

class EmployeePrinter implements HandlerOf<Employee>  {
public void handle( E emp ) {
printPersonalDetails(emp);
}

protected void printPersonalDetails( Employee emp ) {
...
}
}

class ManagerPrinter extends EmployeePrinter implements HandlerOf<Manager> {
@Override
public void handle( Manager m ) {
super.handle(m);
System.out.println("- Manages " + m.getDepartmentName());
}
}

Don't try to run it - it wouldn't compile. The problem is that one cannot override a type parameter. Once a generic class had its type parameter specified, there's no turning back. Luckily, we can turn sideways: make the base class abstract and limit its type parameter using wildcards:

abstract class 
AEmployeePrinter<E extends Employee> implements HandlerOf<E>  {
public void handle( E emp ) {
printPersonalDetails(emp);
}

protected void printPersonalDetails( Employee emp ) {
...
}
}

class EmployeePrinter extends AEmployeePrinter<Employee>{}

class ManagerPrinter extends AEmployeePrinter<Manager> {
@Override
public void handle( Manager m ) {...}
}

class J2meEngineerPrinter extends AEmployeePrinter<J2meEngineer> {
@Override
public void handle( J2meEngineer m ) {...}
}

class EngineerPrinter extends AEmployeePrinter<Engineer> {
@Override
public void handle( Engineer m ) {...}
}

Note that the compiler forces us to handle the correct class. If the ManagerPrinter would have implemented only a handle(Employee) method, the code wouldn't compile. Consequently, the downcasting is gone.

We now turn to the MatchMaker's register() method. It has to ensure that the client code cannot register a handler to a class it can't take care of:

public class 
MatchMaker<Input, Output extends HandlerOf<? super Input>>  {
    ...
    public <T extends Input>
    void registerHandler(Class<T> aClass,
                         HandlerOf<? super T> aHandler) {
    ...}
}

The above declaration might look slightly daunting, so let's look into it. We want to make sure that only handlers of a class, or superclasses of it, could be registered with that class. This is why we declare the handler as <? super T> rather than <T>. But we wouldn't want just any T - it has to be a T that extends Input. Constraining T is done at the beginning of the declaration, as is applies to all occurrences of T in the declaration. As shown in Figure 2, static analysis (NetBeans, in this case) can detect wrong handler-class registration at design time.

static analysis finds errors
Figure 2. The compiler complains about registering handlers for classes they can't handle.

How Type Safe is it?

Deep in the internals of the matchmaker, maps store the handlers as Objects, and cast them to Handlers, before returning them from the match() method. Drawing on Brian Goets' "Effectively Immutable" idiom, we could call this approach effectively type safe, since:

  1. The client code cannot enter anything that's not a Subclass of Output, hence downcasting to Output is safe;
  2. The BFS algorithm, when implemented correctly, will always return a handler capable of handling the instance; and
  3. The normal usage would be to get the handler from the match and use it immediately.

Still, switch the instance after the match, and get a ClassCastException on a downcast you did not do. Hey, I said it's not a silver bullet.

Summary

We have seen the Match Maker pattern, which allows programmers to:

  • add actions on objects without modifying the objects those actions operate on;
  • add objects and have appropriate actions automatically operate on them; and
  • work on a subclass while holding only a superclass reference to it, without downcasting.

We have seen two implementations, one simple yet not statically type safe, and one effectively type safe. Creating a full statically type safe solution is left as an exercise to the reader, providing the reader is looking for a subject for a PhD thesis.

I have used this this pattern in production code for analyzing emails, drawing tables, building UI, and handling JMS messages; I hope you'll find it useful too.

Resources


width="1" height="1" border="0" alt=" " />
Michael Bar-Sinai is a Senior Software Architect at Be'eri Print
AttachmentSize
matchmaker-sample-code.zip9.82 KB
Art20100427_complex-hierarchy.png42.62 KB
Art20100427_static-type-safety.png14.72 KB
Related Topics >> Java Tech   |   Programming   |   Featured Article   |   

Comments

Use reflection visitor

Use reflection visitor

While it is true that the standard Visitor pattern has all of the problems that you mention, here is an article from 2000 about how to eliminate those problems in Java using a very simple bit of reflection.

http://www.polyglotinc.com/reflection.html
aka
http://www.polyglotinc.com/articles.html#reflectVisitor

Thanks for your comment and

Thanks for your comment and the link. The reflection visitor is ineed a clever solution. However, it fails on adding new sub classes, throwing a java.lang.NoSuchMethodException at runtime. Changes to listing1: class Beagle
{ public String toString(){ return "I'm a Beagle"; } }
// new
class Triangle extends Beagle
{ public String toString(){ return "I'm a Triangle"; } }
// /new

class MyVisitor
....

Beagle b = new Beagle();
// new
Triangle t = new Triangle();
Object[] elements = new Object[] { b, c, s, t };
// /new
...
Output: Beagle:[I'm a Beagle]
Circle:[I'm a Circle]
Square:[I'm a Square]
java.lang.NoSuchMethodException: MyVisitor.visit(Triangle)
// Michael

It will work if the

It will work if the declaration "public void visit( Triangle x )..." that is missing from class MyVisitor is added.
Since the standard visitor pattern (as demonstrated in the vast majority of early texts) also requires adding a similar declaration, it is the same extra effort in that respect.

It is true though that if one wishes to use superclass signatures (ala Triangle extends Beagle) there needs a little bit more reflection as shown in another article from 2000:
http://www.javaworld.com/javaworld/javatips/jw-javatip98.html

Thanks for keeping me honest.