Chain of responsibility desing pattern
The Chain of Responsibility is one of my favourite design patterns; it can declutter giant switch statements with wonderful elegance.
The problem
I’ll start by presenting the problem first and comment on the pattern later. Say we’ve finished the business logic of our shiny new application (maybe this one) and now want to implement a user interface.
In this case, let the application be an image processing utility. The interface should allow the user to pass in arguments, modifying the output. The arguments may enable different filters that alter the image transformation, such as --rotate +90, --brightness +50 or --vertical-scale 0.7.
The most straightforward approach is to take the input, split it into argument–parameter pairs and run it through a giant switch statement like this:
pairedArgs.foreach { arg =>
arg.name match {
case "--rotate" => ...
case "--brightness" => ...
case "--vertical-scale" => ...
...
}
}
This solution works, but it has some notable problems. The biggest one is extensibility–if you remember the open-close principle (code should be open for extension, closed for modification) you may start to see the problem. Whenever we want to add a new filter, we have to go into this giant switch and write a new case for it.
Okay, that’s a single line of code – what’s the problem? If you’re working on a simple app that shows cat images, there probably isn’t one (unless you want to impress people with your OOP knowledge). But now imagine this is a mature project and you’re testing new filters that are not in production yet. Some arguments influence others or you may want to process multiple arguments with a single filter. You may also want some filters to be available only to users with a subscription. Oh, and don’t forget the enterprise customer that needs a filter to block all images of their CEO and Jeffrey Epstein from being processed. I’ll leave the decision whether you want to quit and pick up goat farming somewhere in the Alps to you and offer a solution to the extendability problem instead.
The solution
Behold: the Chain of Responsibility pattern. In the original Design Patterns book, the Gang of Four describe it as follows:
The Chain of Responsibility pattern establishes a chain within a system, so that a message can either be handled at the level where it is first received, or be directed to an object that can handle it.
You define a common interface and implement it with different handlers that you then chain together.Each handler can process part of the input or pass it entirely to the next one. In our image processing project, we might define the argument handler interface like this:
trait ArgumentHandler {
var next: ArgumentHandler
def handle(arguments: List[Argument]): Unit
}
and then one of the concrete handlers may look like this:
class BrightnessArgumentParser extends ArgumentHandler {
override def handle(arguments: List[Argument]): Unit = {
if (!arguments.exists(_.name == "--brightness")) {
next.handle(arguments)
return
}
// do the handling here
next.handle(arguments.filter((arg: Argument) => arg.name != "--brightness"))
}}
In this specific case, I want to ensure that by the end of the chain all arguments have been handled by their corresponding handlers. If not, I want to throw an exception for an invalid argument. To do that, I reduce the message by filtering the arguments by name, but note that in the original design pattern the message is either processed and the chain is stopped or it is passed further down.
Caveats
If your argument set is small or unlikely to change, going for the Chain of Responsibility pattern may be overkill. When handlers rely on shared state or interact in subtle ways, the chain may get brittle and become a giant pain in the ass to debug because of the extra layer of indirection.
Bonus tip: use an empty handler at the end
When passing down the message, you need to decide what to do when you get to the end of the chain. You may check whether the next handler reference is null and if it is, throw an exception. But now your handler does two things. It would be great if the handler could always pass the message downstream and not have this extra logic inside. To do that, you can implement an empty handler that always throws an exception when receiving a message and just default the next handler reference to that. No nulls, an extra billion dollars to spend.
If you want to check out the entire project this example is based on, take a look here, specifically at the console view.