The Mediator Pattern in JavaScript

The mediator pattern is a way to separate the concerns of what code asks for something vs. what code does the thing.

Note: I have created a working demo of all the handlers and messages shown in this article.

This is similar to the previously posted observer pattern, that it creates a separation of concerns. While observers work on the back end of a data change or modification, the mediator’s responsibility is to create changes.

Show me the code

A mediator is a very simple object. It holds a collection of handlers. When something needs to be done, we send a request to the mediator. We will loop through all of the available handlers until we find something that can handle the request. The caller does not care at all about what code handles the request or how that request is handled.

classMediator{constructor(){this.handlers=[];}addHandler(handler){if(this.isValidHandler(handler)){this.handlers.push(handler);returnthis;}leterror=newError('Attempt to register an invalid handler with the mediator.');error.handler=handler;throwerror;}isValidHandler(handler){return(typeofhandler.canHandle==='function')&&(typeofhandler.handle==='function');}request(message){for(leti=0;i<this.handlers.length;i++){lethandler=this.handlers[i];if(handler.canHandle(message)){returnhandler.handle(message);}}leterror=newError('Mediator was unable to satisfy request.');error.request=message;returnerror;}}

Each handler conforms to a very basic contract. Our request handlers need to have two functions: canHandle() and handle(). Both functions take a message object.

canHandle(message): Returns true if the handler knows what to do with the given message. Messages can be distinguished by many possible features. Usually, the handler will inspect the message for various properties. I have also seen the mediator where every message has a type property. In this case, it is only necessary to inspect the type. This is certainly convenient, and it adds a bit of communication as to what is being done within the application.

// Create a new instance of a mediator. The mediator should be a singleton within// your application.letmediator=newMediator();mediator.addHandler(someHandler);mediator.addHandler(someOtherHandler);mediator.addHandler(yetAnotherHandler);// snip...// Depending on the size of your handler, you may need to add dozens - maybe// hundreds - of handlers.

Once the mediator is created and all of the handlers have been added, we simply make requests to the handler.

This is the highest possible separation of concerns. The caller has no interest in how the request is fulfilled. All it cares about is that it gets fulfilled and the contract of the reply object.

More handlers vs. more complex handlers

Our canHandle() can certainly add more complexity. At this point, it becomes a matter of preference which you would rather see in your code base. For example, you can have just a single handler with conditions in the handle() function.

consttempHandler={canHandle:function(message){return!!message.temp||message.temp===0;},handle:function(message){varreply={temp:message.temp};if(message.temp<60){reply.message='It is too cold!';}elseif(message.temp>90){reply.message='It is too hot!';}else{reply.message='It should be a pleasant day today!';}returnreply;}}

Or you can have a very simple handle() function with conditions in the canHandle() function. This, in turn, means we need multiple handlers.

consttooColdHandler={canHandle:function(message){returnmessage.temp<60;},handle:function(message){return{temp:message.temp,message:'It is too cold!'};}};consttooHotHandler={canHandle:function(message){return90<=message.temp;},handle:function(message){return{temp:message.temp,message:'It is too hot!'};}}constniceDayHandler={canHandle:function(message){return60<=message.temp&&message.temp<90;},handle:function(message){return{temp:message.temp,message:'It should be a pleasant day today!'};}};

Deciding which to use is up entirely to the developer. My preference is (almost always) to have more message handlers and to keep the handle() functions simple as possible. The smaller the handle() function, the easier it is to test.

Handlers and data operations

Handlers encapsulate work into a single handle() function. Instead of having code that looks like this…

The same amount of “code work” has to get done no matter what. I assert that the second option makes for far more maintainable code over the long term. In fact, this opens the door to scenarios where the folder structure itself is documenting of everything the application does.

Think about a new developer coming to this application. Think about the mapping that happens between the UI and the various handlers in the application. We can probably already envision our shopping cart screen, with Add, Remove, and Update Quantity buttons.

We can foresee the parts of the application that might require more effort than normal. For example, ChangePrice is separate from UpdateProduct. Why would that be the case? Perhaps changing the price of a product requires additional work to take place. It’s more than just UPDATE products SET description=$1 WHERE id=$2;. First, prices should be kept in a price history table, where the current price is the latest entry in the table. Second, shopping carts need to be updated.

Self-documenting code is the best code!

Handlers returning promises

Can handlers return promises? Yes. The only thing required by the mediator pattern is that the caller knows how to work with the reply. The caller doesn’t care who does the work how the work gets done. So, if the reply is a promise, then so be it, and the caller needs to know how to work with that.

A final note

You will notice two important issues when working with the mediator pattern. First, the handlers are registered in order. Second, once a handler is found, the mediator’s request() function immediately returns. I beg you, please do not exploit this.