JavaScript Design pattern learning and practice (2)

We continue to summarize and learn JavaScript Design Patterns, this time we summarize two similar patterns in JavaScript point of view, that is, the proxy pattern, the strategy pattern.

The difference between these two patterns is obvious in non-functional programming languages such as Java, but they are relatively similar in JavaScript.

Strategy mode

Strategy mode definition

In program design, we often encounter this situation, to achieve a function there are a variety of options to choose from. For example, a compressed file program, you can choose the zip algorithm, you can also choose the gzip algorithm

These algorithms are flexible and can replace each other at will. This solution is the policy pattern.

The definition of the strategy pattern is: Define a series of algorithms, encapsulate them one by one, and make them interchangeable.

Calculate bonuses using strategy mode

For example, we now have a demand. The bonus at the end of the year is determined based on performance. Performance S is 4 months’ salary, 3 months for A, and 2 months for B

Base code

1
2
3
4
5
6
7
8
9
10
11
12
13
var calculateBonus = function(performanceLevel, salary) {
if (performanceLevel = 'S') {
return salary * 4;
}

if (performanceLevel = 'A') {
return salary * 3
}

if (performanceLevel = 'B') {
return salary * 2
}
}

This code is very simple, but there are also many shortcomings.

  • The calculateBonus function is quite large and contains many if-else statements, which need to cover all branches
  • The calculateBonus function is inflexible, if we add a new performance grade C, or want to change the performance coefficient to 5, we must go deep into the internal implementation of the calculateBonus function, which violates the Open Closed Principle
  • The algorithm is poor to reuse, if the algorithm to reuse part of the bonus elsewhere in the program, only copy and paste

Refactor code using strategy patterns

Separating the immutable from the changing is the theme of every design pattern, and the policy pattern is no exception. The purpose of the policy pattern is to separate the use of the algorithm from the implementation of the algorithm.

In our example, the way the algorithm is used is unchanged, and it is based on the calculated bonus amount obtained by a certain algorithm. The implementation of the algorithm varies, and different performance corresponds to different calculation rules

A program based on a policy pattern consists of at least two parts. The first part is a set of policy classes, which encapsulate specific algorithms and are responsible for the specific calculation process. The second part is the environment class Context, which accepts the client’s request and then delegates the request to a specific policy class. To do this, it is necessary to maintain a reference to a policy object in the Context.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var performanceS = function() {}

performanceS.prototype.calculate = function(salary) {
return salary * 4;
}

var performanceA = function() {}

performanceA.prototype.calculate = function(salary) {
return salary * 3;
}

var performanceB = function() {}

performanceB.prototype.calculate = function(salary) {
return salary * 2;
}

var Bonus = function() {
this.salary = null;
this.strategy = null;
}

Bonus.prototype.setSalary = function(salary) {
this.salary = salary;
}

Bonus.prototype.setStrategy = function(strategy) {
this.salary = strategy;
}

Bonus.prototype.getBonus = function() {
return this.strategy.calculate(this.salary);
}

When a client makes a request to a Context, the Context always delegates the request to one of these policy objects.

Strategy pattern for JavaScript version

We said at the beginning that functional programming in JavaScript makes some Design patterns different

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var strategies = {
'S': function(salary) {
return salary * 4;
},
'A': function(salary) {
return salary * 3;
},
'B': function(salary) {
return salary * 2
}
}

var calculateBonus = function (level, salary) {
return strategies[level](salary);
}

Proxy mode

Proxy mode definition

The proxy pattern is to provide a substitute or placeholder for an object in order to control access to it.

The key to the proxy pattern is that when the client is inconvenient to directly access an object or does not meet the needs, an object is provided to control access to the object, and the client actually accesses the substitute object. After the substitute object does some processing on the request, it forwards the request to the object itself.

When the strategy pattern has only one strategy, the proxy pattern and the strategy pattern look similar. The main difference is that the proxy pattern is to proxy access to the target object, while the strategy pattern is to execute different strategies.

Use proxy mode to send flowers

Suppose Xiaoming wants to send flowers to A, we use code to simulate this process.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Flower = function() {}

var xiaoming = {
sendFlower: function(target) {
var flower = new Flower();
target.receiveFlower(flower);
}
}

var A = {
reveiveFlower: function(flower) {
Consoloe.log ('Received ${flower}')
}
}

xiaoming.sendFlower(A);

If we introduce B to help us send flowers, it is the proxy mode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var Flower = function() {}

var xiaoming = {
sendFlower: function(target) {
var flower = new Flower();
target.receiveFlower(flower);
}
}

var B = {
reveiveFlower: function(flower) {
A.receiveFlower(flower)
}
}

var A = {
reveiveFlower: function(flower) {
Consoloe.log ('Received ${flower}')
}
}

xiaoming.sendFlower(B);

However, this piece of code seems to have no practical use other than going around in a circle.

Of course, that’s true, but if we add a requirement that we need A to send flowers when he’s in a good mood, and only B knows when A is in a good mood, this proxy model is useful

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var Flower = function() {}

var xiaoming = {
sendFlower: function(target) {
var flower = new Flower();
target.receiveFlower(flower);
}
}

var B = {
reveiveFlower: function(flower) {
A.listenGoodMoon(function() {
A.receiveFlower(flower)
});
}
}

var A = {
reveiveFlower: function(flower) {
Consoloe.log ('Received ${flower}')
},
listenGoodMood: function(callback) {
callback();
}
}

xiaoming.sendFlower(B);

Protection agents and virtual agents

Although the above example is simple, we can see the shadow of two kinds of proxies. Proxy B can help proxy A filter some requests, which is called protection proxy.

And if new Flower is a relatively expensive operation, we can give the operation of new Flower to B to do it, thereby saving the cost. This is called a virtual agent.

1
2
3
4
5
6
7
8
var B = {
reveiveFlower: function() {
A.listenGoodMoon(function() {
var flower = new Flower();
A.receiveFlower(flower);
});
}
}

Virtual agent implements image preloading

In web development, if you set the src attribute to an img tag node, if the picture is too large or the network is poor, the position of the picture is often blank for a period of time.

Common practice is to use a loading image placeholder, and then asynchronously to load the image, the image is loaded and then filled into the img.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var myImage = {
setSrc: function(src) {
var imageNode = document.createElement('img');
document.body.appendChild(imageNode);
imageNode.src = src;
}
}

var proxyImage = (function() {
var img = new Image();
img.onload = function() {
myImage.setSrc(this.src)
}

return {
setSrc: function(src) {
myImage.setSrc('file://loading.png');
img.src = src;
}
}
})()

proxyImage.setSrc('http://imgcache.com/music/aaa.jpg');

Significance of the Agent Model

We may wonder, but it is to implement a preloaded function, even if you do not need to introduce any mode, you can do it, so what are the benefits of introducing proxy mode? Let’s take a look at not using proxy mode to do image preloading

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var MyImage = (function() {
var imageNode = document.createElement('img');
document.body.appendChild(imageNode);

var img = new Image();
img.onload = function() {
imageNode.setSrc(this.src)
}

return {
setSrc: function(src) {
imageNode.src = 'file://loading.png';
img.src = src;
}
}
})()

In order to illustrate the meaning of the agency pattern, we introduce an Object Oriented design principle - the Single Responsibility Principle.

The Single Responsibility Principle states that for a class (which usually also includes objects and functions), there should be only one reason for it to change. If an object has multiple responsibilities, it means that the object will become huge, and there can be multiple reasons for it to change.

Object Oriented design encourages the distribution of behavior among fine grained objects. If an object takes on too many responsibilities, it is equivalent to coupling these responsibilities together. This coupling can lead to fragile and low-cohesion designs. When changes occur, the design may be accidentally broken.

Responsibility is defined as “the reason for the change”. The MyImage object in the previous code is responsible for preloading images in addition to setting src for the img node. When we deal with one of the responsibilities, it may affect the implementation of the other responsibility because of its strong coupling.

Consistency of proxy and ontology interfaces

If one day we no longer need preloading, then we no longer need proxy objects and can choose to directly request the ontology. The key is that both the proxy object and the ontology provide the setSrc method to the outside world. In the customer’s view, the proxy object and the ontology are consistent, and the process of receiving requests by the proxy is transparent to the user.

In languages such as Java, both proxies and ontologies need to explicitly implement the same interface. On the one hand, the interface ensures that they have the same method. On the other hand, interface-oriented programming caters to the Dependence Inversion Principle principle and transforms upward through the interface.

It is worth mentioning that if both the proxy body and the object are a function, the function must be executed, and they can be considered to have a consistent interface.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var myImage = function(src) {
var imageNode = document.createElement('img');
document.body.appendChild(imageNode);
imageNode.src = src;
}

var proxyImage = (function() {
var img = new Image();
img.onload = function() {
myImage.setSrc(this.src)
}

return function(src) {
myImage.setSrc('file://loading.png');
img.src = src;
}
})()

proxyImage('http://imgcache.com/music/aaa.jpg');

Other proxy modes

In fact, we usually use a lot of proxy mode development, such as our commonly used anti-shake and throttling, in fact, is a proxy, will be a number of requests into a processing.

Another example is our cache and buffer proxy, our direct access to the database into access to the buffer, if the buffer is not, then the buffer to the database query.

Another example is the message queue we usually use, which can also be an idea of the proxy pattern.