This is the second article on design patterns, the other three are:
- (500 + praise! Don’t know how to encapsulate code? Take a look at these design patterns!
- (100 + praise! Framework source code to improve the extensibility of the design pattern
- Don’t know how to improve code quality? Take a look at these design patterns!
Design patterns to improve code quality follow.
You’ve all heard of the DRY principle, which is basically Don’t repeat yourself. This means Don’t write the same code over and over again. In other words, make your code reusable. So what kind of code is good for reuse?
- Objects can be reused. This is actually a bit like the design principle of our relational database, data table and relational table are separate, data table is pure data, there is no relationship with other tables, there is no business logic, relational table is the storage of specific corresponding relationship. When we need some data, we can just read the table without worrying about the other business in the table. There is a similar design for Redux. The store of Redux is pure data, which does not correspond to specific business logic. If the business needs to change the data, it needs to issue action. Because this kind of data is very simple, we can use it wherever we need it, and it is very reusable. So when we design data or objects, we also try to make them reusable.
- Less duplicate code. If your code is too repetitive, you’re not abstracting enough. A lot of times we duplicate code because we might need to write something similar to something that already exists, so we just copy it and change two lines of code. This functionality is achieved, but it creates a lot of duplicate code. Several design patterns described in this article are designed to solve this problem, improving code abstraction and reducing duplicate code.
- Single module function. This means that a module is focused on one function, and when we need to make a big function, we can combine multiple modules. It’s like Lego, a single block is like a piece of Lego. We can make a car out of 10 pieces, or we can make a truck out of 20 pieces. But if we make the module itself complicated and make cars, we can’t make a big truck out of two cars, and the reusability is reduced.
The main design modes to improve the reuse are bridge mode, share mode and template method mode. Let’s look at them respectively.
The bridge model
Bridging mode, as its name suggests, acts as a bridge, bridging variables of different dimensions together to achieve functionality. Suppose we need to implement three shapes (rectangle, circle, triangle) with three colors (red, green, blue) for each shape. The requirement has two schemes. One scheme writes nine methods, and each method implements a graph:
function redRectangle() {}
function greenRectangle() {}
function blueRectangle() {}
function redCircle() {}
function greenCircle() {}
function blueCircle() {}
function redTriangle() {}
function greenTriangle() {}
function blueTriangle() {}
Copy the code
The above code works, but if our requirements change and we ask for another color, then we have to add three more methods, one for each shape. So many methods seem repetitive, which means he has room for optimization. Let’s take a closer look at this requirement. The graph we will eventually draw has two variables, color and shape. These two variables have no strong logical relationship and are completely variables of two dimensions. So we can take these two variables apart, and then bridge them when we want to draw the graph, like this:
function rectangle(color) { / / rectangle
showColor(color);
}
function circle(color) { / / round
showColor(color);
}
function triangle(color) { / / triangle
showColor(color);
}
function showColor(color) { // Display color method
}
// Use a red circle
let obj = new circle('red');
Copy the code
Using bridge mode we changed our method from 3 * 3 to 3 + 1, and if subsequent colors are added, we just need to modify the showColor method slightly to support the new colors. If the dimension of our variable is not 2, but 3, this advantage will be more obvious. The former method requires X * y * Z, and after the bridge mode optimization, x + y + Z, which is directly exponential optimization. So the core idea of bridge pattern optimization here is to see if duplicate code can be broken down into multiple dimensions, and if so, separate out different dimensions and bridge those dimensions together when used.
Example: brush and crayon
Bridge mode actually my favorite example is brush and crayon, because this example is very intuitive and easy to understand. The requirement for this example is to draw three sizes of line, thin, medium and thick, each size of line needs 5 colors, if we use crayons to draw, we need 15 crayons, if we change the brush to draw, we only need 3 brushes, each time using different colors of ink, change the ink after using. It looks like this in code, a little bit like the one above:
// Start with three pen classes
function smallPen(color) {
this.color = color;
}
smallPen.prototype.draw = function() {
drawWithColor(this.color); // Use color to draw
}
function middlePen(color) {
this.color = color;
}
middlePen.prototype.draw = function() {
drawWithColor(this.color); // Use color to draw
}
function bigPen(color) {
this.color = color;
}
bigPen.prototype.draw = function() {
drawWithColor(this.color); // Use color to draw
}
// Add a color class
function color(color) {
this.color = color;
}
/ / when used
new middlePen(new color('red')).draw(); // Draw a medium red line
new bigPen(new color('green')).draw(); // Draw a big green line
Copy the code
In the example above, since the size and color of the crayons are their own attributes, they cannot be separated. The number of crayons required is the product of the two dimensions, which is 15, and the complexity increases exponentially if one more dimension is added. However, the size and color of the brush are separate. When using them, they can be bridged together. Only three brushes and five bottles of ink are needed, which greatly reduces the complexity. I created a new class for the color of the code above, whereas the color was passed as a parameter in the previous example, to demonstrate that even the same design pattern can be implemented differently. Which scheme to use depends on our actual needs. If you want to bridge a simple variable like color, you can pass it as a parameter. If you want to bridge a complex object, you may need a class. In addition, the three pen classes in the above code look very repetitive. In fact, further optimization can also extract a template, which is the base class of the pen, see the template method pattern below.
Example: menu item
The requirement for this example is that there are multiple menu items, each with different text, and the color of the text is different when the mouse slides in and out. In general, we might write code like this:
function menuItem(word) {
this.dom = document.createElement('div');
this.dom.innerHTML = word;
}
var menu1 = new menuItem('menu1');
var menu2 = new menuItem('menu2');
var menu3 = new menuItem('menu3');
// Set mouse slide in and out events for each menu
menu1.dom.onmouseover = function(){
menu1.dom.style.color = 'red';
}
menu2.dom.onmouseover = function(){
menu1.dom.style1.color = 'green';
}
menu3.dom.onmouseover = function(){
menu1.dom.style1.color = 'blue';
}
menu1.dom.onmouseout = function(){
menu1.dom.style1.color = 'green';
}
menu2.dom.onmouseout = function(){
menu1.dom.style1.color = 'blue';
}
menu3.dom.onmouseout = function(){
menu1.dom.style1.color = 'red';
}
Copy the code
The above code all looks quite repetitive. To eliminate this duplication, we separate the event binding and color Settings dimensions:
// The menu item class receives one more parameter color
function menuItem(word, color) {
this.dom = document.createElement('div');
this.dom.innerHTML = word;
this.color = color; // Take the received color parameters as instance properties
}
// The menu item class adds an instance method to bind events
menuItem.prototype.bind = function() {
var that = this; // This refers to the menuItem instance object
this.dom.onmouseover = function() {
this.style.color = that.color.colorOver; // Note that this is the this in the event callback, pointing to the DOM node
}
this.dom.onmouseout = function() {
this.style.color = that.color.colorOut; }}// Create a new class to store colors. This class is relatively simple, and can be extended as needed
function menuColor(colorOver, colorOut) {
this.colorOver = colorOver;
this.colorOut = colorOut;
}
// New menu items can now loop through an array
var menus = [
{word: 'menu1'.colorOver: 'red'.colorOut: 'green'},
{word: 'menu2'.colorOver: 'green'.colorOut: 'blue'},
{word: 'menu3'.colorOver: 'blue'.colorOut: 'red'},]for(var i = 0; i < menus.length; i++) {
// Pass in the parameters to instantiate, and finally call the bind method so that the event is automatically bound
new menuItem(menus[i].word, new menuColor(menus[i].colorOver, menus[i].colorOut)).bind();
}
Copy the code
The above code has the same idea. We extract the event binding and color dimensions separately and bridge them when using them, thus reducing a lot of similar code.
The flyweight pattern
When we observe a large number of similar blocks of code that may all be doing the same thing but applying different objects each time, we can consider using the share element pattern. Now suppose we have a requirement to display multiple popovers, each with different text and size:
// There is already a popover class
function Popup() {}
// The popover class has a display method
Popup.prototype.show = function() {}
Copy the code
If we don’t use the meta-mode, one bullet at a time looks like this:
var popup1 = new Popup();
popup1.show();
var popup2 = new Popup();
popup2.show();
Copy the code
We carefully observe the above code, found that the two instances do the same thing, both are to display popovers, but the size of each popover is different, so whether the show method can be proposed for public, different parts of the parameters in the pass. This idea is actually enjoy yuan model, we transform as follows:
var popupArr = [
{text: 'popup 1'.width: 200.height: 400},
{text: 'popup 2'.width: 300.height: 300},]var popup = new Popup();
for(var i = 0; i < popupArr.length; i++) {
popup.show(popupArr[i]); // Note that the show method accepts arguments
}
Copy the code
Example: File upload
Let’s look at another example. If we have a requirement to upload a file, we might need to upload multiple files. We might write code like this:
// An uploaded class
function Uploader(fileType, file) {
this.fileType = fileType;
this.file = file;
}
Uploader.prototype.init = function() {} // Initialize method
Uploader.prototype.upload = function() {} // The specific method of uploading
var file1, file2, file3; // Multiple files need to be uploaded
// Uploader is instantiated for each file
new Uploader('img', file1).upload();
new Uploader('txt', file2).upload();
new Uploader('mp3', file3).upload();
Copy the code
The above code we need to upload three files and instantiate the Uploader, but in fact this three instances only file types and the file data is different, the other is the same, we can reuse the same part, not the same part as a parameter to preach in line, with the flyweight pattern optimization are as follows:
// File data is thrown into an array
var data = [
{filetype: 'img'.file: file1},
{filetype: 'txt'.file: file2},
{filetype: 'mp3'.file: file3},
];
// The Uploader constructor no longer accepts arguments
function Uploader() {}
// The other methods on the prototype remain unchanged
Uploader.prototype.init = function() {}
// File type and file data are only used when uploading, as parameters of the upload
Uploader.prototype.upload = function(fileType, file) {}
// Only one instance is required for the upload loop
var uploader = new Uploader();
for(var i = 0; i < data.length; i++) {
uploader.upload(data[i].filetype, data[i].file)
}
Copy the code
The above code is simplified to 1 by parameter extraction, which improves the reusability of Uploader class. The above two examples are similar, but they are just a form of the schema. Anything that conforms to this idea can be called the schema. For example, the extend method in jQuery also uses the schema.
Example: extend method of jQuery
The extend method in jQuery is a common one that accepts one or more arguments:
- With only one parameter,
extend
The parameters passed in are merged into jQuery itself.- When you pass in two arguments obj1 and obj2,
extend
Obj2 will be merged onto obj1.
Based on the above requirements, we can easily implement it ourselves:
$.extend = function() {
if(arguments.length === 1) {
for(var item in arguments[0]) {
this[item] = arguments[0][item]
}
} else if(arguments.length === 2) {
for(var item in arguments[1]) {
arguments[0][item] = arguments[1][item]; }}}Copy the code
This [item] = arguments[0][item] = arguments[1][item] They differ in that the target and source of the copy are different, but the operation of the copy is the same. So we use the share mode optimization to extract different places and keep the common copy unchanged:
$.extend = function() {
// Extract two variables from different parts
var target = this; // The default is this, which is $itself
var source = arguments[0]; // Defaults to the first variable
// If there are two parameters, change target and source
if(arguments.length === 2) {
target = arguments[0];
source = arguments[1];
}
// The common copy operation remains the same
for(var item insource) { target[item] = source[item]; }}Copy the code
Template method pattern
The template method pattern is similar to inheritance, in that we define a common template skeleton and then extend it. Let’s look at the basic structure through a requirement. Suppose we now need to implement a navigation component, but there are many navigation types, some with messages, some horizontal, some vertical, and there may be new types later:
// Create a base class
function baseNav() {
}
baseNav.prototype.action = function(callback){} // Receive a callback for specific processing
Copy the code
In the above code, we built a basic class with only the most basic properties and methods, which is basically a template, and can receive callbacks in specific methods, so that the derived classes can pass in callbacks according to their own needs. The template method pattern is similar to the relationship between object oriented base and derived classes. Let’s look at another example.
Example: popover
Again, the popover example we used before, we’re going to make a popover component with different sizes of text, but this time we’re going to have cancel and OK buttons, and these two buttons might have different behaviors in different situations, like making a request. However, they also have a common operation, that is, after clicking these two buttons, pop-ups will disappear, so we can write out the common part first, as a template:
function basePopup(word, size) {
this.word = word;
this.size = size;
this.dom = null;
}
basePopup.prototype.init = function() {
// Initializes the DOM element
var div = document.createElement('div');
div.innerHTML = this.word;
div.style.width = this.size.width;
div.style.height = this.size.height;
this.dom = div;
}
// Cancel the method
basePopup.prototype.cancel = function() {
this.dom.style.display = 'none';
}
// The validation method
basePopup.prototype.confirm = function() {
this.dom.style.display = 'none';
}
Copy the code
Now that we have a basic template, if we need to do something else after clicking Cancel or confirm, such as making a request, we can use this template as a base and add the following actions:
// Start with basePopup
function ajaxPopup(word, size) {
basePopup.call(this, word, size);
}
ajaxPopup.prototype = new basePopup();
ajaxPopup.prototype.constructor = ajaxPopup;
// This is an inherited standard, in fact equivalent to the template
// Add the required action to initiate the network request
var cancel = ajaxPopup.prototype.cancel; // Cache the cancel method on the template first
ajaxPopup.prototype.cancel = function() {
// Call template cancel first
cancel.call(this);
// Add special processing, such as making requests
$.ajax();
}
// Confirm method is the same
var confirm = ajaxPopup.prototype.confirm;
ajaxPopup.prototype.confirm = function() {
confirm.call(this);
$.ajax();
}
Copy the code
The above example implements the template method pattern through inheritance, but this pattern does not necessarily use inheritance. Instead, it emphasizes extracting some basic parts as a template from which more operations can be extended.
Example: Algorithmic calculator
We don’t need to inherit this example, but the requirement is that we now have a set of algorithms, but these algorithms may be used to add different operations, and the operations that need to be added may be performed before the algorithm or after the algorithm.
// Define a basic class
function counter() {}// There is a method on the class
counter.prototype.count = function(num) {
// There is a basic calculation method of the algorithm itself
function baseCount(num) {
// It doesn't matter what the algorithm is, let's just add 1 here
num += 1;
returnnum; }}Copy the code
According to the requirements, we need to solve the problem that there may be other calculation operations during the calculation of the basic algorithm, which may be before or after the basic calculation, so we need to leave extensible interfaces on this calculation class:
function counter() {
// Add two queues for execution before or after the basic algorithm
this.beforeCounting = [];
this.afterCounting = [];
}
// Add an interface to receive the computation that should be done before the basic algorithm is computed
counter.prototype.before = function(fn) {
this.beforeCounting.push(fn); // Put the method directly into the array
}
// Add another interface to receive the calculation that should be done after the basic algorithm calculation
counter.prototype.after = function(fn) {
this.afterCounting.push(fn);
}
// Change the calculation method so that it follows the pre-calculation, basic calculation and post-calculation
counter.prototype.count = function(num) {
function baseCount(num) {
num += 1;
return num;
}
var result = num;
var arr = [baseCount]; // Put all the necessary calculations into this array
arr = this.beforeCounting.concat(arr); // Put the pre-evaluation operations in front of the array
arr = arr.concat(this.afterCounting); // Put the calculated operations after the array
// Execute the array in order
while(arr.length > 0) {
result = arr.shift()(result);
}
return result;
}
// Now counter can be used directly
var counterIntance = new counter();
counterIntance.before(num= > num + 10); // Add 10 before calculating
counterIntance.after(num= > num - 5); // Subtract 5 from the calculation
counterIntance.count(2); // 2 + 10 + 1-5 = 8
Copy the code
We didn’t use inheritance this time, but we still defined a basic skeleton of operations and then extended the skeleton to specific operations needed in different places.
conclusion
- If we have lots of similar blocks in our code, it usually means there is room for further optimization.
- If these repetitive blocks of code can be broken up into different dimensions, try bridge mode, which takes the dimensions apart and Bridges them to use.
- If some of these repetitive code operations are the same, but the objects of each operation are different, we can consider using the share mode to extract the public operations into methods and pass in the private parts as parameters.
- If these duplicated code have some basic operation is the same, but when the application needs to function more, we can consider these basic operations to extract into the template, and then set aside extension interface on the template, where it is needed can be extended by the interface functions, somewhat similar to the inheritance, but is not limited to the realization of inheritance.
- We’ve taken the duplicates out of the code, and we can use them elsewhere, and we’ve actually improved the reusability of the code.
- Again, there is no fixed paradigm for design patterns, but it’s all about understanding the idea that code can be implemented differently in different places.
At the end of this article, thank you for your precious time to read this article. If this article gives you a little help or inspiration, please do not spare your thumbs up and GitHub stars. Your support is the motivation of the author’s continuous creation.
The material for this article comes fromNetease Senior Front-end Development Engineer Micro majorTang Lei’s design pattern course.
Welcome to follow my public numberThe big front end of the attackThe first time to obtain high quality original ~
“Front-end Advanced Knowledge” series:Juejin. Cn/post / 684490…
“Front-end advanced knowledge” series article source code GitHub address:Github.com/dennis-jian…