kdocs
GitHub
SC - Software Architecture
SC - Software Architecture
  • About
    • Architectural Requirements (RAs)
    • Architectural Perspectives
  • Software Design
    • Microservices
      • Patterns
    • Monolithic
    • C4 Model
  • Software Architectures
    • Clean Architecture
    • DDD (Domain Driven Design)
      • Strategic Modeling
      • Tactical Modeling
    • Event Driven Architecture
      • CAP Theorem
    • Hexagonal Architecture (Ports and Adapters)
  • Design Patterns
    • Behavioral
    • Creational
    • Data Access
    • Structural
  • Practices
    • Clean Code
    • SOLID
  • Others
    • CQRS
Powered by GitBook
On this page
  • Chain of Responsibility
  • Circuit Breaker
  • Strategy
  • Command
  • Interpreter
  • Mediator
  • Observer
  • Publisher
  • Subscriber
  • Memento
  • State
  • Template Method
  • Visitor
  1. Design Patterns

Behavioral

How objects communicate with each other.

PreviousHexagonal Architecture (Ports and Adapters)NextCreational

Last updated 4 months ago

Chain of Responsibility

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.

Ride.ts
export Ride {
    ...
    finish(positions: Position[]): void {
        ...
        distanceBetweenThisAndNext = ...
        positionFare = FareCalculatorFactory.create(position.date).calculate(distanceBetweenThisAndNext);
        ...
    }
}

Command

Interpreter

Mediator

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.

A Mediator have a relationship of many→manymany \to manymany→many, meaning:

  • 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.

For this we can make these changes:

Ride.ts
export class Ride extends Mediator {
    ...
    finish(): void {
        ...
        this.notify('triggerPublishRideCompletedEvent', { rideId: this.getRideId(), fare: this.getFare() });
    }
}

As a curiosity:

When making the Ride extend Mediator, the Mediator is actually acting as an Observer, because it has a relationship of 1→many1 \to many1→many, since only the Ride Object can notify (create events).

FinishRide.ts
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.

RideCompletedEvent.ts
export class RideCompletedEvent {
   constructor(readonly rideId: string, readonly fare: number) {} 
}
Ride.ts
export class Ride {
    ...
    finish(): RideCompletedEvent {
        ...
        this.notify('rideCompleted', new RideCompletedEvent(this.getRideId(), this.getFare()));
    }
}

Observer

Similar to Mediator, but only ONE Object can be the publisher.

Allow many Objects (Subscribers) to subscribe to events that are broadcasted by an Object (Publisher).

An Observer have a relationship of 1→many1 \to many1→many, meaning:

  • 1 Object acts as the Publisher. (One or multiple Events)

  • Many Objects can subscribe and react to these Events.

Publisher

The class that will notify the Subscribers.

class Publisher {
    subscribers: Subscriber[];
    
    subscribe(Subscriber subscriber): void {
        this.subcribers.push(subscriber);
    };
    
    unsubscribe(Subscriber subscriber): void {
        this.subscribers.pop(subscriber);
    };
    
    notify(): void {
        for (let subscriber of this.subscribers) doSomething();
    };
}

Subscriber

The interface or class of objects that will get notified.

class Subscriber {
    constructor (readonly name) {}
    
    update(Publisher subject): void;
}

Memento

State

// Could be an Abastract Class instead of an Interface
export default interface RideStatus {
    value: string;
    request(): void;
    accept(): void;
    start(): void;
    finish(): void;
}

export class RequestedStatus implements RideStatus {
    value: string;
    constructor(readonly ride: Ride) { this.value = 'requested'; }
    
    // Cannot request the ride again if state is already requested
    request(): void { throw new Error('Invalid State'); }
    // Setup next possible status after request
    accept(): void { this.ride.setStatus(new AcceptedStatus(this.ride)); }
    start(): void { throw new Error('Invalid State'); }
    finish(): void { throw new Error('Invalid State'); }
}

export class AcceptedStatus implements RideStatus {
    value: string;
    constructor(readonly ride: Ride) { this.value = 'accepted'; }
    
    request(): void { throw new Error('Invalid State'); }
    // Cannot accept the ride again if state is already accepted
    accept(): void { throw new Error('Invalid State'); }
    // Setup next possible status after request
    start(): void { this.ride.setStatus(new InProgressStatus(this.ride)); }
    finish(): void { throw new Error('Invalid State'); }
}

export class InProgressStatus implements RideStatus {
    value: string;
    constructor(readonly ride: Ride) { this.value = 'in_progress'; }
    
    request(): void { throw new Error('Invalid State'); }    
    accept(): void { throw new Error('Invalid State'); }
    // Cannot start the ride again if state is already started
    start(): void { throw new Error('Invalid State'); }
    // Setup next possible status after request
    finish(): void { this.ride.setStatus(new CompletedStatus(this.ride)); }
}

export class CompletedStatus implements RideStatus {
    value: string;
    constructor(readonly ride: Ride) { this.value = 'completed'; }
    
    request(): void { throw new Error('Invalid State'); }    
    accept(): void { throw new Error('Invalid State'); }
    start(): void { throw new Error('Invalid State'); }
    // Cannot finish the ride again if state is already completed
    finish(): void { throw new Error('Invalid State'); }
}
class Ride {
    private status: RideStatus;
    constructor() {
        this.status = new RequestedStatus(this);
    }
    
    accept(): void { this.status.accept(); }
    start(): void { this.status.start(); }
    finish(): void { this.status.finish(); }
    
    getStatus(): string { return this.status.value; }
}

Template Method

  • Is an abstraction of a behavior part.

Visitor

The Catalog of Design Patterns
Logo