Programming
JavaScript and Design Patterns - Chapter 2 π
π€ Intro
Hello fellow coders! Today we will talk about another interesting Design Pattern responsible for dynamically adding behavior to the existing classes - The Decorator Pattern.
The decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically. Let me explain it by using examples.
β Implementation
USER DECORATOR
1const User = function(name) {
2 this.name = name;
3
4 this.say = function() {
5 log.add("User: " + this.name);
6 };
7}
8
9const DecoratedUser = function(user, street, city) {
10 this.user = user;
11 this.name = user.name; // ensures interface stays the same
12 this.street = street;
13 this.city = city;
14
15 this.say = function() {
16 log.add("Decorated User: " + this.name + ", " +
17 this.street + ", " + this.city);
18 };
19}
20
21// logging helper
22
23const log = (function() {
24 let log = "";
25
26 return {
27 add: function(msg) { log += msg + "\n"; },
28 show: function() { alert(log); log = ""; }
29 }
30})();
31
32const user = new User("Kelly");
33user.say();
34
35const decorated = new DecoratedUser(user, "Broadway", "New York");
36decorated.say();
37
38log.show();
COFFEE SHOP DECORATOR STORY
Now imagine a coffee shop. The coffee shop only sells coffee. But, the clever manager figured out that they could earn an extra π° by selling different coffee condiments separately. We can help them manage that. Let's see how we can use our Decorator Pattern in this case.
1//Constructor that will be decorated
2function Coffee(desc) {
3 //the type of the copy
4 this.type = desc;
5 //the description that will be modified
6 this.description = desc;
7 /*
8 A function expression is very similar to
9 and has almost the same syntax as a function
10 declaration. The main difference between a function
11 expression and a function declaration
12 is the function name, which can be omitted
13 in function expressions to create anonymous functions
14 A function expression can be used as an Immediately
15 Invoked Function Expression
16 */
17 this.cost = function () {
18 return 1.99;
19 };
20 this.desc = function () {
21 return this.description;
22 };
23 //A regular function
24 function type() {
25 return this.type;
26 }
27}
28
29//We are going to "decorate" our coffee with whip, Milk,
30//Soy or whatever you want, you just need to add another
31//condiment function
32//which is going to change the price and the description that
33//we see at the end
34//Decorator 1
35function Whip(houseBlend) {
36 let hbCost = houseBlend.cost();
37 let hbDesc = houseBlend.desc();
38 houseBlend.desc = function () {
39 return hbDesc + ', Whip';
40 };
41 houseBlend.cost = function () {
42 return hbCost + 0.09;
43 };
44}
45//Decorator 2
46function Milk(houseBlend) {
47 let hbCost = houseBlend.cost();
48 let hbDesc = houseBlend.desc();
49 houseBlend.desc = function () {
50 return hbDesc + ', Milk';
51 };
52 houseBlend.cost = function () {
53 return hbCost + 0.1;
54 };
55}
56//Decorator 3
57function Soy(houseBlend) {
58 let hbCost = houseBlend.cost();
59 let hbDesc = houseBlend.desc();
60 houseBlend.desc = function () {
61 return hbDesc + ', Soy';
62 };
63 houseBlend.cost = function () {
64 return hbCost + 0.12;
65 };
66}
67//We create a brand new coffee object instance
68//for example Espresso (type="Espresso", description="Espresso")
69let coffee = new Coffee('Espresso');
70//Double milk decorator
71Milk(coffee);
72Milk(coffee);
73//A whip
74Whip(coffee);
75//And a soy? π²
76//(This ain't coffee anymore, I don't know what this is...π)
77Soy(coffee);
78//fancy console log
79console.log(coffee.desc() + ` ${coffee.cost()}`);
80let coffee2 = new Coffee('House Blend');
81Milk(coffee2);
82//A whip
83Whip(coffee2);
84console.log(coffee2.desc() + `, $${coffee2.cost()}`);
85
86//Output
87// Espresso, Milk, Milk, Whip, Soy, $2.4
88// House Blend, Milk, Whip, $2.1799999999999997
In the previous coffee shop example, we saw that it is possible to apply multiple decorators, which can come in handy sometimes.
βWHY AND WHEN DO WE USE DECORATOR PATTERN?
Decorators use a special syntax in JavaScript, whereby they are prefixed with an @ symbol and placed immediately before the code being decorated. (see tc39)
It's possible to use as many decorators on the same piece of code as you desire, and they'll be applied in the order that you declare them.
Example:
1@log()
2@immutable()
3class Example {
4 @time('demo')
5 doSomething() {
6 //
7 }
8}
This is going to define a class and apply decorators - two to the class itself, and one to a property of a the class
- @\log - could log all access to the class
- @immutable - could make the class immutable - by calling Object.freeze()
- time - will recrod how long a method takes to execute and log this out with a unique tag
Decorators can allow for a cleaner syntax for applying this kind of wrapper around your code. Whilst function composition is already possible, it is significantly more difficult - or even impossible - to apply the same techniques to other pieces of code.
π΅ DIFFERENT TYPES OF DECORATOR PATTERN
Class member decorators
Property decorators are applied to a single member in a class β
whether they are properties, methods, getters, or setters. This
decorator function is called with three parameters:
- target - the class that the member is on
- name - the name of the member in the class
- descriptor - the member descriptor. This is essentially the object that would have been passed to Object.defineProperty.
Class decorators
Class decorators are applied to the entire class definition all in one go. The decorator function is called with a single parameter which is the constructor function being decorated. In general, these are less useful than class member decorators, because everything you can do here you can do with a simple function call in exactly the same way. Anything you do with these needs to end up returning a new constructor function to replace the class constructor.
π REACT EXAMPLE
React makes a very good example because of the concept of Higher-Order Components. These are simply React components that are written as a function, and that wrap around another component. These are ideal candidates for use as a decorator because there's very little you need to change to do so. For example. the react-redux library has a function, connect. That's used to connect a React component to a Redux store.
In general, this would be used as follows:
1class MyReactComponent extends React.Component {}
2export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
However, because of how the decorator syntax works, this can be replaced with the following code to achieve the exact same functionality:
1@connect(mapStateToProps, mapDispatchToProps)
2export default class MyReactComponent extends React.Component {}
Decorators, especially class member decorators provide a very good way of wrapping code inside a class in a very similar way to how you can already do so for freestanding functions.