This is the second article in our series on design patterns, in which we try not to introduce a new level of difficulty by using terminology that makes your head sound too big to explain design patterns. We will use real-life scenarios and simple examples to show the scenarios and problems that each design pattern uses.
In this article we are going to look at the decorator pattern, so what is the decorator pattern? The name may sound unfamiliar to you, but you’ve used this pattern a lot to solve problems in your life. You just didn’t know it was decorator mode.
Decorator pattern in life
Imagine that you live on a lower floor in the summer and are annoyed by the number of mosquitoes that come to visit your house at night. You realize that there are no window screens on your Windows, so if you don’t close them in time at night, a lot of mosquitoes will visit you. But if you want to feel the breeze at night and not be visited by mosquitoes, all you have to do is put window screens on your Windows.
Yeah, putting a window screen on a window is using decorator mode. We did not make any changes to the original window, it is still the same window, you can open and close, you can watch the scenery through it, you can feel the breeze. With the addition of window screens, our Windows have a new function, that is to keep mosquitoes out of the room. So we’ve expanded the Windows, but we haven’t changed the Windows.
There are many such examples in life, but I won’t list them all here. After reading this article, I believe you will have a deeper understanding of this design pattern. Then you can find more examples in your life to strengthen your understanding and mastery of this design pattern.
So what problems do we need to solve with this design pattern in development? We want to solve the problem of extending the properties and functions of existing objects without changing the properties and methods of existing objects.
Why, you wonder? First of all, you may not be able to modify existing objects. Why not? This may be because the object was introduced by a third-party library, is used globally in your code, or is an object that you are not familiar with and understand very well. In these cases, you can’t easily add new functionality to those objects. But you have to add some new functionality to this object to meet current development needs. In this case, the decorator pattern is a good solution to this problem. Let’s learn about it
Use an example to practice the decorator pattern
The boss who sells pancakes downstairs knows you can write a program, so he wants you to help write a small program to order food, to facilitate him to buy pancakes for customers to order food. The reward is to give you a discount of 88 percent when you buy pancakes. I feel good about it, so I agree.
When you’re ready to start, your boss tells you that he already has some code for his ordering system and wants you to leave it alone because he’s not sure if it’s ever been used in his ordering system. Changes can cause problems, so you can only add new features to the previous ones. The code given by the boss is as follows:
// Pancake fruit
class Pancake {
constructor() {
this.name = "Pancake fruit";
}
// Get the name of the pancake
getName() {
return this.name;
}
// Get the price
getPrice() {
return 5; }}Copy the code
The boss’s requirements are as follows:
- Previous code cannot be modified
- Pancakes can be served with eggs, sausage, and bacon, and there is no limit to the amount of each
- When the order is complete, the pancake can be displayed with the current ingredients and the price
You can’t modify existing code right now, but adding new features is a little bit more difficult. But it just so happens that you’ve just learned the Decorator pattern, and this design pattern solves this problem perfectly. And that’s without modifying the original code. You immediately go home and start working on your 88 percent discount.
A basic decoration of an existing object
Before we start decorating the original object in detail, we need to write a basic decorating class, as follows:
// The decorator needs to have the same interface as the decorator object
class PancakeDecorator {
// We need to pass in an instance of pancake fruit
constructor(pancake) {
this.pancake = pancake;
}
// Get the name of the pancake
getName() {
return `The ${this.pancake.getName()}`;
}
// Get the price of the pancake
getPrice() {
return this.pancake.getPrice(); }}Copy the code
If we look at the code above, you can see that PancakeDecorator is consistent with Pancake, except that the constructor passes an instance of Pancake.
The purpose of this basic decorator class is to make the specific decorator class developed later have the same interface with the decorated object, paving the way for the subsequent composition and delegation functions.
Develop specific decoration classes
We know the boss’s ingredients are eggs, bacon, and sausage. So we need to develop three specific decorator classes as follows:
// Pancakes and eggs
class PancakeDecoratorWithEgg extends PancakeDecorator {
// Get the name of the pancake and egg
getName() {
return `The ${this.pancake.getName()}➕ eggs `;
}
getPrice() {
return this.pancake.getPrice() + 2; }}/ / add sausage
class PancakeDecoratorWithSausage extends PancakeDecorator {
/ / add sausage
getName() {
return `The ${this.pancake.getName()}➕ sausage `;
}
getPrice() {
return this.pancake.getPrice() + 1.5; }}/ / add bacon
class PancakeDecoratorWithBacon extends PancakeDecorator {
/ / add bacon
getName() {
return `The ${this.pancake.getName()}➕ bacon `;
}
getPrice() {
return this.pancake.getPrice() + 3; }}Copy the code
From the code above, we can see that each specific decoration class corresponds to only one ingredient, and each specific decoration class, because it inherits from PancakeDecorator, has the same interface as the decorated class. In the getName method, we first get the name of the pancake currently passed in, and then add the name of the ingredient corresponding to the current decorator. In the getPrice method, we use the same method to get the price after adding the ingredients specified by the decorator.
With the above specific decorators written, our work is almost complete. Let’s write some test code to verify that our functionality meets the requirements. The test code is as follows:
let pancake = new Pancake();
/ / add the eggs
pancake = new PancakeDecoratorWithEgg(pancake);
console.log(pancake.getName(), pancake.getPrice());
/ / add sausage
pancake = new PancakeDecoratorWithSausage(pancake);
console.log(pancake.getName(), pancake.getPrice());
/ / add bacon
pancake = new PancakeDecoratorWithBacon(pancake);
console.log(pancake.getName(), pancake.getPrice());
Copy the code
The output is as follows:
Pancake fruit ➕ egg 7 pancake fruit ➕ egg ➕ sausage 8.5 pancake fruit ➕ egg milk sausage bacon 11.5Copy the code
The result is exactly what we expected, so our code above does a pretty good job of meeting our boss’s requirements. You can give it to your boss right away.
Decorator pattern composition and delegation
If you don’t fully understand the purpose of doing this from the above code, I will show you an example diagram of this pattern, which you are sure to understand quite well.
- Step 1: Invoke
PancakeDecoratorWithSausage
The instancegetPrice
Methods. - Step 2: Because
PancakeDecoratorWithSausage
The instancegetPrice
Method needs accessPancakeDecoratorWithEgg
So go to step 3. - Step 3: Because
PancakeDecoratorWithEgg
The instancegetPrice
Method needs accessPancakeDecorator
So go to step 4. - Step 4: Because
PancakeDecorator
The instancegetPrice
Method needs accessPancake
Go to step 5. - Step 5: Pass
Pancake
Example returns the price of unsweetened pancake fruit is 5 yuan. - Step 6:
PancakeDecorator
Example gets the price of the original pancake and returns that price. - Step 7:
PancakeDecoratorWithEgg
Instance fetchPancakeDecorator
The price returned is 5 yuan, plus the price of the egg ingredients is 2 yuan, so it returns 7 yuan. - Step 8:
PancakeDecoratorWithSausage
Instance fetchPancakeDecoratorWithEgg
The price returned by the instance is 7 yuan, plus the price of the toppings sausage is 1.5 yuan, and the return price is 8.5 yuan.
From the picture above we can clearly see how this process changes. We realized the price calculation of adding different ingredients through combination and delegation. The so-called delegation means that we do not directly calculate the current price, but need the method of another instance in the delegation method to obtain the corresponding price, until we access the price of the most original unsweetened pancake, and then return the result of the delegation one by one. Finally calculate the price after adding material. Do you feel that this process is similar to DOM event capture and bubbling?
By combination, we mean that we don’t need to know the current state of the pancake. We just need to take the instance of the pancake as an argument to the constructor of our concrete decoration class, and then generate a new instance of the pancake, so that we can add the appropriate ingredients to the pancake that comes in.
How about that? The decorator mode is pretty simple and useful. All right, we need to get this code to the owner of Pancake, have him run it, see what happens. You can experience the incomplete pancake ordering system here. The following GIF is a simple operation demonstration, so you can feel it in advance.
Some thoughts on the decorator model
Every time we learn a new knowledge, we should learn to integrate this knowledge into our existing knowledge system; For example, after studying the Decorator pattern, you might wonder where should I use this design pattern? Is there anything I already know that relates to this? Are there any downsides to this design pattern? Wait, wait, wait, wait, wait, wait, wait.
Some extensions of the Decorator pattern
React is a high-order component of React. Take a look at how the React documentation describes higher-order components:
A higher-order component (HOC) is an advanced technique in React for reusing component logic. HOCs are not part of the The React API, per se. They are a pattern that 01 from React’s compositional nature.
React uses higher-order components to reuse component logic in a composite manner, which is an advanced technique 😁. You’ve got this advanced technique now.
If you’re interested in the future of JavaScript, you probably know that in future versions of JavaScript, support for decorators will be added to the native level of the language. More details can be found at TC39 /proposal-decorators.
For example, if decorators were supported at the native level of the language, we could write code like this:
@annotation
class MyClass {}function annotation(target) {
target.annotated = true;
}
Copy the code
The code above is from an example of babel-plugin-proposal-decorators.
In the code above, @Annotation is the decorator for class MyClass, which adds a property annotated to our MyClass and sets it to true. This process allows us to add an attribute to MyClass without making any changes to the original class. Isn’t it great?
We can also verify this:
console.log(MyClass.annotated); # true
Copy the code
Scenarios where the Decorator pattern applies and possible problems
The Decorator pattern utilizes the properties of composition and delegation to allow us to add new functionality and properties without changing the functionality and properties of existing objects, allowing us to keep our code low coupled and extensible. It’s a good design pattern.
But there are potential problems with using the decorator pattern, because as more decorators increase the complexity of your code, make sure you use the decorator pattern in the right context.
This is the end of the article, if you have any comments and suggestions welcome to leave a message to me. You can also follow my official account, Guanshan, for more articles on design patterns and fun front-end knowledge.
Related reading recommendations:
- Design Mode Adventure Level 1: Observer mode