Definition 1.
Strategy pattern: Define a set of algorithms, encapsulate them one by one, and make them interchangeable.
2. Use the strategy mode to calculate the bonus
The policy pattern has a wide range of applications. Take the annual bonus calculation as an example to introduce. For example, the person performing at S gets 4 times his salary, the person performing at A gets 3 times his salary, and the person performing at B gets 2 times his salary. Provide a code to calculate an employee’s year-end bonus.
2.1 Initial code implementation
Write a function called calculateBonus to calculate each person’s bonus. The function takes two arguments: the employee’s salary and his performance rating.
var calculateBonus = function(performanceLevel, salary){
if (performanceLevel === 'S') {
return salary * 4;
}
if (performanceLevel === 'A') {
return salary * 3;
}
if (performanceLevel === 'B') {
return salary * 2;
}
}
calculateBonus('B'.20000) // Output: 40000
calculateBonus('S'.6000) // Output: 24000
Copy the code
This code is quite simple, but has obvious drawbacks.
-
The calculateBonus function is quite large and contains many if-else statements that need to override all logical branches.
-
CalculateBonus function is inelastic, if we add a new performance level C, or want to change the bonus coefficient of performance S to 5, then we have to go deep into the internal implementation of calculateBonus function, which is against the open-closed principle.
-
Algorithms are not reusable. What if you need to reuse these algorithms for calculating bonuses elsewhere in the program? Our options are copy and paste.
2.2 Refactoring code using combinatorial functions
Encapsulate the algorithm in small functions that are well named so that you can see which algorithm it corresponds to at a glance, and they can be reused elsewhere in the program.
var performanceS = function(salary){
return salary * 4;
}
var performanceA = function(salary){
return salary * 3;
}
var performanceB = function(salary){
return salary * 2;
}
var calculateBonus = function(performanceLevel, salary) {
if (performanceLevel === 'S') {
return performanceS(salary)
}
if (performanceLevel === 'A') {
return performanceA(salary)
}
if (performanceLevel === 'B') {
return performanceB(salary)
}
}
calculateBonus('A'.10000) // Output: 30000
Copy the code
The program has improved somewhat, but very limited, and remains unsolved: calculateBonus functions are likely to grow larger and less elastic as the system changes.
2.3 Refactoring code using policy patterns
The strategy pattern refers to defining a set of algorithms and encapsulating them one by one. Separating the immutable from the changing is the theme of every design pattern, and policy patterns are no exception. The purpose of policy patterns is to separate the use of algorithms from the implementation of algorithms.
In this example, the algorithm is used in the same way, according to a certain algorithm to obtain the calculated amount of the bonus. The implementation of the algorithm is different and varied, and each performance corresponds to different calculation rules.
A policy-based program consists of at least two parts. The first part is a set of policy classes, which encapsulate the specific algorithm and are responsible for the specific calculation process. The second part is the environment class Content, which accepts the client’s request and then delegates it to a policy class. To do this, maintain a reference to a policy object in the Content.
Now use the policy pattern to refactor the above code. The first version mimics the implementation in traditional object-oriented languages. First, each performance calculation rule is encapsulated in the corresponding strategy class:
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;
};
// Next define the Bonus class:
var Bonus = function(){
this.salary = null; // Original salary
this.strategy = null; // The policy object corresponding to the performance level
}
Bonus.prototype.setSalary = function(salary) {
this.salary = salary; // Set the original salary of the employee
}
Bonus.prototype.setStrategy = function(strategy) {
this.strategy = strategy; // Set the policy object corresponding to the employee performance level
}
Bonus.prototype.getBonus = function(){ // Get the amount of bonus
if(!this.strategy) {
throw new Error('Strategy attribute not set')}return this.strategy.calculate(this.salary) // Delegate the calculation of bonuses to the corresponding policy object
}
Copy the code
Before finishing the final code, review the idea of the strategy pattern: define a series of algorithms, encapsulate them one by one, and make them interchangeable.
Define a list of algorithms and encapsulate each of them into a policy class. The algorithm is encapsulated in methods within the policy class. When a client makes a request to the Context, the Context always delegates the request to one of these policy objects for calculation.
Now let’s finish the rest of the code in this example. Create a Bonus object and set the Bonus object with some raw data, such as the original salary of the employee. Next, pass some policy object that calculates the bonus into the Bonus object and save it inside. When bonus.getBonus() is called to calculate the bonus, the Bonus object does not have the ability to do the calculation itself, but delegates the request to the previously saved policy object:
var bonus = new Bonus();
bonus.setSalary(10000);
bonus.setStrategy(new performanceS()); // Set the policy object
console.log(bonus.getBonus()); The output40000
bonus.setStrategy(new performanceA()); // Set the policy object
console.log(bonus.getBonus()); The output30000
Copy the code
After refactoring through the policy pattern, the code becomes clearer and the responsibilities of each class are more distinct. But this code is based on a parody of a traditional object-oriented language.
3.JavaScript version of the policy mode
In the previous example, the Strategy object was created from the various policy classes, mimicking the implementation of some traditional object-oriented languages. In fact, functions are also objects in the JavaScript language, so it is simpler and more straightforward to define strategy directly as a function:
var strategies = {
"S": function(salary){
return salary * 4;
},
"A": function(salary){
return salary * 3;
},
"B": function(salary){
return salary * 2; }}Copy the code
Similarly, the Context does not necessarily have to be represented by the Bonus class; we still use the calculateBonus function as the Context to accept user requests.
var calculateBonus = function(level, salary) {
return strategies[level](salary);
}
console.log(calculateBonus('S'.20000)); // Output: 80000
console.log(calculateBonus('A'.10000)); // Output: 30000
Copy the code
4. The embodiment of polymorphism in the strategy mode
By refactoring the code using the policy pattern, we eliminated a large number of conditional branch statements from the original program. All the logic associated with calculating bonuses is no longer placed in the Context, but is distributed among the policy objects. The Context does not have the ability to calculate bonuses, but delegates this responsibility to some policy object. The algorithm responsible for each policy object has been encapsulated within the object. When we ask these policy objects to “calculate the bonus”, they will return different results, which is the object polymorphism and the purpose of “they can be interchangeable”. By replacing the currently saved policy object in the Context, we can perform different algorithms to get the desired result.
5. Use strategy mode for slow animation
5.1 Principle of realizing animation effect
The gap between some of the original painting with a faster number of frames to play, to achieve visual animation effect. In JavaScript, you can animate an element by continuously changing one of its CSS attributes, such as left, top, and background-position.
5.2 Train of thought and some preparations
Goal: Write an animation class and some easing algorithms that let the ball move around the page with various easing effects.
Before you start your exercise, you need some information:
- The original position of the ball when the animation starts;
- The target position of the ball
- The exact point in time when the animation began
- The duration of the ball movement
Use setInterval to create a timer that runs every 19ms. In each frame of the timer, the animation has consumed time, the original position of the ball, the target position of the ball and the total time of the animation continued information into the slow algorithm. Using these parameters, the algorithm calculates where the ball should be at the moment. Finally, update the CSS properties of the div, and the ball will move smoothly.
5.3 Get the ball moving
Slow algorithm: accept 4 parameters, meaning respectively: animation has consumed time, ball original position, ball target position, the total duration of animation, return value is the animation element should be in the current position.
var tween = {
linear: function(t,b,c,d) {
return c*t/d + b;
},
easeIn: function(t,b,c,d) {
return c * (t/= d) * t + b;
},
strongEaseIn: function(t,b,c,d) {
return c * (t/= d) * t * t * t * t+ b;
},
strongEaseOut: function(t,b,c,d) {
return c * ((t = t/d - 1) * t * t * t * t + 1)+ b;
},
sineaseIn: function(t,b,c,d) {
return c * (t/= d) * t * t + b;
},
sineaseOut: function(t,b,c,d) {
return c * ((t= t/ d - 1) * t * t + 1) + b; }},Copy the code
Complete code
Start by placing a div in the page:
<body>
<div style="position:absolute; background:blue" id="div">I am a div</div>
</body>
Copy the code
Next you define the Animate class, whose constructor takes a single parameter: the DOM node to Animate.
var Animate = function(dom) {
this.dom = dom; // Move the DOM node
this.startTime = 0; // Exercise start time
this.startPos = 0; // The position of the DOM node when the animation starts, which is the initial position of the DOM
this.endPos = 0; // At the end of the animation, the position of the DOM node, the target position of the DOM
this.propertyName = null; // The name of the CSS property of the DOM node to be changed
this.easing = null; // Slow algorithm
this.duration = null; // Animation duration
}
Copy the code
The Animate.prototype.start method is then responsible for starting the animation, and as soon as the animation is started, some information is recorded that the slow algorithm can use later when calculating the current position of the ball. After recording this information, this method is also responsible for starting the timer.
Animate.prototype.start = function(propertyName, endPos, duration, easing) {
this.startTime = +new Date; // Animation start time
this.startPos = this.dom.getBoundingClientRect()[propertyName]; // Dom node initial position
this.propertyName = propertyName; // The name of the CSS property of the DOM node to be changed
this.endPos = endPos; // dom node target location
this.duration = duration; // Animation duration
this.easing = tween[easing]; // Slow algorithm
var self = this;
var timeId = setInterval(function(){ // Start the timer to execute the animation
if(self.step()===false) {// If the animation is over, clear the timer
clearInterval(timeId); }},19);
}
Copy the code
The Animate.prototype.start method takes the following four parameters:
- PropertyName: indicates the name of the CSS property to be changed, for example, ‘left’ and ‘top’, which indicate left and right movement and up and down movement respectively.
- EndPos: The target position of the ball
- Duration: indicates the animation duration
- Easing: The easing algorithm
This is followed by the Animate.prototype.step method, which represents what the ball will do for each frame of motion. In this case, the method is responsible for calculating the current position of the ball and calling the Animate.prototype.update method that updates the CSS property values.
Animate.prototype.step = function(){
var t = +new Date; // Get the current time
if (t >= this.startTime + this.duration) {
this.update(this.endPos); // Update the CSS property value of the ball
return false;
}
var pos = this.easing(t - this.startTime, this.startPos, this.endPos - this.startPos, this.duration);
// pos is the current position of the ball
this.update(pos); // Update the CSS property name of the ball
}
Copy the code
If the current time is greater than the sum of the animation start time plus the animation duration, the animation has ended and the position of the ball should be corrected. Because after the start of this frame, the ball’s position is close to the target position, but maybe not exactly the target position. At this point we need to actively modify the current position of the ball to the final target position. In addition, you can tell the Animate. Prototype. start method to clear the timer by making the Animate
Finally, the Animate.prototype.update method is responsible for updating the ball CSS property values:
Animate.prototype.update = function(pos) {
this.dom.style[this.propertyName] = pos + 'px';
}
/ / test
var div = document.getElementById('div');
var animate = new Animate(div);
animate.start('left'.500.1000.'strongEaseOut');
Copy the code
6. A more generalized “algorithm”
The strategy pattern refers to defining a set of algorithms and encapsulating them. The previous examples of calculating bonuses and slow animations encapsulate some algorithms.
By definition, policy patterns encapsulate algorithms. However, if the policy pattern is only used to encapsulate the algorithm, it is overqualified. In practice, the meaning of the algorithm is often diffused so that the policy pattern can also be used to encapsulate a list of “business rules.” As long as these business rules point to consistent goals and can be used interchangeably, they can be encapsulated in policy patterns.
7. Form verification
Use policy mode to complete form validation
Validation logic
- The user name cannot be empty
- The password must contain at least six characters
- The mobile phone number must conform to the format
7.1 The first version of form validation
No policy patterns are introduced
<html>
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">Please enter user name:<input type="text" name="userName" />Please enter your password:<input type="text" name="password" />Please enter your mobile phone number:<input type="text" name="phoneNumber" />
<button>submit</button>
</form>
<script>
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function(){
if (registerForm.username.value === ' ') {
alert('User name cannot be empty');
return false;
}
if (registerForm.password.value.length < 6) {
alert('Password length must not be less than 6 characters');
return false;
}
if (!/ (a ^ 1 [3 | | 5 8] [0-9] {9} $) /.test(registerForm.phoneNumber.value)) {
alert('Incorrect format of mobile number');
return false; }}</script>
</body>
</html>
Copy the code
This is a very common encoding format, and it has the same shortcomings as the original version of calculating bonuses.
- The registerForm.onsubmit function is quite large and contains many if-else statements that need to override all validation rules
- The registerForm.onsubmit function is inelastic. If you add a new validation rule, or if you want to change the password length validation from 6 to 8, you have to go into the internal implementation of registerForm.onsubmit, which violates the open-closed rule
- The algorithm is not reusable, and if we add another form to our program that requires some similar validation, we might copy the validation logic all over the place.
7.2 Reconstructing form validation in policy mode
Step 1: Encapsulate the validation logic as a policy object:
var strategies = {
isNonEmpty: function(val, errorMsg) { / / not be empty
if (value === ' ') {
returnerrorMsg; }},minLength: function(value, length, errorMsg) { // Limit the minimum length
if(value.length < length) {returnerrorMsg; }},isMobile: function(value, errorMsg) {
if (!/ (a ^ 1 [3 | | 5 8] [0-9] {9} $) /.test(value)) { // Mobile phone number format
return errorMsg
}
}
}
Copy the code
Next, implement the Validator class. The Validator class acts as the Context here and is responsible for receiving the user’s request and delegating it to the Strategy object. Before we show the code for the Validator class, let’s look at how users send requests to the Validator class:
var validataFunc = function(){
var validator = new Validator(); // Create a Validator
// Add some validation rules
validator.add(registerForm.userName, 'isNonEmpty'.'User name cannot be empty');
validator.add(registerForm.password, 'minLength:6'.'Password length must not be less than 6 characters');
validator.add(registerForm.phoneNumber, 'isMobile'.'Incorrect format of mobile number');
var errorMsg = validator.start(); // Get the verification result
return errorMsg; // Return the verification result
}
var registerForm = document.getElementById('registerForm');
registerForm.onsubmit = function(){
var errorMsg = validataFunc(); // If errorMsg has an exact return value, it is not verified
if (errorMsg) {
alert(errorMsg)
return false; // Block form submission}}Copy the code
As you can see from this code, you create a Validator object and then add some validation rules to the Validator object using the validator.add method. The validator.add method takes three arguments, as illustrated by this code:
validator.add(registerForm.password, 'minLength:6'.'Password length must not be less than 6 characters');
Copy the code
- Registerform. password is the input field for verification.
- ‘minLength:6’ is a colon-separated string. The minLength before the colon represents the strategy object selected by the customer, and the number 6 after the colon represents the parameters required during validation. ‘minLength:6′ means that the value of the registerform. password text input field has a minimum length of 6. If the string does not contain a colon, no additional parameter information, such as’ isNonEmpty ‘, is required during validation.
- The third parameter is the error message returned if the verification fails.
After we have added a set of validation rules to the Validator object, the validator.start() method is called to start the validation. If validator.start() returns an exact errorMsg string as the return value, the validation failed, and registerForm.onSubmit returns false to prevent the form submission.
Finally, an implementation of the Validator class:
var Validator = function(){
this.cache = []; // Save the verification rule
}
Validator.prototype.add = function(dom, rule, errorMsg) {
var ary = rule.split(':'); // Separate strategy from parameters
this.cache.push(function(){ // Wrap the verification step in an empty function and place it in the cache
var strategy = ary.shift(); // User-selected strategy
ary.unshift(dom.value); // Add input value to the parameter list
ary.push(errorMsg); // Add errorMsg to the parameter list
return strategies[strategy].apply(dom, ary);
});
};
Validator.prototype.start = function(){
for (var i=0, validatorFunc; validatorFunc = this.cache[i++];) {
var msg = validatorFunc(); // Start the verification and get the return information after the verification
if (msg) { // If an exact value is returned, the verification fails
returnmsg; }}}Copy the code
After refactoring code using policy patterns, you can validate a form simply by “configuring” it, and these validation rules can be reused anywhere in the program or easily ported to other projects as plug-ins.
Only a small amount of code needs to be written or rewritten to modify the validation rules. For example, if you want to change the verification rule of the user name input box to a user name that cannot contain less than 10 characters, the code is as follows
validator.add(registerForm.userName, 'isNonEmpty'.'User name cannot be empty');
/ / to
validator.add(registerForm.userName, 'minLength:10'.'Username length cannot be less than 10 characters');
Copy the code
7.3 Adding Multiple Verification Rules to a File Input box
<html>
<body>
<form action="http://xxx.com/register" id="registerForm" method="post">Please enter user name:<input type="text" name="userName" />Please enter your password:<input type="text" name="password" />Please enter your mobile phone number:<input type="text" name="phoneNumber" />
<button>submit</button>
</form>
<script>
// Policy object
var strategies = {
isNonEmpty: function(val, errorMsg) { / / not be empty
if (value === ' ') {
returnerrorMsg; }},minLength: function(value, length, errorMsg) { // Limit the minimum length
if(value.length < length) {returnerrorMsg; }},isMobile: function(value, errorMsg) {
if (!/ (a ^ 1 [3 | | 5 8] [0-9] {9} $) /.test(value)) { // Mobile phone number format
return errorMsg
}
}
}
/ / the Validator class
var Validator = function(){
this.cache = []; // Save the verification rule
}
Validator.prototype.add = function(dom, rules) {
var self = this;
for (var i=0, rule; rule = rules[i++];) {(function(rule){
var strategyAry = rule.strategy.split(':');
var errorMsg = rule.errorMsg;
self.cache.push(function(){
var strategy = strategyAry.shift();
strategyAry.unshift(dom.value);
strategyAry.push(errorMsg);
return strategies[strategy].apply(dom, strategyAry);
})
})(rule)
}
};
Validator.prototype.start = function(){
for (var i=0, validatorFunc; validatorFunc = this.cache[i++];) {
var errorMsg = validatorFunc();
if (errorMsg) {
returnerrorMsg; }}}// The client calls the code
var registerForm = document.getElementById('registerForm');
var validataFunc = function(){
var validator = new Validator();
validator.add(registerForm.userName, [{
strategy: 'isNonEmpty'.errorMsg: 'User name cannot be empty'}, {strategy: 'minLength:10'.errorMsg: 'Username length cannot be less than 10 characters'
}])
validator.add(registerForm.password, [{
strategy: 'minLength:6'.errorMsg: 'Password length must not be less than 6 characters'
}])
validator.add(registerForm.phoneNumber, [{
strategy: 'isMobile'.errorMsg: 'Incorrect format of mobile number'
}])
var errorMsg = validator.start();
return errorMsg;
}
registerForm.onsubmit = function(){
var errorMsg = validataFunc();
if (errorMsg) {
alert(errorMsg)
return false; }}</script>
</body>
</html>
Copy the code
5.8 Advantages and disadvantages of the policy Mode
Advantages:
- Policy pattern can avoid multiple conditional selection statements by using composition, delegation and polymorphism
- The policy pattern provides perfect support for the open-closed principle by encapsulating algorithms in a separate strategy, making them easy to switch, easy to understand, and easy to extend
- Algorithms in the policy pattern can also be reused elsewhere in the system, avoiding a lot of repetitive copy-and-paste work
- Using composition and delegation in the policy pattern to give the Context the ability to execute the algorithm is also a lighter alternative to inheritance
Disadvantages:
-
Using the policy pattern adds many policy classes or policy objects to the program, but it is actually better than stacking the logic they are responsible for in the Context
-
To use the strategy pattern, it is necessary to understand all strategies and the differences between them in order to choose a suitable strategy. For example, to choose a suitable travel route, we must first understand the details of choosing planes, trains, bicycles and other options. A strategy that exposes all of its implementations to the customer violates the least knowledge rule.
5.9 First-class function Objects and Policy patterns
The previous examples of policy patterns range from versions that mimic traditional object-oriented languages to implementations specific to JavaScript languages. In class-centric traditional object-oriented languages, different algorithms or behaviors are encapsulated in policy classes. Context delegates requests to these policy objects, and these policy objects return different execution results based on the request, thus showing the polymorphism of the object
In languages where functions are first-class objects, the policy pattern is invisible. Strategy is a variable whose value is a function. In JavaScript, in addition to using classes to encapsulate algorithms and behavior, using functions is of course an option. These “algorithms” can be encapsulated in functions and passed around, what I like to call “higher-order functions.” In fact, in languages like JavaScript, where functions are first-class objects, the policy pattern is built into the language itself, and we often use higher-order functions to encapsulate different behaviors and pass them to another function. When we send the “call” message to these functions, different functions return different execution results. In JavaScript, “function object polymorphism” is much simpler.
In JavaScript, policy classes are often replaced by functions, and policy patterns become “invisible”.