The most important design patterns you need to know as a developer (part 3)

The most important design patterns you need to know as a developer (part 3)
Photo by RepentAnd SeekChristJesus / Unsplash

After part 1 and part 2, where creational and structural design patterns were explained, the third and final part is about behavioral design patterns.

Behavioral design patterns are the patterns that provide efficient communication and interaction between classes and objects and improve responsibilities between entities.

We will go through 10 design patterns:

  1. Observer
  2. Chain of responsibility
  3. Command
  4. Iterator
  5. Mediator
  6. Memento
  7. State
  8. Strategy
  9. Template method
  10. Visitor

Each pattern will be described by use-case from real-life and TypeScript code examples.

1) Observer

Sometimes this pattern is also called Publish/Subscribe or Producer/Consumer but I will stick to the name Observer.

The observer is a design pattern that is used when you have too many relationships between one central object and many others.

This pattern provides an elegant way to manage situations when one object updates the state and needs to automatically notify and update other objects.

Usually, that central object is called a Subject, and dependent objects are Observers.

Even when you’re closing in on 100 years old, the experiences when you were young stay with you. My great grandmother is from western Ukraine, went through WWII, and saw a lot. Yet, amazingly, her fierce spirit shows through and mixes with her love for comedy. For this spontaneous shot, I found my Cinco de Mayo hat and plopped it on her. What makes the picture, though, is the life in her eyes and face. Be awake to your experiences.
- with love, Alex
Photo by RepentAnd SeekChristJesus / Unsplash

One of the best examples that embody this pattern is my neighbors Ane and Ina.

They are old ladies who like to observe everything from their windows. They see everything, and we don’t need security cameras in my building.

Let’s use them as an example for this pattern:

interface NosyNeighbor {
  update(subject: Person): void;
}

interface Person {
  attach(observer: NosyNeighbor): void;
  detach(observer: NosyNeighbor): void;
  notify(): void;
}

class Me implements Person {
  public hours: number;
  private observers: NosyNeighbor[] = [];

  public attach(observer: NosyNeighbor): void {
    const observerExists = this.observers.includes(observer);
    if (observerExists) {
      console.log("Me: Observer is already attached.");
      return;
    }
    console.log("Me: Attached an observer.");
    this.observers.push(observer);
  }

  public detach(observer: NosyNeighbor): void {
    const observerIndex = this.observers.indexOf(observer);
    if (observerIndex === -1) {
      console.log("Me: Observer doesn't exist.");
      return;
    }
    this.observers.splice(observerIndex, 1);
    console.log("Me: Detached an observer.");
  }

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

  public livingMyLife(): void {
    console.log("Me: Just living my life...");
    this.hours = Math.floor(Math.random() * (10 + 1));
    console.log(`Me: My state has just changed to: ${this.hours}`);
    this.notify();
  }
}

class NeighborIna implements NosyNeighbor {
  public update(subject: Person): void {
    if (subject instanceof Me && subject.hours < 3) {
      console.log("Ina: Reacted to the event.");
    }
  }
}

class NeighborAne implements NosyNeighbor {
  public update(subject: Person): void {
    if (subject instanceof Me && (subject.hours === 0 || subject.hours >= 2)) {
      console.log("Ane: Reacted to the event.");
    }
  }
}

At the start, we have 2 interfaces NosyNeighbor and Person.

NosyNeighbor contains only 1 method, update. This method executes actions when the subject (me) notifies.

On the other hand, the interface Person contains 3 methods:

  • attach - used to attach the observer
  • detach - used to detach the observer
  • notify - used to notify the observer about the event

Following up, there are 3 classes:

  • Me - this class implements the Person interface with previously explained methods. There is also one more method livingMyLife which is setting hours to a random number
  • NeighborIna - implements the NosyNeighbor interface with the update method. Ina’s watch is up until 3 pm so she only observes up until that time.
  • NeighborAne - Same as NeighborIna but her watch is from 1 pm am to 3 pm

Now let’s test this code:

const me = new Me();
const neighborIna = new NeighborIna();
me.attach(neighborIna);
const neighborAne = new NeighborAne();
me.attach(neighborAne);

me.livingMyLife();
me.livingMyLife();
me.detach(neighborAne);
me.livingMyLife();
/*
"Me: Attached an observer."
"Me: Attached an observer."
"Me: Just living my life..."
"Me: My state has just changed to: 1"
"Me: Notifying observers..."
"Ina: Reacted to the event."
"Me: Just living my life..."
"Me: My state has just changed to: 4"
"Me: Notifying observers..."
"Ane: Reacted to the event."
"Me: Detached an observer."
"Me: Just living my life..."
"Me: My state has just changed to: 9"
"Me: Notifying observers..."
*/

As you can see observers react to the state changes in the subject automatically.

2) Chain of responsibility

This pattern is good when you have specific request types which need to be handled by specific objects or handlers.

Each of these handlers can decide if they want to handle the request or pass it on to the next handler, and so on.

A details of a shop inside Mercado de La Boqueria, in Barcelona.
Photo by Jacopo Maia / Unsplash

A good example would be handling articles if the store.

Each article must be handled on a specific shelf, you can’t place meat near the fruits, or sweets near the milk.

Here is the presentation of this use case in the code:

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

abstract class ArticleHandler implements Handler {
  private nextArticleHandler: Handler;

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

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

class FruitHandler extends ArticleHandler {
  public handle(request: string): string {
    if (request === "Apple" || request === "Banana") {
      return `FruitHandler: Store the ${request}.`;
    }
    return super.handle(request);
  }
}

class MeatHandler extends ArticleHandler {
  public handle(request: string): string {
    if (request === "Bacon") {
      return `MeatHandler: Store the ${request}.`;
    }
    return super.handle(request);
  }
}

class SweetsHandler extends ArticleHandler {
  public handle(request: string): string {
    if (request === "Chocolate") {
      return `SweetsHandler: Store the ${request}.`;
    }
    return super.handle(request);
  }
}

First, there is an interface Handler which consists of 2 functions:

  • setNext - used to pass the request onto the next available handler
  • handle - used for implementation of handling the request

Next, there is the abstract class ArticleHandler which implements the above 2 methods.

After that, there are 3 concrete handlers, FruitHandler, MeatHandler, and SweetsHandler. These handlers are very similar, the only difference is the preference for specific requests.

Now let’s test this code:

function storeArticles(handler: Handler) {
  const articles = ["Bacon", "Apple", "Chocolate", "Milk", "Banana"];

  for (const article of articles) {
    console.log(`Next article is ${article}?`);
    const result = handler.handle(article);
    if (result) {
      console.log(`${result}`);
    } else {
      console.log(`${article} is not stored.`);
    }
  }
}

const fruitHandler = new FruitHandler();
const meatHandler = new MeatHandler();
const sweetsHandler = new SweetsHandler();
fruitHandler.setNext(meatHandler).setNext(sweetsHandler);

storeArticles(fruitHandler);
/*
"Next article is Bacon?"
"MeatHandler: Store the Bacon."
"Next article is Apple?"
"FruitHandler: Store the Apple."
"Next article is Chocolate?"
"SweetsHandler: Store the Chocolate."
"Next article is Milk?"
"Milk is not stored."
"Next article is Banana?"
"FruitHandler: Store the Banana." 
*/

storeArticles(meatHandler);
/* 
"Next article is Bacon?"
"MeatHandler: Store the Bacon."
"Next article is Apple?"
"Apple is not stored."
"Next article is Chocolate?"
"SweetsHandler: Store the Chocolate."
"Next article is Milk?"
"Milk is not stored."
"Next article is Banana?"
"Banana is not stored." 
*/

Function storeArticles is used to iterate over various types of articles and pass them to handlers.

As you can see, handlers either store the article on a shelf or pass it to the next handler.

After that, a chain of handlers is formed using setNext function.

Notice how if the first example when  fruitHandler is passed as a parameter, all items except milk are stored, as milk doesn’t have a handler.

In the second example, meatHandler is passed as a parameter and only meat and sweets articles are stored, as their handler are the only ones that are chained previously.

3) Command

This design pattern is often used when you have features with a lot of requests that can be executed in different ways and are all related to functionality.

With this pattern, you can avoid creating a huge number of classes and code repetition.

Geralt of Rivia Nendoroid
Photo by Daniel Lee / Unsplash

A good example of this design pattern is video games.

Usually, in video games, you have your character’s inventory which you can open in 2 or more ways:

  • Click the button on the in-game menu
  • Or press the keyboard shortcut, for example, the character “I”

Both of these commands are doing the same thing, they open the inventory menu in the game. So let’s implement this in the code:

interface Command {
  execute(): void;
}

class OpenInventoryKeyboardCommand implements Command {
  private videoGame: VideoGame;

  constructor(videoGame: VideoGame) {
    this.videoGame = videoGame;
  }

  public execute(): void {
    console.log("OpenInventoryKeyboardCommand: execute method called!");
    this.videoGame.openInventory();
  }
}

class OpenInventoryButtonCommand implements Command {
  private videoGame: VideoGame;

  constructor(videoGame: VideoGame) {
    this.videoGame = videoGame;
  }

  public execute(): void {
    console.log("OpenInventoryButtonCommand: execute method called!");
    this.videoGame.openInventory();
  }
}

class OperatingSystem {
  private commands: Command[];

  constructor() {
    this.commands = [];
  }

  public storeAndExecute(cmd: Command) {
    this.commands.push(cmd);
    cmd.execute();
  }
}

class VideoGame {
  public openInventory(): void {
    console.log("VideoGame: action open inventory executed!");
  }
}

At the begging there are 2 classes:

  • OpenInventoryKeyboardCommand - a class that accepts the game object in the constructor and implements the execute function, calling the openInventory function
  • OpenInventoryButtonCommand - same as OpenInventoryKeyboardCommand class

Next, 2 classes are the core of this pattern:

  • OperatingSystem - this is the invoker class that is used to store and execute all commands to the class with business logic. This class is not dependent on any specific commands, it just sends commands to the receiver class
  • VideoGame - this is a class that contains business logic, which means this class knows how to execute any command

Let’s see how it works:

const videoGame: VideoGame = new VideoGame();
const openInventoryKeyboardCommand: Command = new OpenInventoryKeyboardCommand(videoGame);
const openInventoryButtonCommand: Command = new OpenInventoryButtonCommand(videoGame);
const operatingSystem: OperatingSystem = new OperatingSystem();

operatingSystem.storeAndExecute(openInventoryKeyboardCommand);
operatingSystem.storeAndExecute(openInventoryButtonCommand);
/* 
"KeyboardShortcutCommand: execute method called!"
"VideoGame: action is being executed!"
"InGameMenuButtonCommand: execute method called!"
"VideoGame: action is being executed!"
*/

As you can see commands are executed and the video game performs the action of opening the inventory.

4) Iterator

When you have an iterable structure like a tree, stack, or list, you must traverse them to execute some operations.

To do this efficiently, but without exposing the internal logic or implementation, you can use the iterator design pattern.

Mood in food
Photo by Tina Guina / Unsplash

A good example would be an address book or cake recipe book search.

When you search for a specific person in the address book or cake recipe, you need to go through the book to find it.

Everything is better with food, so let’s implement this cake recipe book example in the code:

interface RecipeIterator {
  next(): any;
  hasNext(): boolean;
}

interface RecipeAggregator {
  createIterator(): RecipeIterator;
}

class CakeRecipeIterator implements RecipeIterator {
  private cakeRecipes: any[] = [];
  private index: number = 0;

  constructor(recipes: any[]) {
    this.cakeRecipes = recipes;
  }

  public next(): any {
    const result = this.cakeRecipes[this.index];
    this.index += 1;
    return result;
  }

  public hasNext(): boolean {
    return this.index < this.cakeRecipes.length;
  }
}

class CakeRecipes implements RecipeAggregator {
  private cakeRecipes: string[] = [];

  constructor(recipes: string[]) {
    this.cakeRecipes = recipes;
  }

  public createIterator(): RecipeIterator {
    return new CakeRecipeIterator(this.cakeRecipes);
  }
}

First, there are 2 simple interfaces RecipeIterator and RecipeAggregator.

Next, the core of this pattern is 2 classes CakeRecipeIterator and CakeRecipes:

  • CakeRecipeIterator - a simple class that takes the recipes in the constructor and implements 2 methods, next and hasNext. The first one returns the next recipe and hasNext checks if there is the next element in the iterable collection.
  • CakeRecipes - this class is a wrapper around an iterable collection that implements the createIterator method and returns an iterator instance.

Now let’s see this in action:

const cakes = [
  "New York Cheesecake",
  "Molten Chocolate Cake",
  "Tres Leches Cake",
  "Schwarzwälder Kirschtorte",
  "Cremeschnitte",
  "Sachertorte",
  "Kasutera",
];
const cakeRecipes: CakeRecipes = new CakeRecipes(cakes);
const it: CakeRecipeIterator = <CakeRecipeIterator>cakeRecipes.createIterator();

while (it.hasNext()) {
  console.log(it.next());
}
/*
"New York Cheesecake"
"Molten Chocolate Cake"
"Tres Leches Cake"
"Schwarzwälder Kirschtorte"
"Cremeschnitte"
"Sachertorte"
"Kasutera"
*/

As you can see all cake recipes are printed out with the help of an iterator.

5) Mediator

This pattern is a good choice when you have a big number of objects and their behavior is dependent on each other, so to reduce chaos in communication between them there is a mediator - a special object which is restricting direct communication between objects and forcing communication only through the mediator object.

Aerial view of a crossroad in Shah Alam, Malaysia
Photo by Firdouss Ross / Unsplash

One of the best examples of mediators in real life is police officers in situations where traffic lights are broken, so they have to guide the drivers and their cars and signal them when they can drive and when they must wait.

So let’s imagine there are 2 cars on opposite lanes and a patrol officer guiding the traffic:

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

class Car {
  protected mediator: Mediator;

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

class PatrolOfficer implements Mediator {
  private car1: Car1;
  private car2: Car2;

  constructor(c1: Car1, c2: Car2) {
    this.car1 = c1;
    this.car1.setMediator(this);
    this.car2 = c2;
    this.car2.setMediator(this);
  }

  public notify(sender: object, message: string): void {
    if (message === "Car1-left") {
      console.log(`PatrolOfficer: reacts on ${message}`);
      this.car2.stopAndWait();
    }

    if (message === "Car2-left") {
      console.log(`PatrolOfficer: reacts on ${message}`);
      this.car1.stopAndWait();
    }
  }
}

class Car1 extends Car {
  public driveLeft(): void {
    console.log("Car1: drive left!");
    this.mediator.notify(this, "Car1-left");
  }

  public driveStraight(): void {
    console.log("Car1: drive straight!");
    this.mediator.notify(this, "Car1-straight");
  }

  public stopAndWait(): void {
    console.log("Car1: stop and wait!");
    this.mediator.notify(this, "Car1-stop");
  }
}

class Car2 extends Car {
  public driveLeft(): void {
    console.log("Car2: drive left!");
    this.mediator.notify(this, "Car2-left");
  }

  public driveStraight(): void {
    console.log("Car2: drive straight!");
    this.mediator.notify(this, "Car2-straight");
  }

  public stopAndWait(): void {
    console.log("Car2: stop and wait!");
    this.mediator.notify(this, "Car2-stop");
  }
}

const car1 = new Car1();
const car2 = new Car2();
const policeOfficer = new PatrolOfficer(car1, car2);

car1.driveLeft();

car1.driveStraight();
car2.driveStraight();

car2.stopAndWait();
/* 
"Car1: drive left!"
"PatrolOfficer: reacts on Car1-left"
"Car2: stop and wait!"
"Car1: drive straight!"
"Car2: drive straight!"
"Car2: stop and wait!"
*/

The core of this pattern is PatrolOfficer class which implements the Mediator interface and notify method.

In that method, the mediator reacts only when the situation will cause a car crash, eg. car 1 needs to go left so car 2 needs to stop and wait, and vice-versa.

The next 2 classes are car objects with their functions driveLeft, driveStraight, and stopAndWait.

In each function, the mediator object is notified about the behavior and can react.

You can also see that when cars are driving straight mediator doesn’t intervene.

6) Memento

This design pattern is not used very often, but sometimes you need it in situations where you need to restore the previous state of an object without revealing any business logic in it.

Imagine a museum where you could buy things at a heck of a bargain price! Well, Between an old vase and a picture of Stalin, you can find old Soviet medals, Georgian books, maps, pins, badges, and kitchenware.

This place is located in Tbilisi, Georgia.
Photo by Tbel Abuseridze / Unsplash

A good example of a memento is the Maccy program I use every day.

Copy-paste is a very useful tool but when you copy something previously copied content is gone.

This is where Maccy comes in, it allows you to have a history of copied content and use it again. So it perfectly embodies the Memento design pattern with how it handles the clipboard state.

Let’s show it in the code:

class ClipboardState {
  private copiedContent: string;

  constructor(command: string) {
    this.copiedContent = command;
  }

  get Command(): string {
    return this.copiedContent;
  }

  set Command(command: string) {
    this.copiedContent = command;
  }
}

class StateManager {
  private state: ClipboardState;

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

  get State(): ClipboardState {
    return this.state;
  }

  set State(state: ClipboardState) {
    console.log("State:", state);
    this.state = state;
  }

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

  public setMemento(memento: Memento) {
    this.State = memento.State;
  }
}

class Memento {
  private state: ClipboardState;

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

  get State(): ClipboardState {
    console.log("get memento state");
    return this.state;
  }
}

class MementoManager {
  private memento: Memento;

  get Memento(): Memento {
    return this.memento;
  }

  set Memento(memento: Memento) {
    this.memento = memento;
  }
}

const state: ClipboardState = new ClipboardState("I copied this line of text");
const stateManager: StateManager = new StateManager(state);
const mementoManager: MementoManager = new MementoManager();

mementoManager.Memento = stateManager.createMemento();
stateManager.State = new ClipboardState("This is another line I copied");
stateManager.setMemento(mementoManager.Memento);
/* 
"State:", {copiedContent: "This is another line I copied"}
"get memento state"
"State:", {copiedContent: "I copied this line of text"}
*/

First, there is ClipboardState class, which is very simple. It can hold one copy, has a getter and setter for that copied content, and that’s it.

Next is StateManager class, which also has a getter and setter for  ClipboardState instance, but also createMemento and setMemento functions. This class is the middleman between copied content and memento.

After that, there is the Memento class, which can store only one instance of ClipboardState and contains a getter for it.

Finally, MementoManager is a class that is a wrapper for the Memento object, with a getter and setter for it.

Below is the example section, you can see how copied content “This is another line I copied” can be restored to the clipboard state.

7) State

State lets you modify object behavior when its internal state changes, so it can behave like a completely different object.

If you are familiar with finite-state machines, this pattern is a very similar concept.

Photo by Luke Porter / Unsplash

The coffee machine is a great example of a state design pattern:

  • when the coffee machine is turned on and ready to work
  • when is making a coffee
  • when the water tank is empty, it shows the symbol to fill it.

So basically, it has 3 different types of states. Of course, the coffee machine has more states but for the sake of simplicity, the code example can contain only these 3 above:

interface State {
  handle(machine: CoffeeMachine): void;
}

class CoffeeMachineReadyState implements State {
  public handle(machine: CoffeeMachine): void {
    console.log("CoffeeMachineReadyState: ready to work!");
    if (machine.isWaterTankEmpty()) {
      machine.State = new WaterTankEmptyState();
      return;
    }
    machine.State = new MakingCoffeeState();
  }
}

class WaterTankEmptyState implements State {
  public handle(machine: CoffeeMachine): void {
    console.log("WaterTankEmptyState: it is empty, please refill water tank!");
    machine.State = new CoffeeMachineReadyState();
  }
}

class MakingCoffeeState implements State {
  public handle(machine: CoffeeMachine): void {
    console.log("MakingCoffeeState: making coffee!");
    machine.takeWater();
    machine.State = new CoffeeMachineReadyState();
  }
}

class CoffeeMachine {
  private state: State;
  private waterLevel: number;

  constructor(state: State) {
    this.state = state;
    this.waterLevel = 20;
  }

  get State(): State {
    return this.state;
  }

  set State(state: State) {
    this.state = state;
  }

  public takeWater(): void {
    this.waterLevel -= 10;
  }

  public isWaterTankEmpty(): boolean {
    return this.waterLevel < 10;
  }

  public fillWaterTank(water: number): void {
    this.waterLevel = water;
  }

  public request(): void {
    this.state.handle(this);
  }
}

There is an interface State, followed by 3 classes that implement that interface:

  • CoffeeMachineReadyState - checks if the water tank is empty and if yes, it switches the machines state to WaterTankEmptyState, otherwise to MakingCoffeeState
  • WaterTankEmptyState displays the message that the water tank is empty
  • MakingCoffeeState which is displaying a message to make a coffee

Finally, there is CoffeeMachine class which contains a bunch of stuff, getter, and setter for a state, then functions to manage the water tank, handle state requests, and others.

Now let’s see how it works:

const coffeeMachine: CoffeeMachine = new CoffeeMachine(new CoffeeMachineReadyState());
coffeeMachine.request();
coffeeMachine.request();
coffeeMachine.request();
coffeeMachine.request();
coffeeMachine.request();
coffeeMachine.request();
coffeeMachine.fillWaterTank(50);
coffeeMachine.request();
coffeeMachine.request();
/* 
"CoffeeMachineReadyState: ready to work!"
"MakingCoffeeState: making coffee!"
"CoffeeMachineReadyState: ready to work!"
"MakingCoffeeState: making coffee!"
"CoffeeMachineReadyState: ready to work!"
"WaterTankEmptyState: it is empty, please refill water tank!"
"CoffeeMachineReadyState: ready to work!"
"MakingCoffeeState: making coffee!"
*/

Notice how the machine switches states and when the water tank is empty until it’s refilled it won’t make coffee.

8) Strategy

This pattern is very popular and it lets you create a close group of strategies in separate classes and make their objects exchangeable.

Go on a trip. Make an adventure.
Photo by Tabea Schimpf / Unsplash

A good example of strategy pattern application is open-world RPG games. They let you decide what you want to do next, so you have multiple choices:

  • You can roam free through the open world and do your own thing
  • You can do some side quests
  • You can do the main quest and progress the main story

So let’s implement this in the code:

interface Strategy {
  execute(): void;
}

class ExploreTheWorldStrategy implements Strategy {
  public execute(): void {
    console.log("ExploreTheWorldStrategy executed!");
  }
}

class SideQuestStrategy implements Strategy {
  public execute(): void {
    console.log("SideQuestStrategy executed!");
  }
}

class MainQuestStrategy implements Strategy {
  public execute(): void {
    console.log("MainQuestStrategy executed!");
  }
}

class Context {
  private strategy: Strategy;

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

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

  public executeStrategy(): void {
    this.strategy.execute();
  }
}

const context: Context = new Context(new SideQuestStrategy());

context.executeStrategy();

context.Strategy = new MainQuestStrategy();
context.executeStrategy();

context.Strategy = new ExploreTheWorldStrategy();
context.executeStrategy();

/* 
"SideQuestStrategy executed!"
"MainQuestStrategy executed!"
"ExploreTheWorldStrategy executed!"
*/

First, there are 3 classes representing different strategies you can do in the game.

These classes can have business logic inside them, but in this example, they just print the message.

Next, there is a Context class, which has the property to store one strategy. Keep in mind, that Context is not responsible for choosing the strategy, it only delegates the work to the preferred one.

Finally, in the end, you can see how each strategy can be swapped for a new one and executed.

9) Template method

This pattern is very useful when you have set of a specific instructions that can be used as a basis for different actions.

It allows you to define a core in the base class and then override the various steps in the subclasses without a change in the structure.

Photo by Priscilla Du Preez / Unsplash

For example, you are making apple pie and pumpkin pie. The First 4 steps are identical:

  • Add flour
  • Add eggs
  • Add water
  • Mix to make a dough

The only difference is the main pie ingredient, which will be apples or pumpkin, so you need to add that and bake the pie to eat it.

Let’s implement this in the code:

abstract class Pie {
  protected addFlour(): void {
    console.log("Pie: addFlour");
  }

  protected addEggs(): void {
    console.log("Pie: addEggs");
  }

  protected addWater(): void {
    console.log("Pie: addWater");
  }

  protected mix(): void {
    console.log("Pie: mix");
  }

  public makeDough(): void {
    console.log("template method 'makeDough' is called!");
    this.addFlour();
    this.addEggs();
    this.addWater();
    this.mix();
  }

  protected abstract addPieIngredient(): void;
  protected abstract bake(): void;
}

class ApplePie extends Pie {
  public addPieIngredient(): void {
    console.log(`ApplePie: addPieIngredient Apples!`);
  }

  public bake(): void {
    console.log(`ApplePie: bake!`);
  }
}

class PumpkinPie extends Pie {
  public addPieIngredient(): void {
    console.log(`PumpkinPie: addPieIngredient pumpkin!`);
  }

  public bake(): void {
    console.log(`PumpkinPie: bake!`);
  }
}

The main class is the Pie class, which contains a template method called makeDough.

This method calls for all other repetitive steps that are used to make pie dough. It also contains two methods that must be implemented in the subclasses, these are addPieIngredient and bake.

Next, there are 2 classes ApplePie and PumpkinPie that implement previously explained methods.

Now to the testing part:

const applePie: ApplePie = new ApplePie();
const pumpkinPie: PumpkinPie = new PumpkinPie();

applePie.makeDough();
applePie.addPieIngredient();
applePie.bake();

pumpkinPie.makeDough();
pumpkinPie.addPieIngredient();
pumpkinPie.bake();
/* 
"template method 'makeDough' is called!"
"Pie: addFlour"
"Pie: addEggs"
"Pie: addWater"
"Pie: mix"
"ApplePie: addPieIngredient Apples!"
"ApplePie: bake!"

"template method 'makeDough' is called!"
"Pie: addFlour"
"Pie: addEggs"
"Pie: addWater"
"Pie: mix"
"PumpkinPie: addPieIngredient pumpkin!"
"PumpkinPie: bake!"
*/

As you can see, the template method is executed so there is no need to repeat all these core steps in making pie dough, while specific steps such as adding apples or pumpkins are executed later.

10) Visitor

When you are in a situation where you need to separate business logic depending on the objects on which you operate, a visitor design pattern comes in handy.

Singer at gig
Photo by Austin Neill / Unsplash

Let’s take a singer for example. A singer will sing:

  • kid’s songs at kids’ birthdays,
  • his songs at his concert,
  • other songs or requested ones at weddings

So the singer is a visitor object.

Let’s try to implement this scenario in the code:

interface IOcassion {
  accept(singer: ISinger): void;
}

interface ISinger {
  singChildrenSongs(ocassion: ChildBirthday): void;
  singWeddingSongs(ocassion: Wedding): void;
  singConcertSongs(ocassion: Concert): void;
}

class ChildBirthday implements IOcassion {
  public accept(singer: ISinger): void {
    singer.singChildrenSongs(this);
  }

  public singBabyShark(): string {
    return "Sing 'Baby Shark' from Pinkfong";
  }
}

class Wedding implements IOcassion {
  public accept(singer: ISinger): void {
    singer.singWeddingSongs(this);
  }

  public singIGiveYouMyWord(): string {
    return "Sing 'I give you my word' from Dalmatino";
  }
}

class Concert implements IOcassion {
  public accept(singer: ISinger): void {
    singer.singConcertSongs(this);
  }

  public singIfYouLeaveMe(): string {
    return "Sing 'If You Leave Me' from Mišo Kovač";
  }
}

class Singer implements ISinger {
  public singChildrenSongs(ocassion: ChildBirthday): void {
    console.log(`Singer: ${ocassion.singBabyShark()}`);
  }

  public singWeddingSongs(ocassion: Wedding): void {
    console.log(`Singer: ${ocassion.singIGiveYouMyWord()}`);
  }

  public singConcertSongs(ocassion: Concert): void {
    console.log(`Singer: ${ocassion.singIfYouLeaveMe()}`);
  }
}

const ocassions = [new ChildBirthday(), new Wedding(), new Concert()];
const singer = new Singer();
for (const component of ocassions) {
  component.accept(singer);
}
/*
"Singer: Sing 'Baby Shark' from Pinkfong"
"Singer: Sing 'I give you my word' from Dalmatino"
"Singer: Sing 'If You Leave Me' from Mišo Kovač"
*/

First, there are 2 interfaces IOcassion and ISinger to describe the above types of occasions and types of songs that singers can perform.

Then there are 3 classes and all of them implement accept method which calls the targeted function from the singer instance.

Each class also contains a specific method for that occasion with a song for it.

Finally, there is a Singer class that implements all methods from the ISinger interface and accepts the occasion as a parameter.

In the end, you can see the output with songs that the singer is singing.

That’s all for the final part, hope you enjoyed it and find it useful!