25 Behavioral Differences Between Template Strategy and State Patterns

25 Behavioral Differences Between Template Strategy and State Patterns #

Hello, I’m Ishikawa.

Today, let’s talk about the remaining behavioral patterns in design patterns. Personally, I think the remaining six patterns can be roughly divided into two categories. One category is more inclined towards the “strategy model” of design patterns, which includes the Strategy, State, and Template patterns. The other major category is more inclined towards “data transmission” design patterns, which includes the Mediator, Command, and Chain of Responsibility patterns. What are their similarities and differences? Let’s first take a look at their respective ideas and implementations.

Behavioral Patterns for Strategy-like Models #

First, let’s take a look at the three design patterns that are biased towards “strategy models”: Strategy, State, and Template.

Strategy Pattern #

First, let’s talk about the strategy pattern. Its core idea is to select strategies based on the context at runtime.

Let’s take an example. The tire adaptation of our car can be considered as a strategy pattern. For example, in the snowy Siberia, we can choose winter tires; if it’s just a daily grocery shopping car, we can choose regular tires; if it’s for off-road racing, we can switch to off-road tires.

Image

Next, let’s take a look at the implementation of this concept through a traffic light program. In this example, we can see that the traffic control (TrafficControl) determines the runtime context, and it can switch between different strategies by using the turn method. Traffic lights (TrafficLight) is an abstract class for strategies, and it can extend to concrete strategy classes based on the environment needs.

// encapsulation
class TrafficControl {
  turn(trafficlight) {
    return trafficlight.trafficcolor();
  }
}

class TrafficLight {
  trafficcolor() {
    return this.colorDesc;
  }
}

// strategy 1
class RedLight extends TrafficLight {
  constructor() {
    super();
    this.colorDesc = "Stop";
  }
}

// strategy 2
class YellowLight extends TrafficLight {
  constructor() {
    super();
    this.colorDesc = "Wait";
  }
}

// strategy 3
class GreenLight extends TrafficLight {
  constructor() {
    super();
    this.colorDesc = "Go";
  }
}

// usage
var trafficControl = new TrafficControl();

console.log(trafficControl.turn(new RedLight())); // Stop
console.log(trafficControl.turn(new YellowLight())); // Wait
console.log(trafficControl.turn(new GreenLight())); // Go

State Pattern #

Now let’s take a look at the state pattern. Its core concept is to switch different strategies based on different runtime states. So we can say that it is an extension of the strategy pattern.

Let’s take hotel reservations as an example. For example, we all have experience in booking hotels on some online travel portals. During the reservation, there are usually several different states. For example, before we pay, the order status may be “unconfirmed”, and we can either confirm or delete the order, but we cannot cancel because the reservation has not been successful yet. But when we have confirmed the reservation and made the payment, there are no more options to confirm or delete the order. At this point, we can only choose to cancel. Furthermore, many hotels require cancellations to be made only 24 hours before check-in, and if it is within 24 hours before check-in, even the cancel button may be disabled. At this point, we can only choose to check-in or communicate with customer service to cancel the reservation. This is the state pattern, which means that the program responds with different strategies based on different runtime states.

Image

Similarly, let’s modify the traffic light example we used for the strategy pattern and introduce the concept of state. Here, we can see that every time we execute the turn method to switch, the traffic light indication also updates along with the state in a cycle of red, yellow, and green.

class TrafficControl {
  constructor() {
    this.states = [new GreenLight(), new RedLight(), new YellowLight()];
    this.current = this.states[0];
  }
  turn() {
    const totalStates = this.states.length;
    let currentIndex = this.states.findIndex((light) => light === this.current);
    if (currentIndex + 1 < totalStates) this.current = this.states[currentIndex + 1];
    else this.current = this.states[0];
  }
  desc() {
    return this.current.desc();
  }
}

class TrafficLight {
  constructor(light) {
    this.light = light;
  }
}

class RedLight extends TrafficLight {
  constructor() {
    super('red');
  }
  desc() {
    return 'Stop';
  }
}

class YellowLight extends TrafficLight {
  constructor() {
    super('yellow');
  }
  desc() {
    return 'Wait';
  }
}

class GreenLight extends TrafficLight {
	constructor() {
		super('green');
	}
	desc() {
		return 'Go';
	}
}

// usage
var trafficControl = new TrafficControl();
console.log(trafficControl.desc()); // 'Go'
trafficControl.turn();
console.log(trafficControl.desc()); // 'Stop'
trafficControl.turn();
console.log(trafficControl.desc()); // 'Wait'

Template Pattern #

Finally, let’s take a look at the Template pattern. Its core idea is to define a business logic template in a method and delegate certain steps to be implemented in subclasses. So it is somewhat similar to the Strategy pattern.

Image

Below is an example of implementation. In this example, we see that the work in the employee class is a template, and the tasks are delegated to be implemented in the developer and designer subclasses. This is a simple design implementation of the Template pattern.

class Employee {
	constructor(name, salary) {
	this.name = name;
	this.salary = salary;
	}
	work() {
		return `${this.name} is responsible for ${this.tasks()}`;
	}
	getPaid() {
		return `${this.name}'s salary is ${this.salary}`;
	}
}

class Developer extends Employee {
	constructor(name, salary) {
		super(name, salary);
	}
	// details implemented by subclasses
	tasks() {
		return 'writing code';
	}
}

class Designer extends Employee {
	constructor(name, salary) {
		super(name, salary);
	}
	// details implemented by subclasses
	tasks() {
		return 'doing design';
	}
}

// usage
var dev = new Developer('John', 10000);
console.log(dev.getPaid()); // 'John's salary is 10000'
console.log(dev.work()); // 'John is responsible for writing code'
var designer = new Designer('Jane', 11000);
console.log(designer.getPaid()); // 'Jane's salary is 11000'
console.log(designer.work()); // 'Jane is responsible for doing design'

To summarize, from the above examples, we can see that whether it is the Strategy, State, or Template pattern, they are all based on some kind of “strategy model” to implement. For example, in the Strategy pattern, the strategies are switched based on the context; in the State pattern, the switch is based on the state; and finally, in the example of the Template pattern, some strategy templates are defined in the base class, while others are implemented in the subclasses.

Behavioral Patterns for Information Transmission #

Mediator Pattern #

The core of the Mediator pattern is to allow components to interact with each other through a central point. An example in real life is an air traffic control tower - airplanes cannot communicate directly with each other, so coordination is done through the ground control center. Ground control personnel need to ensure that all airplanes receive the necessary information for safe flying without colliding with other aircraft.

Image

Let’s take a look at how this pattern is implemented with some code. The TrafficTower class has methods to receive coordinates from each airplane and to get coordinates of other airplanes. Meanwhile, each Airplane registers its own coordinates with the TrafficTower and can request the coordinates of other airplanes. All this information is managed by the TrafficTower.

class TrafficTower {
  #airplanes;
  constructor() {
    this.#airplanes = [];
  }

  register(airplane) {
    this.#airplanes.push(airplane);
    airplane.register(this);
  }

  requestCoordinates(airplane) {
    return this.#airplanes.filter(plane => airplane !== plane).map(plane => plane.coordinates);
  }
}

class Airplane {
  constructor(coordinates) {
    this.coordinates = coordinates;
    this.trafficTower = null;
  }

  register(trafficTower) {
    this.trafficTower = trafficTower;
  }

  requestCoordinates() {
    if (this.trafficTower) return this.trafficTower.requestCoordinates(this);
    return null;
  }
}

// usage
var tower = new TrafficTower();

var airplanes = [new Airplane(10), new Airplane(20), new Airplane(30)];
airplanes.forEach(airplane => {
  tower.register(airplane);
});

console.log(airplanes.map(airplane => airplane.requestCoordinates()));
// [[20, 30], [10, 30], [10, 20]]

Command Pattern #

After discussing the Mediator pattern, let’s take a look at the Command pattern. The Command pattern allows us to separate the command itself from the object that initiates the command, which gives us more control over commands that have specific lifecycles or are executed in a queue. It also provides the ability to pass method calls as arguments, allowing methods to be executed on demand.

Image

Here’s an example of this pattern. The OperationManager receives tasks and executes them based on different commands such as StartOperationCommand, TrackOperationCommand, and CancelOperationCommand.

class OperationManager {
  constructor() {
    this.operations = [];
  }

  execute(command, ...args) {
    return command.execute(this.operations, ...args);
  }
}

class Command {
  constructor(execute) {
    this.execute = execute;
  }
}

function StartOperationCommand(operation, id) {
  return new Command(operations => {
    operations.push(id);
    console.log(`You have successfully started operation ${operation} with code ${id}`);
  });
}

function CancelOperationCommand(id) {
  return new Command(operations => {
    operations = operations.filter(operation => operation.id !== id);
    console.log(`You have canceled the operation with code ${id}`);
  });
}

function TrackOperationCommand(id) {
  return new Command(() =>
    console.log(`Your operation with code ${id} is currently in progress`)
  );
}

var manager = new OperationManager();

manager.execute(new StartOperationCommand("Cheetah", "318"));
// Returns: You have successfully started operation Cheetah with code 318
manager.execute(new TrackOperationCommand("318"));
// Returns: Your operation with code 318 is currently in progress
manager.execute(new CancelOperationCommand("318"));
// Returns: You have canceled the operation with code 318

The Command pattern can be used in many different situations, especially when creating highly interactive UIs, such as undoing actions in an editor. It allows for a high degree of decoupling between UI objects and behavior actions. This pattern can also be used as an alternative to callback functions because it facilitates the modular passing of behavior actions between objects.

Chain of Responsibility Pattern #

Lastly, let’s take a look at the Chain of Responsibility pattern. The core of the Chain of Responsibility pattern is to decouple the sender of a request from its receivers. It is implemented through an object chain, where each object can handle the request or pass it on to the next object in the chain. It’s worth noting that we mentioned event capturing and bubbling when discussing the Flyweight pattern earlier - JavaScript internally uses this pattern to handle event capturing and bubbling. Similarly, in the Flyweight example, we mentioned that jQuery achieves chained calling by using the Chain of Responsibility pattern.

Image

So how is this Chain of Responsibility implemented? It’s actually not complicated, as demonstrated in the following example. You can easily implement a simplified version of chain accumulation. The CumulativeSum class has an add method that iteratively adds the previous result and the parameter value, returning the result as a return value for the next method call.

class CumulativeSum {
  constructor(intialValue = 0) {
    this.sum = intialValue;
  }

  add(value) {
    this.sum += value;
    return this;
  }
}

// usage
var sum = new CumulativeSum();
console.log(sum.add(10).add(2).add(50).sum); // 62

Through the examples of these three patterns, we can see how data is passed between different objects. In the Mediator pattern, information is transmitted between multiple objects through a mediator in a network-like environment. In the Command pattern, information is transmitted between objects. Lastly, in the Chain of Responsibility pattern, we see information being transmitted in a pipeline-like manner. Therefore, these patterns can be considered as “data transmission” oriented design patterns.

Summary #

Today, I have introduced you to several different behavioral design patterns. So far, we have covered all the classic patterns.

The patterns we have discussed in this lesson are not only applicable in JavaScript, but also in most other languages. Therefore, they can be considered as “universal” patterns that are not tied to any specific language. In the next lesson, we will take a look at some design patterns that are unique to JavaScript.

Thought Question #

If you have used Redux, you should have used the time-traveling debugging feature in its developer tool. It allows you to move the application’s state forward, backward, or to any point in time. Do you know which (type of) behavioral design pattern(s) used in the implementation of this feature that we have learned today?

Please feel free to share your answers, exchange learning experiences, or ask questions in the comment section. If you find it helpful, you are also welcome to share today’s content with more friends. See you next time!