30 Programming Design Patterns

Nurul Huda (Apon)

Nurul Huda (Apon) / July 13, 2024

18 min read22 views

1. Singleton Pattern

Ensures a class has only one instance and provides a global point of access to it.

When to use: When exactly one instance of a class is needed to control the action.
Why to use: To prevent multiple instances of a class and provide a single point of access.

class Singleton {
    private static instance: Singleton;
    private constructor() {}
    static getInstance(): Singleton {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
}
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true

2. Factory Pattern

Creates objects without specifying the exact class of object that will be created.

When to use: When the exact type of the object is determined at runtime.
Why to use: To encapsulate object creation.

interface Product {
    operation(): string;
}

class ConcreteProduct1 implements Product {
    operation(): string {
        return 'ConcreteProduct1';
    }
}

class ConcreteProduct2 implements Product {
    operation(): string {
        return 'ConcreteProduct2';
    }
}

class Creator {
    static factoryMethod(type: string): Product {
        if (type === '1') {
            return new ConcreteProduct1();
        } else {
            return new ConcreteProduct2();
        }
    }
}

const product1 = Creator.factoryMethod('1');
console.log(product1.operation()); // ConcreteProduct1

3. Abstract Factory Pattern

Provides an interface for creating families of related or dependent objects without specifying their concrete classes.

When to use: When families of related objects need to be created.
Why to use: To ensure consistency among products.

interface AbstractFactory {
    createProductA(): AbstractProductA;
    createProductB(): AbstractProductB;
}

class ConcreteFactory1 implements AbstractFactory {
    createProductA(): AbstractProductA {
        return new ProductA1();
    }
    createProductB(): AbstractProductB {
        return new ProductB1();
    }
}

interface AbstractProductA {
    usefulFunctionA(): string;
}

interface AbstractProductB {
    usefulFunctionB(): string;
}

class ProductA1 implements AbstractProductA {
    usefulFunctionA(): string {
        return 'The result of ProductA1';
    }
}

class ProductB1 implements AbstractProductB {
    usefulFunctionB(): string {
        return 'The result of ProductB1';
    }
}

const factory1 = new ConcreteFactory1();
const productA1 = factory1.createProductA();
const productB1 = factory1.createProductB();
console.log(productA1.usefulFunctionA()); // The result of ProductA1
console.log(productB1.usefulFunctionB()); // The result of ProductB1

4. Builder Pattern

Separates the construction of a complex object from its representation, allowing the same construction process to create various representations.

When to use: When an object needs to be created with various configurations.
Why to use: To construct a complex object step by step.

class Product {
    public parts: string[] = [];
    public listParts(): void {
        console.log(`Product parts: ${this.parts.join(', ')}`);
    }
}

class Builder {
    private product: Product;
    constructor() {
        this.product = new Product();
    }
    public reset(): void {
        this.product = new Product();
    }
    public addPartA(): void {
        this.product.parts.push('PartA');
    }
    public addPartB(): void {
        this.product.parts.push('PartB');
    }
    public getProduct(): Product {
        return this.product;
    }
}

const builder = new Builder();
builder.addPartA();
builder.addPartB();
const product = builder.getProduct();
product.listParts(); // Product parts: PartA, PartB

5. Prototype Pattern

Creates new objects by copying an existing object, known as the prototype.

When to use: When the cost of creating a new object is expensive.
Why to use: To reduce the cost of creating objects.

class Prototype {
    public primitive: any;
    public component: object;
    public circularReference: ComponentWithBackReference;

    public clone(): this {
        const clone = Object.create(this);
        clone.component = Object.create(this.component);
        clone.circularReference = {
            ...this.circularReference,
            prototype: { ...this }
        };
        return clone;
    }
}

class ComponentWithBackReference {
    public prototype;
    constructor(prototype: Prototype) {
        this.prototype = prototype;
    }
}

const prototype = new Prototype();
prototype.primitive = 245;
prototype.component = new Date();
prototype.circularReference = new ComponentWithBackReference(prototype);

const clone = prototype.clone();
console.log(clone.primitive); // 245
console.log(clone.component); // Date object
console.log(clone.circularReference.prototype === prototype); // false

6. Adapter Pattern

Allows incompatible interfaces to work together.

When to use: When you want to use an existing class, but its interface is not compatible with the rest of your code.
Why to use: To enable collaboration between classes with incompatible interfaces.

class Target {
    request(): string {
        return 'Target: The default target\'s behavior.';
    }
}

class Adaptee {
    specificRequest(): string {
        return '.eetpadA eht fo roivaheb laicepS';
    }
}

class Adapter extends Target {
    private adaptee: Adaptee;

    constructor(adaptee: Adaptee) {
        super();
        this.adaptee = adaptee;
    }

    request(): string {
        return `Adapter: (TRANSLATED) ${this.adaptee.specificRequest().split('').reverse().join('')}`;
    }
}

const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);
console.log(adapter.request()); // Adapter: (TRANSLATED) Special behavior of the Adaptee.

7. Bridge Pattern

Decouples an abstraction from its implementation so that the two can vary independently.

When to use: When you want to separate a monolithic class into several class hierarchies.
Why to use: To avoid a permanent binding between an abstraction and its implementation.

interface Implementation {
    operationImplementation(): string;
}

class ConcreteImplementationA implements Implementation {
    operationImplementation(): string {
        return 'ConcreteImplementationA: Here\'s the result on the platform A.';
    }
}

class ConcreteImplementationB implements Implementation {
    operationImplementation(): string {
        return 'ConcreteImplementationB: Here\'s the result on the platform B.';
    }
}

class Abstraction {
    protected implementation: Implementation;

    constructor(implementation: Implementation) {
        this.implementation = implementation;
    }

    operation(): string {
        return `Abstraction: Base operation with:\n${this.implementation.operationImplementation()}`;
    }
}

const implementationA = new ConcreteImplementationA();
let abstraction = new Abstraction(implementationA);
console.log(abstraction.operation()); // Abstraction: Base operation with: ConcreteImplementationA: Here's the result on the platform A.

const implementationB = new ConcreteImplementationB();
abstraction = new Abstraction(implementationB);
console.log(abstraction.operation()); // Abstraction: Base operation with: ConcreteImplementationB: Here's the result on the platform B.

8. Composite Pattern

Composes objects into tree structures to represent part-whole hierarchies.

When to use: When you need to treat individual objects and compositions of objects uniformly.
Why to use: To simplify client code that deals with complex tree structures.

interface Component {
    operation(): string;
}

class Leaf implements Component {
    operation(): string {
        return 'Leaf';
    }
}

class Composite implements Component {
    private children: Component[] = [];
    
    add(component: Component): void {
        this.children.push(component);
    }

    remove(component: Component): void {
        const index = this.children.indexOf(component);
        this.children.splice(index, 1);
    }

    operation(): string {
        const results = [];
        for (const child of this.children) {
            results.push(child.operation());
        }
        return `Branch(${results.join('+')})`;
    }
}

const leaf = new Leaf();
const tree = new Composite();
const branch1 = new Composite();
const branch2 = new Composite();

branch1.add(leaf);
branch2.add(leaf);
tree.add(branch1);
tree.add(branch2);

console.log(tree.operation()); // Branch(Branch(Leaf)+Branch(Leaf))

9. Decorator Pattern

Adds additional responsibilities to an object dynamically.

When to use: When you need to add responsibilities to objects dynamically.
Why to use: To avoid subclassing for functionalities.

interface Component {
    operation(): string;
}

class ConcreteComponent implements Component {
    operation(): string {
        return 'ConcreteComponent';
    }
}

class Decorator implements Component {
    protected component: Component;

    constructor(component: Component) {
        this.component = component;
    }

    operation(): string {
        return this.component.operation();
    }
}

class ConcreteDecoratorA extends Decorator {
    operation(): string {
        return `ConcreteDecoratorA(${super.operation()})`;
    }
}

class ConcreteDecoratorB extends Decorator {
    operation(): string {
        return `ConcreteDecoratorB(${super.operation()})`;
    }


}

const simple = new ConcreteComponent();
console.log(simple.operation()); // ConcreteComponent

const decorator1 = new ConcreteDecoratorA(simple);
const decorator2 = new ConcreteDecoratorB(decorator1);
console.log(decorator2.operation()); // ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))

10. Facade Pattern

Provides a simplified interface to a complex subsystem.

When to use: When you need to provide a simple interface to a complex subsystem.
Why to use: To hide the complexities of the system.

class Subsystem1 {
    operation1(): string {
        return 'Subsystem1: Ready!';
    }
    operationN(): string {
        return 'Subsystem1: Go!';
    }
}

class Subsystem2 {
    operation1(): string {
        return 'Subsystem2: Get ready!';
    }
    operationZ(): string {
        return 'Subsystem2: Fire!';
    }
}

class Facade {
    protected subsystem1: Subsystem1;
    protected subsystem2: Subsystem2;

    constructor(subsystem1: Subsystem1, subsystem2: Subsystem2) {
        this.subsystem1 = subsystem1;
        this.subsystem2 = subsystem2;
    }

    operation(): string {
        let result = 'Facade initializes subsystems:\n';
        result += this.subsystem1.operation1() + '\n';
        result += this.subsystem2.operation1() + '\n';
        result += 'Facade orders subsystems to perform the action:\n';
        result += this.subsystem1.operationN() + '\n';
        result += this.subsystem2.operationZ();
        return result;
    }
}

const subsystem1 = new Subsystem1();
const subsystem2 = new Subsystem2();
const facade = new Facade(subsystem1, subsystem2);
console.log(facade.operation());

11. Flyweight Pattern

Uses sharing to support large numbers of fine-grained objects efficiently.

When to use: When a large number of similar objects are needed.
Why to use: To reduce memory consumption.

class Flyweight {
    private sharedState: any;

    constructor(sharedState: any) {
        this.sharedState = sharedState;
    }

    operation(uniqueState: any): void {
        const s = JSON.stringify(this.sharedState);
        const u = JSON.stringify(uniqueState);
        console.log(`Flyweight: Displaying shared (${s}) and unique (${u}) state.`);
    }
}

class FlyweightFactory {
    private flyweights: { [key: string]: Flyweight } = {};

    constructor(initialFlyweights: string[][]) {
        for (const state of initialFlyweights) {
            this.flyweights[this.getKey(state)] = new Flyweight(state);
        }
    }

    private getKey(state: string[]): string {
        return state.join('_');
    }

    getFlyweight(sharedState: string[]): Flyweight {
        const key = this.getKey(sharedState);

        if (!(key in this.flyweights)) {
            console.log('FlyweightFactory: Can\'t find a flyweight, creating new one.');
            this.flyweights[key] = new Flyweight(sharedState);
        } else {
            console.log('FlyweightFactory: Reusing existing flyweight.');
        }

        return this.flyweights[key];
    }
}

const factory = new FlyweightFactory([
    ['Chevrolet', 'Camaro2018', 'pink'],
    ['Mercedes Benz', 'C300', 'black'],
    ['Mercedes Benz', 'C500', 'red'],
]);

const flyweight1 = factory.getFlyweight(['Chevrolet', 'Camaro2018', 'pink']);
flyweight1.operation(['John Doe', 'CL234IR']);
const flyweight2 = factory.getFlyweight(['Mercedes Benz', 'C300', 'black']);
flyweight2.operation(['Jane Doe', 'CL234IR']);

12. Proxy Pattern

Provides a surrogate or placeholder for another object to control access to it.

When to use: When you need a more versatile or sophisticated reference to an object.
Why to use: To control access and add additional functionality.

interface Subject {
    request(): void;
}

class RealSubject implements Subject {
    request(): void {
        console.log('RealSubject: Handling request.');
    }
}

class Proxy implements Subject {
    private realSubject: RealSubject;

    constructor(realSubject: RealSubject) {
        this.realSubject = realSubject;
    }

    request(): void {
        if (this.checkAccess()) {
            this.realSubject.request();
            this.logAccess();
        }
    }

    private checkAccess(): boolean {
        console.log('Proxy: Checking access prior to firing a real request.');
        return true;
    }

    private logAccess(): void {
        console.log('Proxy: Logging the time of request.');
    }
}

const realSubject = new RealSubject();
const proxy = new Proxy(realSubject);
proxy.request(); // Proxy: Checking access prior to firing a real request. RealSubject: Handling request. Proxy: Logging the time of request.

13. Chain of Responsibility Pattern

Passes a request along a chain of handlers.

When to use: When more than one object can handle a request.
Why to use: To decouple sender and receiver.

interface Handler {
    setNext(handler: Handler): Handler;
    handle(request: string): string;
}

abstract class AbstractHandler implements Handler {
    private nextHandler: Handler;

    public setNext(handler: Handler): Handler {
        this.nextHandler = handler;
        return handler;
    }

    public handle(request: string): string {
        if (this.nextHandler) {
            return this.nextHandler.handle(request);
        }
        return null;
    }
}

class MonkeyHandler extends AbstractHandler {
    public handle(request: string): string {
        if (request === 'Banana') {
            return `Monkey: I'll eat the ${request}.`;
        }
        return super.handle(request);
    }
}

class SquirrelHandler extends AbstractHandler {
    public handle(request: string): string {
        if (request === 'Nut') {
            return `Squirrel: I'll eat the ${request}.`;
        }
        return super.handle(request);
    }
}

const monkey = new MonkeyHandler();
const squirrel = new SquirrelHandler();

monkey.setNext(squirrel);

console.log(monkey.handle('Banana')); // Monkey: I'll eat the Banana.
console.log(monkey.handle('Nut')); // Squirrel: I'll eat the Nut.

14. Command Pattern

Turns a request into a stand-alone object that contains all information about the request.

When to use: When you want to parameterize objects with operations.
Why to use: To encapsulate requests as objects.

interface Command {
    execute(): void;
}

class SimpleCommand implements Command {
    private payload: string;

    constructor(payload: string) {
        this.payload = payload;
    }

    public execute(): void {
        console.log(`SimpleCommand: See, I can do simple things like printing (${this.payload})`);
    }
}

class ComplexCommand implements Command {
    private receiver: Receiver;
    private a: string;
    private b: string;

    constructor(receiver: Receiver, a: string, b: string) {
        this.receiver = receiver;
        this.a = a;
        this.b = b;
    }

    public execute(): void {
        console.log('ComplexCommand: Complex stuff should be done by a receiver object.');
        this.receiver.doSomething(this.a);
        this.receiver.doSomethingElse(this.b);
    }
}

class Receiver {
    public doSomething(a: string): void {
        console.log(`Receiver: Working on (${a}).`);
    }

    public doSomethingElse(b: string): void {
        console.log(`Receiver: Also working on (${b}).`);
    }
}

class Invoker {
    private onStart: Command;
    private onFinish: Command;

    public setOnStart(command: Command): void {
        this.onStart = command;
    }

    public setOnFinish(command: Command): void {
        this.onFinish = command;
    }

    public doSomethingImportant(): void {
        if (this.onStart) {
            this.onStart.execute();
        }

        if (this.onFinish) {
            this.onFinish.execute();
        }
    }
}

const invoker = new Invoker();
invoker.setOnStart(new SimpleCommand('Say Hi!'));
const receiver = new Receiver();
invoker.setOnFinish(new ComplexCommand(receiver, 'Send email', 'Save report'));

invoker.doSomethingImportant();

15. Iterator Pattern

Provides a way to access elements of a collection sequentially without exposing its underlying representation.

When to use: When you need to traverse a collection without exposing its implementation.
Why to use: To provide a standard way to iterate over a collection.

interface Iterator<T> {
    next(): T;
    hasNext(): boolean;
}

interface Aggregator {
    getIterator(): Iterator<string>;
}

class ConcreteIterator implements Iterator<string> {
    private collection: ConcreteCollection;
    private position: number = 0;

    constructor(collection: ConcreteCollection) {
        this.collection = collection;
    }

    public next(): string {
        const item = this.collection.items[this.position];
        this.position += 1;
        return item;
    }

    public hasNext(): boolean {
        return this.position < this.collection.items.length;
    }
}

class ConcreteCollection implements Aggregator {
    public items: string[] = [];

    public getIterator(): Iterator<string> {
        return new ConcreteIterator(this);
    }
}

const collection = new ConcreteCollection();
collection.items.push('Item 1');
collection.items.push('Item 2');
collection.items.push('Item 3');

const iterator = collection.get

Iterator();
while (iterator.hasNext()) {
    console.log(iterator.next());
}

16. Mediator Pattern

Defines an object that encapsulates how a set of objects interact.

When to use: When you need to reduce the complexity of communication between objects.
Why to use: To promote loose coupling by keeping objects from referring to each other explicitly.

interface Mediator {
    notify(sender: object, event: string): void;
}

class ConcreteMediator implements Mediator {
    private component1: Component1;
    private component2: Component2;

    constructor(c1: Component1, c2: Component2) {
        this.component1 = c1;
        this.component1.setMediator(this);
        this.component2 = c2;
        this.component2.setMediator(this);
    }

    public notify(sender: object, event: string): void {
        if (event === 'A') {
            console.log('Mediator reacts on A and triggers following operations:');
            this.component2.doC();
        }

        if (event === 'D') {
            console.log('Mediator reacts on D and triggers following operations:');
            this.component1.doB();
            this.component2.doC();
        }
    }
}

class BaseComponent {
    protected mediator: Mediator;

    public setMediator(mediator: Mediator): void {
        this.mediator = mediator;
    }
}

class Component1 extends BaseComponent {
    public doA(): void {
        console.log('Component 1 does A.');
        this.mediator.notify(this, 'A');
    }

    public doB(): void {
        console.log('Component 1 does B.');
        this.mediator.notify(this, 'B');
    }
}

class Component2 extends BaseComponent {
    public doC(): void {
        console.log('Component 2 does C.');
        this.mediator.notify(this, 'C');
    }

    public doD(): void {
        console.log('Component 2 does D.');
        this.mediator.notify(this, 'D');
    }
}

const c1 = new Component1();
const c2 = new Component2();
const mediator = new ConcreteMediator(c1, c2);

c1.doA();
c2.doD();

17. Memento Pattern

Captures and externalizes an object's internal state without violating encapsulation, so the object can be restored to this state later.

When to use: When you need to restore an object to a previous state.
Why to use: To provide the ability to restore an object to its previous state.

class Memento {
    private state: string;

    constructor(state: string) {
        this.state = state;
    }

    public getState(): string {
        return this.state;
    }
}

class Originator {
    private state: string;

    public setState(state: string): void {
        console.log(`Originator: Setting state to ${state}`);
        this.state = state;
    }

    public save(): Memento {
        return new Memento(this.state);
    }

    public restore(memento: Memento): void {
        this.state = memento.getState();
        console.log(`Originator: State after restoring from Memento: ${this.state}`);
    }
}

class Caretaker {
    private mementos: Memento[] = [];
    private originator: Originator;

    constructor(originator: Originator) {
        this.originator = originator;
    }

    public backup(): void {
        console.log('Caretaker: Saving Originator\'s state...');
        this.mementos.push(this.originator.save());
    }

    public undo(): void {
        if (!this.mementos.length) {
            return;
        }
        const memento = this.mementos.pop();
        console.log('Caretaker: Restoring state to:', memento.getState());
        this.originator.restore(memento);
    }
}

const originator = new Originator();
const caretaker = new Caretaker(originator);

originator.setState('State1');
caretaker.backup();
originator.setState('State2');
caretaker.backup();
originator.setState('State3');

caretaker.undo(); // Caretaker: Restoring state to: State2
caretaker.undo(); // Caretaker: Restoring state to: State1

18. Observer Pattern

Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

When to use: When you need many other objects to receive an update when another object changes.
Why to use: To allow a single object to notify multiple other objects about changes in its state.

interface Observer {
    update(subject: Subject): void;
}

class ConcreteObserverA implements Observer {
    public update(subject: Subject): void {
        if (subject.state < 3) {
            console.log('ConcreteObserverA: Reacted to the event.');
        }
    }
}

class ConcreteObserverB implements Observer {
    public update(subject: Subject): void {
        if (subject.state === 0 || subject.state >= 2) {
            console.log('ConcreteObserverB: Reacted to the event.');
        }
    }
}

class Subject {
    public state: number;
    private observers: Observer[] = [];

    public attach(observer: Observer): void {
        const isExist = this.observers.includes(observer);
        if (isExist) {
            return console.log('Subject: Observer has been attached already.');
        }
        this.observers.push(observer);
    }

    public detach(observer: Observer): void {
        const observerIndex = this.observers.indexOf(observer);
        if (observerIndex === -1) {
            return console.log('Subject: Nonexistent observer.');
        }
        this.observers.splice(observerIndex, 1);
    }

    public notify(): void {
        console.log('Subject: Notifying observers...');
        for (const observer of this.observers) {
            observer.update(this);
        }
    }

    public someBusinessLogic(): void {
        console.log('Subject: I\'m doing something important.');
        this.state = Math.floor(Math.random() * 10);
        console.log(`Subject: My state has just changed to: ${this.state}`);
        this.notify();
    }
}

const subject = new Subject();

const observer1 = new ConcreteObserverA();
subject.attach(observer1);

const observer2 = new ConcreteObserverB();
subject.attach(observer2);

subject.someBusinessLogic();
subject.someBusinessLogic();

19. State Pattern

Allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

When to use: When an object must change its behavior at runtime depending on its state.
Why to use: To simplify state-specific behavior and transitions.

class Context {
    private state: State;

    constructor(state: State) {
        this.transitionTo(state);
    }

    public transitionTo(state: State): void {
        console.log(`Context: Transition to ${(<any>state).constructor.name}.`);
        this.state = state;
        this.state.setContext(this);
    }

    public request1(): void {
        this.state.handle1();
    }

    public request2(): void {
        this.state.handle2();
    }
}

abstract class State {
    protected context: Context;

    public setContext(context: Context) {
        this.context = context;
    }

    public abstract handle1(): void;
    public abstract handle2(): void;
}

class ConcreteStateA extends State {
    public handle1(): void {
        console.log('ConcreteStateA handles request1.');
        console.log('ConcreteStateA wants to change the state of the context.');
        this.context.transitionTo(new ConcreteStateB());
    }

    public handle2(): void {
        console.log('ConcreteStateA handles request2.');
    }
}

class ConcreteStateB extends State {
    public handle1(): void {
        console.log('ConcreteStateB handles request1.');
    }

    public handle2(): void {
        console.log('ConcreteStateB handles request2.');
        console.log('ConcreteStateB wants to change the state of the context.');
        this.context.transitionTo(new ConcreteStateA());
    }
}

const context = new Context(new ConcreteStateA());
context.request1();
context.request2();

20. Strategy Pattern

Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

When to use: When you have multiple algorithms for a specific task and you want to switch between them at runtime.
Why to use: To choose the appropriate algorithm at runtime.

interface Strategy {
    doAlgorithm(data: string[]): string[];
}

class ConcreteStrategyA implements Strategy {
    public doAlgorithm(data: string[]): string[] {
        return data.sort();
    }
}

class ConcreteStrategyB implements Strategy {
    public doAlgorithm(data: string[]): string[] {
        return data.reverse();
    }
}

class Context {
    private strategy: Strategy;

    constructor(strategy: Strategy) {
        this.strategy = strategy;
    }

    public setStrategy(strategy: Strategy) {
        this.strategy = strategy;
    }

    public doSomeBusinessLogic(): void {
        const result = this.strategy.doAlgorithm(['a', 'b', 'c', 'd', 'e']);
        console.log(result.join(','));
    }
}

const context = new Context(new ConcreteStrategyA());
context.doSomeBusinessLogic(); // a,b,c,d,e

context.setStrategy(new ConcreteStrategyB());
context.doSomeBusinessLogic(); // e,d,c,b,a

21. Template Method Pattern

Defines the skeleton of an algorithm in a method, deferring some steps to subclasses.

When to use: When you want to let subclasses redefine certain steps of an algorithm without changing its structure

.
Why to use: To prevent code duplication and ensure the algorithm's structure stays unchanged.

abstract class AbstractClass {
    public templateMethod(): void {
        this.baseOperation1();
        this.requiredOperations1();
        this.baseOperation2();
        this.hook1();
        this.requiredOperations2();
        this.baseOperation3();
        this.hook2();
    }

    protected baseOperation1(): void {
        console.log('AbstractClass says: I am doing the bulk of the work');
    }

    protected baseOperation2(): void {
        console.log('AbstractClass says: But I let subclasses override some operations');
    }

    protected baseOperation3(): void {
        console.log('AbstractClass says: But I am doing the bulk of the work anyway');
    }

    protected abstract requiredOperations1(): void;
    protected abstract requiredOperations2(): void;

    protected hook1(): void { }
    protected hook2(): void { }
}

class ConcreteClass1 extends AbstractClass {
    protected requiredOperations1(): void {
        console.log('ConcreteClass1 says: Implemented Operation1');
    }

    protected requiredOperations2(): void {
        console.log('ConcreteClass1 says: Implemented Operation2');
    }
}

class ConcreteClass2 extends AbstractClass {
    protected requiredOperations1(): void {
        console.log('ConcreteClass2 says: Implemented Operation1');
    }

    protected requiredOperations2(): void {
        console.log('ConcreteClass2 says: Implemented Operation2');
    }

    protected hook1(): void {
        console.log('ConcreteClass2 says: Overridden Hook1');
    }
}

const concreteClass1 = new ConcreteClass1();
concreteClass1.templateMethod();

const concreteClass2 = new ConcreteClass2();
concreteClass2.templateMethod();

22. Visitor Pattern

Lets you separate algorithms from the objects on which they operate.

When to use: When you need to perform operations across a heterogeneous collection of objects.
Why to use: To avoid polluting object classes with unrelated behaviors.

interface Visitor {
    visitConcreteComponentA(element: ConcreteComponentA): void;
    visitConcreteComponentB(element: ConcreteComponentB): void;
}

class ConcreteVisitor1 implements Visitor {
    public visitConcreteComponentA(element: ConcreteComponentA): void {
        console.log(`${element.exclusiveMethodOfConcreteComponentA()} + ConcreteVisitor1`);
    }

    public visitConcreteComponentB(element: ConcreteComponentB): void {
        console.log(`${element.specialMethodOfConcreteComponentB()} + ConcreteVisitor1`);
    }
}

class ConcreteVisitor2 implements Visitor {
    public visitConcreteComponentA(element: ConcreteComponentA): void {
        console.log(`${element.exclusiveMethodOfConcreteComponentA()} + ConcreteVisitor2`);
    }

    public visitConcreteComponentB(element: ConcreteComponentB): void {
        console.log(`${element.specialMethodOfConcreteComponentB()} + ConcreteVisitor2`);
    }
}

interface Component {
    accept(visitor: Visitor): void;
}

class ConcreteComponentA implements Component {
    public accept(visitor: Visitor): void {
        visitor.visitConcreteComponentA(this);
    }

    public exclusiveMethodOfConcreteComponentA(): string {
        return 'A';
    }
}

class ConcreteComponentB implements Component {
    public accept(visitor: Visitor): void {
        visitor.visitConcreteComponentB(this);
    }

    public specialMethodOfConcreteComponentB(): string {
        return 'B';
    }
}

const components = [new ConcreteComponentA(), new ConcreteComponentB()];

console.log('The client code works with all visitors via the base Visitor interface:');
const visitor1 = new ConcreteVisitor1();
for (const component of components) {
    component.accept(visitor1);
}

console.log('It allows the same client code to work with different types of visitors:');
const visitor2 = new ConcreteVisitor2();
for (const component of components) {
    component.accept(visitor2);
}

Subscribe to receive updates from me!