Design pattern 2: don't know how to improve code reusability? Look at these design patterns!

This is the second article of design pattern. The first article is Don't know how to package code? Look at these design patterns! Later, there will be a design pattern to improve scalability and code quality. Pay attention to it and don't get lost, ha ha~

I think everyone has heard of the DRY principle. In fact, it is don't repeat yourself, which means don't write the same code repeatedly. In other words, it means to improve the reusability of the code. What kind of code is good reusability?

  1. Objects can be reused. In fact, this is a bit like the design principle of our relational database. The data table and relational table are separated. The data table is pure data and has no relationship with other tables or business logic. The relational table is the storage of specific correspondence. When we need some data, we can read this table directly without worrying about other businesses in this table. Similar designs include redux. The store of Redux is pure data, which does not correspond to the specific business logic. If the business needs to change the data, it needs to send an action. It is because this kind of data is very simple, so we can use it everywhere we need, with high reusability. So when we design data or objects, we should also try to make them reusable.
  2. Less duplicate code. If you write code with a high degree of repetition, it means that your code is not abstract enough. A lot of times we generate duplicate code because we may need to write a function similar to the existing function, so we copy the previous code and change two lines of code. In this way, although the function is realized, a lot of repetitive code is produced. Several design patterns mentioned in this paper are used to solve this problem, improve the abstraction of code and reduce repetitive code.
  3. Module function is single. This means that a module focuses on one function. When we need to do a big function, we can combine multiple modules. It's like Lego building blocks. A single module is like a small piece of Lego building blocks. We can use 10 pieces to make a car, or 20 pieces to make a big truck. But if we make the module complex and make a car, we can't use two cars to make a big truck, which reduces the reusability.

The design modes to improve reusability mainly include bridge mode, sharing mode and template method mode. Let's take a look at them respectively.

Bridge mode

The bridge mode, as its name implies, is actually a bridge that bridges variables of different dimensions together to realize functions. Suppose we need to realize three shapes (rectangle, circle, triangle), each of which has three colors (red, green, blue). There are two schemes for this requirement, 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 code

Although the function of the above code is realized, if our requirements change, we need to add another color, then we need to add three more methods, one for each shape. So many methods are repetitive, which means there is room for optimization. Let's take a closer look at this requirement. The final figure we want to draw has two variables: color and shape. In fact, these two variables have no strong logical relationship. They are all variables of two dimensions. Then we can separate these two variables, and bridge them when we want to draw a graph finally, which is like this:

function rectangle(color) {     // rectangle
  showColor(color);
}

function circle(color) {     // circular
  showColor(color);
}

function triangle(color) {   // triangle
  showColor(color);
}

function showColor(color) {   // How to display colors
  
}

// When using, a red circle is needed
let obj = new circle('red');
Copy code

After using the bridge mode, our method has changed from 3 * 3 to 3 + 1, and if the subsequent color increases, we only need to modify the showColor method slightly to support the new color. If the dimension of our variable is not 2, but 3, this advantage will be more obvious. The former method is x * y * z, and the bridge mode is x + y + z after optimization, which is directly exponential optimization. So the core idea of bridge pattern optimization here is to observe whether repeated code can be split into multiple dimensions, if possible, separate different dimensions, and bridge these dimensions when using.

Example: brush and crayon

In fact, my favorite example of bridge mode is brush and crayon, because this example is very intuitive and easy to understand. The requirement of this example is to draw fine, medium and thick three types of lines. Each type of line needs 5 colors. If we use crayons to draw, we need 15 crayons. If we change crayons to draw, we only need 3 crayons. Each time we use ink of different colors, we need to change ink. This is how it is written in code, a little like the one above:

// Let's start with the class of three pens
function smallPen(color) {
  this.color = color;
}
smallPen.prototype.draw = function() {
  drawWithColor(this.color);    // Draw with color
}

function middlePen(color) {
  this.color = color;
}
middlePen.prototype.draw = function() {
  drawWithColor(this.color);    // Draw with color
}

function bigPen(color) {
  this.color = color;
}
bigPen.prototype.draw = function() {
  drawWithColor(this.color);    // Draw with color
}

// Another color class
function color(color) {
  this.color = color;
}

// When in use
new middlePen(new color('red')).draw();    // Draw a medium red line
new bigPen(new color('green')).draw();     // Draw a large green line
Copy code

In the above example, because the size and color of crayons are their own attributes, they cannot be separated. The number of crayons needed is the product of two dimensions, that is, 15. If there is another dimension, the complexity will increase exponentially. But the two dimensions of brush size and color are separated. When you use them, you can bridge them together. Only three brushes and five bottles of ink are needed. The complexity is greatly reduced. I created a new class for the color of the above code, and the color of the drawing in the previous example is directly passed as a parameter, so as to demonstrate that even the same design pattern can have different implementation schemes. The specific scheme to be adopted depends on our actual needs. If only a simple variable like color is to be bridged, it can be passed as a parameter. If a complex object is to be bridged, a class may be needed. In addition, the three pen classes of the above code look very repetitive. In fact, further optimization can also extract a template, which is the base class of pen. For details, see the template method mode later.

Instances: menu items

The requirement of this example is: there are multiple menu items, and the text of each menu item is different, and the color of the text is different when the mouse slides in and out. We usually write the code as follows:

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 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 code

The above codes seem to be a lot of duplicate. In order to eliminate these duplicate codes, we separate the event binding and color setting dimensions:

// Menu item class receives one more parameter color
function menuItem(word, color) {
  this.dom = document.createElement('div');
  this.dom.innerHTML = word;
  this.color = color;        // Use received color parameters as instance properties
}

// The menu item class adds an instance method for binding events
menuItem.prototype.bind = function() {
  var that = this;      // this here points to the menuItem instance object
  this.dom.onmouseover = function() {
    this.style.color = that.color.colorOver;    // Note that this is this in the event callback, pointing to the DOM node
  }
  this.dom.onmouseout = function() {
    this.style.color = that.color.colorOut;
  }
}

// Create another class to store colors. At present, this class is relatively simple, which can be expanded as needed later
function menuColor(colorOver, colorOut) {
  this.colorOver = colorOver;
  this.colorOut = colorOut;
}

// Now the new menu item can be looped directly with 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++) {
  // Transfer the parameters into instantiation, and finally adjust the bind method, so that the event will be automatically bound.
  new menuItem(menus[i].word, new menuColor(menus[i].colorOver, menus[i].colorOut)).bind();
}
Copy code

The above code is the same idea. We extract the two dimensions of event binding and color respectively, and bridge them when using, so as to reduce a lot of similar code.

Enjoy yuan mode

When we observe that there are a large number of similar code blocks in the code, they may do the same thing, but the objects of each application are different, we can consider using the sharing element mode. Now suppose we have a requirement to display multiple pop ups, each with different text and size:

//There's already a pop-up class
function Popup() {}

//The pop-up class has a display method
Popup.prototype.show = function() {}
Copy code

If we don't use the sharing mode, it's like this:

var popup1 = new Popup();
popup1.show();

var popup2 = new Popup();
popup2.show();
Copy code

We carefully observe the above code, and find that the two instances do the same thing, they are all pop ups, but the size and text of each pop-up window are different, so whether show method can be put forward for public use, and the different parts can be passed in as parameters. In fact, this idea is the sharing mode, which we have transformed 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 needs to receive parameters
}
Copy code

Example: file upload

Let's take another example. If we need to upload files now, we may need to upload multiple files. We usually write code like this:

// An uploaded class
function Uploader(fileType, file) {
  this.fileType = fileType;
  this.file = file;
}

Uploader.prototype.init = function() {}  // Initialization method
Uploader.prototype.upload = function() {}  // Specific upload method

var file1, file2, file3;    // Multiple files to upload
// Each file instantiates an Uploader
new Uploader('img', file1).upload();
new Uploader('txt', file2).upload();     
new Uploader('mp3', file3).upload();  
Copy code

In the above code, we need to upload three files and instantiate three uploaders, but in fact, only the file type and file data are different in these three instances, and the others are the same. We can reuse the same part, and the different parts can be transferred as parameters. The optimization of the sharing mode is as follows:

// File data is thrown into an array
var data = [
  {filetype: 'img', file: file1},
  {filetype: 'txt', file: file2},
  {filetype: 'mp3', file: file3},
];

// After the Uploader class is modified, the constructor will no longer receive parameters
function Uploader() {}

// Other methods on the prototype remain unchanged
Uploader.prototype.init = function() {}

// File type and file data are actually used when uploading, as upload parameters
Uploader.prototype.upload = function(fileType, file) {}

// When calling, only one instance is needed, and the loop calls upload
var uploader = new Uploader();
for(var i = 0; i < data.length; i++) {
  uploader.upload(data[i].filetype, data[i].file)
}
Copy code

In the above code, we simplify three instances into one by extracting parameters, which improves the reusability of Uploader class. The above two examples are similar in fact, but they are only a form of sharing mode. As long as they are consistent with this idea, they can be called sharing mode. For example, the extend method in jQuery also uses sharing mode.

Example: the extend method of jQuery

The extend method of jQuery is often used. It receives one or more parameters:

  1. When there is only one parameter, extend merges the passed in parameters into jQuery itself.
  2. When two parameters obg1 and obj2 are passed in, extend will merge obj2 onto obg1.

According to the above requirements, we can easily achieve by 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 code

this[item] = arguments[0][item] and arguments[0][item] = arguments[1][item] of the above code look very similar. Let's think about whether we can optimize them and look at the two lines of code carefully. They are different in that the copy target and source are different, but the copy operation is the same. Therefore, under the optimization of sharing element mode, we can extract different places and keep the common copy unchanged:

$.extend = function() {
  // Two variables are extracted from different parts
  var target  = this;                  // this is the default, that is, $itself
  var source = arguments[0];           // Default to first variable
  
  // If there are two parameters, change target and source
  if(arguments.length === 2) {       
     target = arguments[0];
  	 source = arguments[1];
  }

  // Common copy operations remain the same
  for(var item in source) {
    target[item] = source[item];
  }
}
Copy code

Template method pattern

The template method pattern is actually similar to inheritance, that is, we first define a general template skeleton, and then continue to expand on this basis. Let's take a look at its basic structure through a requirement. Suppose we need to implement a navigation component now, but there are many types of navigation, some with message prompt, some horizontally, some vertically, and some types may be added later:

//Build a basic class first
function baseNav() {
}

baseNav.prototype.action  =Function (callback) {} / / receive a callback for specific processing
 Copy code

In the above code, we first build a basic class, which has only the most basic properties and methods. In fact, it is equivalent to a template, and it can receive callbacks in specific methods, so that the later derived classes can pass in callbacks according to their own needs. Template method pattern is actually similar to the relationship between object-oriented base class and derived class. Let's take another example.

Example: pop up

It's the same pop-up example that we used before. We need to make a pop-up component with different sizes of text. But this time, our pop-up also has two buttons: Cancel and confirm. These two buttons may have different behaviors in different scenes, such as initiating requests. But they also have a common operation, that is, after clicking these two buttons, the pop-up window will disappear, so that 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() {
  // Initialize DOM elements
  var div = document.createElement('div');
  div.innerHTML = this.word;
  div.style.width = this.size.width;
  div.style.height = this.size.height;
  
  this.dom = div;
}

// Method of cancellation
basePopup.prototype.cancel = function() {
  this.dom.style.display = 'none';
}

// Method of confirmation
basePopup.prototype.confirm = function() {
  this.dom.style.display = 'none';
}
Copy code

Now that we have a basic template, if we need to click cancel or confirm to perform other operations, such as initiating a request, we can add the following operations based on this template:

// Inherit basePopup first
function ajaxPopup(word, size) {
  basePopup.call(this, word, size);
}
ajaxPopup.prototype = new basePopup();
ajaxPopup.prototype.constructor = ajaxPopup;       
// The above is a standard way of inheritance, which is equivalent to applying a template

// Next, add the required operations to initiate the network request
var cancel = ajaxPopup.prototype.cancel;    // Cache the cancel method on the template first
ajaxPopup.prototype.cancel = function() {
  // cancel the template first
  cancel.call(this);     
  // Plus special processing, such as request initiation
  $.ajax();
}

// The confirm method is the same
var confirm = ajaxPopup.prototype.confirm;
ajaxPopup.prototype.confirm = function() {
  confirm.call(this);
  $.ajax();
}
Copy code

The above example implements the template method pattern through inheritance, but this pattern does not have to use inheritance. He emphasizes that some basic parts are extracted as templates, and more later operations can be extended on this basis.

Example: algorithm calculator

We don't need to inherit this example. His requirement is that we now have a series of algorithms, but these algorithms may add some different computing operations when they are used. The operations to be added may be executed before or after this algorithm.

// Define a basic class first
function counter() {
  
}

// There is a calculation method on class
counter.prototype.count = function(num) {
  // There is a basic calculation method of the algorithm itself
  function baseCount(num) {
    // What's the algorithm here is not important, let's add 1 here
    num += 1;
    return num;
  }
}
Copy code

According to the requirements, we need to solve the problem that there may be other calculation operations in the basic algorithm calculation, which may be before or after the basic calculation. Therefore, we need to leave extensible interfaces on this calculation class:

function counter() {
  // Add two queues for pre or post execution of basic algorithm
  this.beforeCounting = [];
  this.afterCounting = [];
}

// Add an interface to receive the calculation before the calculation of basic algorithm
counter.prototype.before = function(fn) {
  this.beforeCounting.push(fn);       // Put the method directly into the array
}

// Add another interface to receive the calculation of the basic algorithm
counter.prototype.after = function(fn) {
  this.afterCounting.push(fn);       
}

// Modify the calculation method so that it can be executed according to pre calculation basic calculation post calculation
counter.prototype.count = function(num) {
  function baseCount(num) {
    num += 1;
    return num;
  }
  
  var result = num;
  var arr = [baseCount];     // Put all the calculations that need to be done into this array
  
  arr = this.beforeCounting.concat(arr);     // Operation before calculation before array
  arr = arr.concat(this.afterCounting);      // Post calculation operation after array
  
  // Take out and execute all arrays 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 calculation
counterIntance.after(num => num - 5);        // Subtract 5 after calculation

counterIntance.count(2);     // 2 + 10 + 1 - 5  = 8
Copy code

This time we didn't use inheritance, but we still defined a basic operation skeleton, and then expanded the special operations needed in different places on the skeleton.

summary

  1. If a large number of similar code blocks appear in our code, it often means that there is room for further optimization.
  2. If these repeated code blocks can be split into different dimensions, you can try the bridging mode, first split the dimensions, and then bridge these dimensions for use.
  3. If some of the operations of these repeated codes are the same, but the objects of each operation are different, we can consider extracting the public operation into a method with the sharing element mode, and passing the private part in as a parameter.
  4. If some basic operations of these repeated codes are the same, but the specific application needs more functions, we can consider extracting these basic operations into a template, and then leaving an extension interface on the template, where we need to extend the functions through these interfaces, which is similar to inheritance, but the implementation is not limited to inheritance.
  5. We will extract the repeated part, and other places can also be used, in fact, it is to improve the reusability of the code.
  6. In the same sentence, there is no fixed paradigm for design patterns. It is mainly to understand his ideas. Code can be implemented in different ways in different places.

At the end of the article, thank you for your precious time reading this article. If this article gives you a little help or inspiration, please don't be stingy with your praise and little GitHub star. Your support is the driving force of the author's continuous creation.

The material of this article comes from Netease senior front-end development engineer Mr. Tang Lei's design mode course.

Author's blog GitHub project address: github.com/dennis-jian...

Summary of the author's gold digging articles: juejin.im/post/5e3ffc...

Tags: JQuery github Database less

Posted on Tue, 26 May 2020 04:13:14 -0400 by biocyberman