1. Definition and composition of template method pattern
The template method pattern is a very simple pattern that can be implemented using inheritance alone
The template method pattern consists of two parts, the first part is the abstract parent class, the second part is the concrete implementation child class. The algorithm framework of a subclass is usually encapsulated in an abstract superclass, including the implementation of some common methods and the execution order of all methods in the subclass. By inheriting this abstract class, subclasses also inherit the entire algorithm structure and can choose to override the methods of the parent class
Let’s say we have some parallel subclasses, some of the same behaviors, some of the different behaviors. If the same and different behaviors are mixed in the implementation of each subclass, it means that the same behaviors are repeated in each subclass. But in fact, the same behavior can be moved to a single place, and the template method pattern was created to solve this problem. In the template method pattern, the same parts of the subclass implementation are moved up into the parent class, leaving the different parts to be implemented by the subclass. That’s a good idea of generalization
Example –Coffee or Tea
Coffee and tea is a classic example that is often used to illustrate the template method pattern.
2.1 Make a cup of coffee first
The steps are as follows:
- Bring the water to a boil
- Brew coffee in boiling water
- 3. Pour the coffee into the cup
- Add sugar and milk
var Coffee = function(){};
Coffee.prototype.boilWater = function(){
console.log('Boil the water')}; Coffee.prototype.brewCoffeeGriends =function(){
console.log('Brew coffee in boiling water')
}
Coffee.prototype.pourInCup = function(){
console.log('Pour the coffee into the cup')
}
Coffee.prototype.addSugarAndMilk = function(){
console.log('With sugar and milk')
}
Coffee.prototype.init = function(){
this.boilWater()
this.brewCoffeeGriends()
this.pourInCup()
this.addSugarAndMilk()
}
var coffee = new Coffee();
coffee.init();
Copy the code
2.2 Make a pot of tea
The steps are as follows:
- Bring the water to a boil
- 2. Soak tea leaves in boiling water
- 3. Pour the tea into the cup
- 4. A twist of lemon
var Tea = function(){};
Tea.prototype.boilWater = function(){
console.log('Boil the water')}; Tea.prototype.steepTeaBag =function(){
console.log('Soak tea leaves in boiling water')
}
Tea.prototype.pourInCup = function(){
console.log('Pour the tea into the cup')
}
Tea.prototype.addLemon = function(){
console.log('Add lemon')
}
Tea.prototype.init = function(){
this.boilWater()
this.steepTeaBag()
this.pourInCup()
this.addLemon()
}
var tea = new Tea();
tea.init();
Copy the code
2.3 Isolate common ground
- The ingredients are different. One is coffee, one is tea, and they’re both abstracted as “drinks.”
- The way of soaking is different. Coffee is brewed, tea is soaked, they are abstracted as “brews”
- The spices are different. One is sugar and milk, one is lemon, and we’re abstracting them all as “spices.”
Sorted into the following four steps
- Bring the water to a boil
- 2. Brew drinks with boiling water
- 3. Pour the drink into the glass
- 4. Add seasoning
So, either brew or soak, give it a new method name, such as brew(). Similarly, whether you add sugar and milk or lemon, you can call it addCondiments()
Create an abstract superclass to represent the process of making a drink. Coffee or Tea
var Beverage = function(){}
Beverage.prototype.boilWater = function(){
console.log('Boil the water')
}
Beverage.prototype.brew = function(){} // Empty method, should have subclass overridden
Beverage.prototype.pourInCup = function(){} // Empty method, should have subclass overridden
Beverage.prototype.addCondiments = function(){} // Empty method, should have subclass overridden
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments()
}
Copy the code
2.4 Create Coffee subclasses and Tea subclasses
Beverage is just an abstract existence here, and it makes no sense to create an object of the Beverage class.
Create the coffee and tea classes and inherit the beverage classes
var Coffee = function(){};
Coffee.prototype = new Beverage();
// overwrite some methods in the abstract parent class. Only the behavior of "boilWater" can be directly used in the parent class of Beverage boilWater. All other methods need to be overwritten in the Coffee subclass
Coffee.prototype.brew = function(){
console.log('Brew coffee in boiling water')
}
Coffee.prototype.pourInCup = function(){
console.log('Pour the coffee into the cup')
}
Coffee.prototype.addCondiments = function(){
console.log('With sugar and milk')}var coffee = new Coffee()
coffee.init();
Copy the code
When the cofeee object’s init method is called, since neither the coffee object nor the coffee constructor’s prototype has a corresponding init method, the request is delegated along the prototype chain to the init method on coffee’s “parent” Beverage prototype
Tea class
var Tea = function(){};
Tea.prototype = new Beverage();
Tea.prototype.brew = function(){
console.log('Soak tea leaves in boiling water')
}
Tea.prototype.pourInCup = function(){
console.log('Pour the tea into the cup')
}
Tea.prototype.addCondiments = function(){
console.log('Add lemon')}var tea = new Tea()
tea.init();
Copy the code
Temp.prototype. init is the template method. The method encapsulates the algorithm framework of the subclass, which acts as an algorithm template to instruct the subclass to execute which methods in which order. In the method Beverage.prototype.init, every step in the algorithm is clearly shown to us
3. An abstract class
JavaScript does not provide syntactic support for abstract classes. The first purpose of an abstract class is to hide the concrete type of an object, which is not important in JavaScript because JavaScript is a “type-fuzzy” language
On the other hand, when we use stereotype chain inheritance in JavaScript to simulate traditional class inheritance, there is no compiler to help us do any kind of checking, and there is no way to guarantee that subclasses will override the “abstract methods” of their parent classes.
As we know, the method Beverage. Prototype. init as a template method has specified the algorithm framework of the subclass, the code is as follows:
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments()
}
Copy the code
What if our Coffee or Tea class forgets to implement one of these four methods? For example, if we forget to write the coffee.prototype. brew method, then when requesting the brew of the Coffee object, the request will follow the prototype chain to find the Beverage. The method of Beverage. Prototype. brew is an empty method so far, which obviously can not meet our needs.
In Java, the compiler guarantees that a subclass overrides an abstract method in its parent class, but in JavaScript these checks are not done. It is dangerous to write code without any form of warning, relying entirely on the programmer’s memory and consciousness, especially when we use the template method pattern, a design pattern that relies entirely on inheritance.
There are two workarounds
-
- The duck type is used to simulate interface checking to ensure that methods of the parent class are actually overridden in subclasses. However, simulated interface checks introduce unnecessary complexity and require the programmer to perform these interface checks voluntarily, which requires us to add some code to the business code that is irrelevant to the business logic
-
- Brew, etc., throw an exception directly, and if we forget to write the coffee.prototype. brew method because of carelessness, we will at least get an error when the program runs
Beverage.prototype.brew = function(){
throw new Error('Subclasses must override brew methods')
}
Beverage.prototype.pourInCup = function(){
throw new Error('Subclasses must override the pourInCup method')
}
Beverage.prototype.addCondiments = function(){
throw new Error('Subclasses must override the addCondiments method')}Copy the code
The advantage of the second scheme is that it is simple to implement and pays little extra cost. The downside is that we get the error information too late
Hook method
Using the template method pattern, we encapsulate the algorithm framework of the subclass in the parent class. These algorithms work for most subclasses in normal state, but what if there are some special “personality” subclasses? For example, we encapsulated the brewing sequence of beverages in the Beverage category:
- Bring the water to a boil
- 2. Brew drinks with boiling water
- 3. Pour the drink into the glass
- 4. Add seasoning
If some guests drink coffee without seasoning (sugar and milk), is there any way to exempt the subclass from this restriction since the parent class of Beverage has specified the 4 steps for making the Beverage?
Hooks can be used to solve this problem, and placing hooks is a common means of isolating changes. We place hooks in mutable parts of the parent class. Hooks can have a default implementation, and it is up to subclasses to decide whether they want to “hook” or not. The result of the hook method determines the next part of the template method, which is the next direction of the program, and thus the program has the possibility to change
In this example, we named the hook customerWantsCondiments, and then we put the hook into the Beverage class to see how we can get a cup of coffee that doesn’t need sugar or milk
var Beverage = function(){}
Beverage.prototype.boilWater = function(){
console.log('Boil the water')
}
Beverage.prototype.brew = function(){
throw new Error('Subclasses must override brew methods')
}
Beverage.prototype.pourInCup = function(){
throw new Error('Subclasses must override the pourInCup method')
}
Beverage.prototype.addCondiments = function(){
throw new Error('Subclasses must override the addCondiments method')
}
Beverage.prototype.customerWantsCondiments = function(){
return true; // Seasoning is required by default
}
Beverage.prototype.init = function(){
this.boilWater();
this.brew();
this.pourInCup();
if(this.customerWantsCondiments()){ // If the hook returns true, seasoning is required
this.addCondiments()
}
}
var CoffeeWithHook = function(){}
CoffeeWithHook.prototype = new Beverage();
CoffeeWithHook.prototype.brew = function(){
console.log('Brew coffee in boiling water')
}
CoffeeWithHook.prototype.pourIncup = function(){
console.log('Coffee goes into the cup')
}
CoffeeWithHook.prototype.addCondiments = function(){
console.log('With sugar and milk')
}
CoffeeWithHook.prototype.customerWantsCondiments = function(){
return window.confirm(Would you like any dressing, please? ')}var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();
Copy the code
5. The Hollywood Principle
Introducing a new design principle — the famous “Hollywood Principle”
Many new actors hand in their resumes in Hollywood and then go home and wait for a call. Sometimes, when the actor gets tired of waiting, he or she calls the acting company to ask about the situation, and the acting company often replies, “Don’t come to me, I’ll call you.”
In design, such rules are known as the Hollywood principle. Under the guidance of this principle, we allow the underlying components to tied himself to the high-level component, and high-level component can decide when and how to use these underlying components, high-level component treatment of underlying components, new actors, with performing arts company is “don’t call us, we will call you”.
The template method pattern is a typical use of the Hollywood principle, and its connection to the Hollywood principle is obvious. When we write a program using the template method pattern, it means that the subclass gives up control over itself and instead tells the subclass which methods should be called and when. As a subclass, it is only responsible for providing some design details
In addition, the Hollywood principles are often applied to other patterns and scenarios, such as publish-subscribe and callback functions
6. Is inheritance really necessary
The template method pattern is a design pattern based on inheritance. The parent class encapsulates the algorithm framework and method execution order of the subclass. After the subclass inherits the parent class, the parent class informs the subclass to execute these methods
The template method pattern is one of the few design patterns based on inheritance, but JavaScript doesn’t really provide true class inheritance, which is implemented through object-to-object delegation. That is, it borrows form from a language that provides class inheritance, but the template approach pattern here is not very authentic. And do you really need to inherit this heavy weapon to implement such an example in a language as flexible as JavaScript?
Guided by the Hollywood principle, the following code can achieve the same effect as inheritance
var Beverage = function(param){
var boilWater = function(){
console.log('Boil the water')}var brew = param.brew || function(){
throw new Error('Must pass brew method')}var pourInCup = param.pourInCup || function(){
throw new Error('pourInCup method must be passed')}var addCondiments = param.addCondiments || function(){
throw new Error('Must pass the addCondiments method')}var F = function(){}
F.prototype.init = funciton(){
boilWater();
brew();
pourInCup();
addCondiments();
}
return F;
}
var Coffee = Beverage({
brew: function(){
console.log('Brew coffee in boiling water')},pourInCup: function(){
console.log('Pour the coffee into the cup')},addCondiments: function(){
console.log('With sugar and milk')}})var Tea = Beverage({
brew: function(){
console.log('Soak tea leaves in boiling water')},pourInCup: function(){
console.log('Pour the tea into the cup')},addCondiments: function(){
console.log('Add lemon')}})var coffee = new Coffee();
coffee.init();
var tea = new Tea();
tea.init();
Copy the code
In this code, brew, pourInCup and addCondiments methods are passed into the Beverage function in turn. After the Beverage function is called, the constructor F.F class contains the “template method” f. prototype.init. As with inheritance, the template method still encapsulates the framework of the beverage subclass
7. Summary
The template method pattern is a typical design pattern that improves system extensibility by encapsulating changes. In traditional object-oriented languages, in a program that uses the template method pattern, the class of methods and the order of execution are the same, so we abstract this part of the logic into the template method of the parent class. However, how the methods of subclasses are implemented is variable, so we encapsulate this part of the changing logic in the subclass. By adding new subclasses, we can add new functionality to the system without changing the abstract superclass and other subclasses, which is also in accordance with the open-closed principle
In JavaScript, there is often no need to implement a template method pattern, and higher-order functions are the better choice
Note: This article refers to the book “JavaScript Design Patterns and Development Practices”