Overview
A recent thread on the Usenet newsgroup comp.object discussed replacing switch logic with polymorphism in a message processing system. My suggestion was to use the Chain of Responsibility pattern (Design Patterns) in order to decouple the creation of a concrete message instance from the message broker communication logic. The goal is to allow the addition of new message types without modifying the message broker, adhering to the Open Closed Principle.
Java Implementation
I implemented this in Java with just a handful of classes:
-
MessageBroker
The MessageBroker class is the server. It listens for messages on a specified port and dispatches the connections to an inner class named ClientHandler. - MessageHandler
MessageHandler is an abstract base class for all concrete message handlers. -
MessageHandlerFactory
The MessageHandlerFactory class, as its name suggests, is a factory for creating instances of concrete subtypes of MessageHandler. This implementation provides the methodaddHandler(String handlerName)to allow new message handlers to be added by name. -
MessageHandlerContext
Instances of MessageHandlerContext are passed to MessageHandler subtypes to provide access to the execution context, including input and output streams. -
UnknownMessageFormatException
The UnknownMessageFormatException is thrown by the constructors of MessageHandler subtypes if the subtype cannot process the message. This exception is caught by the MessageHandlerFactory during processing of the Chain of Responsibility.
Two example message handlers are also included:
-
AddHandlerMessageHandler
AddHandlerMessageHandler is a concrete subtype of MessageHandler that handles messages to add additional message handlers to the MessageBroker. -
EchoMessageHandler
EchoMessageHandler is a concrete subtype of MessageHandler that simply echoes the message received back to the sender.
Caveats
This is example code, not production code. Synchronization and thread pooling, for example, are not used. Error handling and recovery is minimal at best. No scalability or performance testing has been done. Message handlers can only be added, not removed.
Usage
After compiling the source, the server can be started with the command:
java org.softwarematters.example.MessageBroker.MessageBroker
The default port is 8079; an alternative can be passed on the command line.
MessageBroker's main method bootstraps the server by adding the AddHandlerMessageHandler. This allows additional handlers to be added dynamically. To demonstrate this, connect to the server with:
telnet localhost 8079
(or whatever port was specified when MessageBroker was started) and send the string:
CONTROL.ADD_HANDLER org.softwarematters.example.MessageBroker.EchoMessageHandler
Reconnect via telnet and send the string:
ECHO foo bar baz
You should see foo bar baz echoed back.
Implementation Choices
Dynamic Registration of Message Handlers
In this implementation, new MessageHandler subtypes are registered by name. The MessageBroker passes the name to the MessageHandlerFactory where a Class of the appropriate type is created, if possible. There are a large variety of alternative implementations that meet the same design goals, including:
-
Reading class files from a known directory.
The MessageHandlerFactory would monitor this directory for changes and register new MessageHandler subtypes when they appear. -
Registration of a proxy object.
Each concrete MessageHandler subtype would connect to the MessageBroker and register a proxy. The MessageBroker would provide a separate port for these commands. The proxy could either contain all of the functionality required for message handling or could dispatch messages to a separate process. -
Jini.
Sun's Jini network technology is a powerful architecture that enables the creation of distributed systems that are highly adaptive and supportive of change.
MessageHandlerContext
The MessageHandlerContext class is a bit of a hack, in the sense of building furniture with an axe rather than something with hack value . Concrete subtypes of MessageHandler are, of necessity, coupled to the MessageHandler abstract base class. The use of MessageHandlerContext couples those subtypes to two other classes in the same package, namely MessageHandlerContext and MessageBroker. Not all message handlers will require all, or even any, of the context information provided. On the other hand, the Reuse / Release Equivalence Principle and related granularity guidelines suggest that this isn't a huge problem. Still, in a production system I'd work harder to eliminate this coupling.
Common Lisp Implementation
For a different view of the same concepts, I implemented the message broker in Common Lisp, still using the Chain of Responsibility pattern to allow dynamic addition of new message types. All the source is in the file message-broker.lisp.Caveats
As with the Java implementation, this is example code, not production code. Error handling is minimal and the code hasn't even been compiled (yes, Common Lisp is a compiled language). Finally, the code is dependent on the SBCL networking and threading packages.Usage
Start SBCL:
sbcl
Load message-broker.lisp:
* (load "message-broker.lisp")
This will start the server in its own thread with only an echo message handler registered. To demonstrate this, connect to the server with:
telnet localhost 8079
and send the string:
ECHO foo bar baz
You should see foo bar baz echoed back.
To add a new handler, change to the appropriate package and call add-handler from the SBCL prompt:
* (in-package :org.softwarematters.message-broker)
* (add-handler *message-broker* #'rot13-message-handler)
Reconnect via telnet and test this handler by sending the string:
ENCRYPT.ROT13 foo bar baz
You should see sbb one onm echoed back.
To stop the server, quit out of SBCL:
* (quit)
Implementation Choices
It would easily be possible to create the classes of the Java implementation directly using the Common Lisp Object System (CLOS), but such a direct translation wouldn't take advantage of Common Lisp's advantages. Peter Norvig described how many standard design patterns are unnecessary or transparent in languages such as Common Lisp . The Java MessageHandler class is an example of the Strategy pattern, used because Java lacks proper closures . Functions are first class objects in Common Lisp, eliminating the need for both an explicit MessageHandler class and its associated factory. Instead of creating concrete subtypes of a MessageHandler class via a MessageHandlerFactory, this implementation allows message handler functions to be registered directly with an instance of message-broker.If more complex behavior were required, a message-handler class could, of course, be used. The factory class would still be unnecessary.
Summary
The code here shows how to implement the Chain of Responsibility pattern to allow the behavior to be added dynamically existing, running system.The most interesting result of this exercise for me is the significant difference in the amount of code required for the Java implementation compared with the Common Lisp implementation. MessageBroker.java alone has nearly twice the SLOCS of all of message-broker.lisp. If this difference remains when both implementations are extended to production quality in terms of performance and resiliency, it strongly suggests that Common Lisp is significantly more expressive and powerful than Java.
Please contact me by email if you have any questions, comments, or suggestions.