This pattern helps in creating failovers for Objects, so that if one of them fails another one will try to execute the logic.
Like a chain at each failure to execute, next object is called, retrying the execution.
It is possible to implement a retry on the service failed, before calling next.
Example
Lets suppose we have services to process payments, like PjBankGateway and CieloBankGateway which is a gateway for an external service.
So we will try to process the payment with one of them, and if for some reason it fails it will automatically try to run the payment process with the other.
PaymentProcessor.ts
export default interface PaymentProcessor {
next?: PaymentProcessor;
processPayment(input: Input): Promise<Output>;
}
export class PjBankProcessor implements PaymentProcessor {
constructor(readonly next?: PaymentProcessor) {}
async processPayment(input: Input): Promise<Output> {
try {
const pjBankGateway = new PjBankGateway();
return await pjBankGateway.createTransaction(input);
} catch(error: any) {
if (!this.next) throw new Error('Out of processors.');
return this.next.processPayment(input);
}
}
}
export class CieloBankProcessor implements PaymentProcessor {
constructor(readonly next?: PaymentProcessor) {}
async processPayment(input: Input): Promise<Output> {
try {
const cieloBankGateway = new CieloBankGateway();
return await cieloBankGateway.createTransaction(input);
} catch(error: any) {
if (!this.next) throw new Error('Out of processors.');
return this.next.processPayment(input);
}
}
}
The way done bellow is not necessarily the only way.
You could for instance create a class that would in some way chain the Processors.
Main.ts
const cieloProcessor = new CieloBankProcessor();
const pjBankProcessor = new PjBankProcessor(cieloProcessor);
// This is Typescript way with Annotations and Registry Pattern to inject Dependencies
Registry.getInstance().provide('paymentProcessor', pjBankProcessor);
Circuit Breaker
Strategy
Basically the implementation of a contract (interface).
Use Factories to reuse code that is similar.
The Factory decides which version of the Object to instanciate.
The Object that uses the Factory don't know which version will be used.
Example
Lets suppose that in a Ride Entity, when a ride ends we must calculate the ride fare based on the date and time of each position generated by the ride.
But we must use a different multipler depending on the date of the month and time of the day.
FareCalculator.ts
export default interface FareCalculator {
calculate(distance: number): number;
}
export class NormalFareCalculator implements FareCalculator {
calculate(distance: number): number {
return distance * 2;
}
}
export class OvernightFareCalculator implements FareCalculator {
calculate(distance: number): number {
return distance * 3;
}
}
export class SpecialDayFareCalculator implements FareCalculator {
calculate(distance: number): number {
return distance * 1;
}
}
export class FareCalculatorFactory {
static create(date: Date) {
if (date.getDate() === 1) return new SpecialDayFareCalculator();
if (date.getHours() > 22 || date.getHours() < 6) return new OvernightCalculator();
return new NormalFareCalculator();
}
}
Since the Factory can contain Independent Business Rules, FareCalculator can be a Domain Service.
It is like a middle-man or broker (mediator), this pattern help reducing coupling between two or multiple Objects, by intruducing a bus/channel for communication, so that one Object can notify the other Object whenever it has to.
It provides separation of concerns and eliminates code duplication.
Many Objects can notify/publish. (One or multiple events)
Many Objects can subscribe and react to these events.
Depending on how you use it, Mediator can also act as Observer.
Ex.:
If many different Objects generate events to many interested parties, it acts as Mediator.
If only one Object generate events to many different parties, it acts as Observer.
interface Handler {
// The Event name
event: string;
// What must be executed when this Event happens
callback: Function
}
class Mediator {
// The handlers are the interested parties
handlers: Handler[];
constructor() {
this.handlers = [];
}
// Here we register events to the handlers and set what should execute when
// the event happens.
register(event: string, callback: Function) {
this.handlers.push({ event, callback });
}
// When a notify of a specific event is called, all handlers that subscribed
// to this event get notified.
async notify(event: string, data: any) {
for (const handler of this.handlers) {
if (handler.event === event) await handler.callback(data);
}
}
}
Chaining Usecases Example
In this example, lets suppose that we have these three usecases (FinishRide, ProcessPayment and GenerateInvoice), and the last two should be called after the Ride ends.
To avoid injecting and calling ProcessPayment and GenerateInvoice inside FinishRide class, we use a Mediator to generate the nofication that the Ride ended.
Notify from Application Layer
Usecases
export class FinishRide {
@inject('mediator')
mediator?: Mediator;
async execute(input: Input): Promise<void> {
// Get the Ride to be finished and put it in a Domain Object
const ride = await this.rideRepository.getRideById(input.rideId);
// Mutate the Domain Object
ride.finish();
// Persist the Domain Object
await this.rideRepository.updateRide(ride);
// Notify the Ride Completion
await this.mediator.notify('rideCompleted', { rideId: ride.getRideId(), fare: ride.getFare() });
}
}
export class ProcessPayment {
async execute(): Promise<void> { ... }
}
export class GenerateInvoice {
async execute(): Promise<void> { ... }
}
Main.ts
const processPayment = new ProcessPayment();
const generateInvoice = new GenerateInvoice();
const mediator = new Mediator();
mediator.register('rideCompleted', async function (event: any) {
await processPayment.execute(event);
await generateInvoice.execute(event);
});
Registry.getInstance().provide('mediator', mediator)
Notify from Domain Layer
You could be more strict, and state that the Event notification is the Domain Object Ride responsability.
We avoid making the finish() method from Ride return the Event data, because otherwise, the method return type is tied to Events.
export class FinishRide {
@inject('mediator')
mediator?: Mediator;
async execute(input: Input): Promise<void> {
// Get the Ride to be finished and put it in a Domain Object
const ride = await this.rideRepository.getRideById(input.rideId);
/*
Note that the Mediator from `Ride` is a different instance of the Mediator
from `Main.ts`, and for this reason we must make the notify from `Ride`
trigger the notify for the injected Mediator.
*/
ride.register('triggerPublishRideCompletedEvent', async (event: any) => {
// Persist the Domain Object
// Moved inside here so that Ride is persisted before the notify.
await this.rideRepository.updateRide(ride);
// Notify the Ride Completion
await this.mediator.notify('rideCompleted', event);
});
// Mutate the Domain Object
ride.finish();
}
}
Notify from Domain Layer - With Domain Event
It is possible to define the Event, more specifically, as a Domain Event.
A Mediator have a relationship of many→many, meaning:
When making the Ride extend Mediator, the Mediator is actually acting as an Observer, because it has a relationship of 1→many, since only the Ride Object can notify (create events).
An Observer have a relationship of 1→many, meaning: