loading...
Cover image for Use a Mediator in your Javascript Project to improve productivity

Use a Mediator in your Javascript Project to improve productivity

chenxeed profile image Albert Mulia Shintra Updated on ・4 min read

Heya! 👋

In this post, I would like to share a design pattern to help organize your application code structure, especially when you are working as a team and the code needs to be developed collectively.

Let's imagine a real use case to help us go through the tips.

Use Case Example

You need to build a payment form and a module to handle the payment. Let's say there's two developer to work on it, you and Albert. (Yes you, let's work together! 😘) On the discussion, you discussed with Albert that you're going to split the task: Albert will be working on the payment form, and you'll handle the payment module.

The scope is quite specific, since Albert is mostly only working on the client side interface while you will be working on the API. It usually means, Albert should be working on the HTML code and the form submission, while you are working on the method to send the payment data to the API and check the response.

If this app is handled in a single file, it might look like this:

<form onsubmit="return submitForm(event)">
  <div>Credit Card: <input type="text" name="credit-card"/></div>
  <div>CVV: <input type="text" name="cvv"/></div>
  <div><input type="submit" value="Submit"/></div>
</form>
// pagePayment.js

async function submitForm(e) {
  const creditCard = e.target[0].value;
  const cvv = e.target[1].value;
  const success = await sendPayment({
    creditCard,
    cvv
  });
  if (success) {
    alert('payment success!');
  }
}

async function sendPayment(data) {
  // let's just pretend this API exist ;)
  return fetch('/pay', {
    method: 'POST',
    body: JSON.stringify(data)
  });
}

Looking at the basic payment code above to receive credit card information and submit it to the API, it can (and definitely) still be improved based on the app specification. But in the current situation, you and Albert will have to modify the same file, which likely raise conflict on merging the code. 😰

Imagine these use cases to add:

  • Add validation to the form input
  • Add multiple payment services
  • Create different payment form for different payment services

To avoid the conflict, the code should be separated by concern. We can simply modularize the payment module, but if the form submission code directly call the payment module, it creates "dependency" with the form and thus making the payment module hard to change.

One solution to decouple the user interface code and the module code is, by having a "Mediator".

Mediator Pattern

Let's see what it means:

"Mediator is a behavioral design pattern that lets you reduce chaotic dependencies between objects. The pattern restricts direct communications between the objects and forces them to collaborate only via a mediator object." - refactoring.guru

With this concept, now you and Albert can modify and improve your own codebase without having too worried about breaking each other. Let's see what kind of "Mediator" will serve them:

// payment-mediator.js

const paymentMethod = {};

export function registerPaymentMethod(method, module) {
  if (paymentMethod[method]) {
    throw new Error(`Payment Method ${method} is already exist!`);
  }
  paymentMethod[method] = module;
}

export function sendPayment(method, data) {
  if (!paymentMethod[method]) {
    throw new Error(`Payment Method ${method} is not exist!`);
  }
  return paymentMethod[method].send(data);
}

Just like a traffic control, the mediator will keep the registered payment method and use it when someone needs to send the payment.

Pro tips: if you're a fan of Typescript, you can define the interface of payment module to make sure that the registered module have the expected interface.

Now let's get back to our dear developers, you and Albert 😄

For Albert, he can focus on improving his form submission script:

// pagePayment.js
import { sendPayment } from './payment-mediator.js';

async function submitForm(e) {
  const creditCard = e.target[0].value;
  const cvv = e.target[1].value;
  const success = await sendPayment('default', {
    creditCard,
    cvv
  });
  if (success) {
    alert('payment success!');
  }
}

As for you, you can improve and add more payment method independently as well:

// payment-module.js
import { registerPaymentMethod } from './payment-mediator.js';

function payDefault(data) {
  // let's just pretend this API exist ;)
  return fetch('/pay', {
    method: 'POST',
    body: JSON.stringify(data)
  });
}

registerPaymentMethod('default', payDefault);

And that's it for the basic mediator! Hopefully this makes you and Albert stay productive, able to work independently, and cheers for the collaboration 🍻

Consideration

The question may raise here, who will develop and maintain the Mediator then? I believe that an intermediary module like mediator shall be develop and maintained by anyone in the team, to keep it running and updated based on the new requirement.

Also, having a mediator is simply another module to maintain in the codebase, so it shall be a pattern that's agreed and embraced by your developer team to ensure certain concern are considered:

  • Is it overengineering?
  • Is it too much abstraction?
  • Is the codebase going to be improved for a long term?
  • Is the team fine with the mediator abstraction?

Let me know what do you think of the pattern and your concern as well.

Thanks a lot for reading my post, hope it helps and have an awesome day!

Posted on by:

chenxeed profile

Albert Mulia Shintra

@chenxeed

Web Developer with the passion of solving problem and clean solution.

Discussion

markdown guide