Don't know how to package code? Look at these design patterns!

Why encapsulate code?

We often hear that "writing code needs good encapsulation, high cohesion and low coupling". So what is a good package? Why should we package it? In fact, encapsulation has several advantages:

  1. Encapsulated code, internal variables do not pollute the external.
  2. It can be called externally as a module. External callers don't need to know the details of the implementation, just use it according to the agreed specifications.
  3. Open to extension, close to modification, i.e. open close principle. The external module can not be modified, which not only ensures the correctness of the internal module, but also leaves the expansion interface for flexible use.

How to encapsulate code?

There are many modules in JS ecosystem, some of which are well packaged and easy to use, such as jQuery, Vue, etc. If we look at the source code of these modules carefully, we will find that their encapsulation is regular. These rules are summed up as design patterns. There are four design patterns for code encapsulation: factory pattern, creator pattern, singleton pattern and prototype pattern. Let's take a look at these four design patterns in combination with some framework source codes:

Factory mode

The name of the factory pattern is very straightforward. The encapsulated module is like a factory to produce the required objects in batches. One of the characteristics of the common factory pattern is that it does not need to use new when calling, and the parameters passed in are relatively simple. But the number of calls may be more frequent, often need to produce different objects, frequent calls without new is also convenient. The code structure of a factory pattern is as follows:

function factory(type) {
  switch(type) {
    case 'type1':
      return new Type1();
    case 'type2':
      return new Type2();
    case 'type3':
      return new Type3();
  }
}
Copy code

In the above code, we passed in type, and then the factory created different objects according to different types.

Example: pop up components

Let's take a look at the example of using factory mode. If we have the following requirements:

We need a pop-up window for our project. There are several kinds of pop-up windows: Message pop-up window, confirmation pop-up window and cancellation pop-up window. Their colors and contents may be different.

For these kinds of pop ups, let's build a class respectively:

function infoPopup(content, color) {}
function confirmPopup(content, color) {}
function cancelPopup(content, color) {}
Copy code

If we use these classes directly, it's like this:

let infoPopup1 = new infoPopup(content, color);
let infoPopup2 = new infoPopup(content, color);
let confirmPopup1 = new confirmPopup(content, color);
...
Copy code

Every time we use it, we need to go to the pop-up window class corresponding to new. We use the factory mode to transform it, which is like this:

// Add a new method, pop, to package all these classes
function popup(type, content, color) {
  switch(type) {
    case 'infoPopup':
      return new infoPopup(content, color);
    case 'confirmPopup':
      return new confirmPopup(content, color);
    case 'cancelPopup':
      return new cancelPopup(content, color);
  }
}
Copy code

Then we can use pop instead of new, just call the function directly:

let infoPopup1 = popup('infoPopup', content, color); 
Copy code

Transform to object-oriented

Although the above code implements the factory mode, switch does not always feel very elegant. We use the object-oriented transformation of pop-up, change it to a class, and mount different types of pop ups on this class to become a factory method:

function popup(type, content, color) {
  // If it is called through new, the corresponding type of pop-up window is returned
  if(this instanceof popup) {
    return new this[type](content, color);
  } else {
    // If it is not called by new, it will go to the above line
    return new popup(type, content, color);
  }
}

// All kinds of pop ups are attached to the prototype, which becomes an example method
popup.prototype.infoPopup = function(content, color) {}
popup.prototype.confirmPopup = function(content, color) {}
popup.prototype.cancelPopup = function(content, color) {}
Copy code

Package into modules

This pop-up not only makes us call a new less, but also encapsulates various related pop-up windows. This pop-up can be directly export ed as a module for others to call, or mounted on the window as a module for others to call. Because pop-up encapsulates all kinds of details of pop-up window. Even if the pop-up is changed internally, or the pop-up type is added, or the name of pop-up class is changed, as long as the external interface parameters are kept unchanged, it has no impact on the external. As a module mounted on the window, self executing functions can be used:

(function(){
 	function popup(type, content, color) {
    if(this instanceof popup) {
      return new this[type](content, color);
    } else {
      return new popup(type, content, color);
    }
  }

  popup.prototype.infoPopup = function(content, color) {}
  popup.prototype.confirmPopup = function(content, color) {}
  popup.prototype.cancelPopup = function(content, color) {}
  
  window.popup = popup;
})()

// You can use the pop module directly outside
let infoPopup1 = popup('infoPopup', content, color); 
Copy code

The factory mode of jQuery

JQuery is also a typical factory mode. If you give it a parameter, it will return the DOM object that matches the parameter. So how does jQuery, a factory mode without new, come true? In fact, it is jQuery that helps you call new internally. The call process of jQuery is simplified as follows:

(function(){
  var jQuery = function(selector) {
    return new jQuery.fn.init(selector);   // new init, init is the real constructor
  }

  jQuery.fn = jQuery.prototype;     // jQuery.fn namely jQuery.prototype Short for

  jQuery.fn.init = function(selector) {
    // It implements the real constructor
  }

  // Let init and jQuery prototypes point to the same object, which is convenient to mount instance methods
  jQuery.fn.init.prototype = jQuery.fn;  

  // Finally, Mount jQuery to window
  window.$ = window.jQuery = jQuery;
})();
Copy code

The above code structure comes from jQuery source code, from which we can see that the new omitted when you call is used in jQuery for your convenience. However, this structure requires an init method. Finally, jQuery and init prototypes are bound together. In fact, there is a simpler way to achieve this requirement:

var jQuery = function(selector) {
  if(!(this instanceof jQuery)) {
    return new jQuery(selector);
  }
  
  // Let's do the real constructor
}
Copy code

The above code is much simpler and can also be called directly without new. The feature used here is that when this function is called by new, it points to the object out of new. Naturally, the object out of new is the instance of class, and this instanceof jQuery here is true. If it's a normal call, it's false. Let's help him new.

Builder pattern

The builder mode is used to build complex large objects, such as Vue, which contains a powerful and logically complex object. Many parameters need to be passed in when building. There are not many cases that need to be created like this. When the created object itself is very complex, the builder mode is applicable. The general structure of the builder model is as follows:

function Model1() {}   // Module 1
function Model2() {}   // Module 2

// Final class
function Final() {
  this.model1 = new Model1();
  this.model2 = new Model2();
}

// When in use
var obj = new Final();
Copy code

In the above code, we finally use final, but the structure of final is relatively complex, and there are many sub modules. Final is to combine these sub modules to complete the function, and the one that needs to be refined is applicable to the builder mode.

Instance: Editor plug in

Suppose we have a requirement:

To write an editor plug-in, you need to configure a large number of parameters during initialization, and the internal functions are very complex. You can change the font color and size, or move forward and backward.

Generally, there is only one editor for a page, and the functions in it may be complex, and you may need to adjust colors, fonts, etc. That is to say, other classes may be called inside the plug-in and then combined to implement functions, which is suitable for the builder mode. Let's analyze which modules are needed for such an editor:

  1. The editor itself definitely needs a class, which is an interface for external calls
  2. Need a class to control parameter initialization and page rendering
  3. Need a class to control fonts
  4. Need a state managed class
// Editor itself, exposed
function Editor() {
  // The editor is to combine all modules to realize functions
  this.initer = new HtmlInit();
  this.fontController = new FontController();
  this.stateController = new StateController(this.fontController);
}

// Initializing parameters, rendering pages
function HtmlInit() {
  
}
HtmlInit.prototype.initStyle = function() {}     // Initialize style
HtmlInit.prototype.renderDom = function() {}     // Render DOM

// Font controller
function FontController() {
  
}
FontController.prototype.changeFontColor = function() {}    // Change font color
FontController.prototype.changeFontSize = function() {}     // size of font changing

// State controller
function StateController(fontController) {
  this.states = [];       // An array to store all States
  this.currentState = 0;  // A pointer to the current state
  this.fontController = fontController;    // Inject font manager to change font when changing state
}
StateController.prototype.saveState = function() {}     // Save state
StateController.prototype.backState = function() {}     // Backward state
StateController.prototype.forwardState = function() {}     // Forward state
Copy code

In fact, the above code sets up the shelf of an editor plug-in. The specific implementation function is to fill in specific contents in these methods, which is actually the mutual call of each module. For example, if we want to realize the function of backward state, we can write as follows:

StateController.prototype.backState = function() {
  var state = this.states[this.currentState - 1];  // Remove previous state
  this.fontController.changeFontColor(state.color);  // Change back to last color
  this.fontController.changeFontSize(state.size);    // Change back to last size
}
Copy code

Singleton mode

The single instance mode is applicable to scenarios where there can only be one instance object globally. The general structure of the single instance mode is as follows:

function Singleton() {}

Singleton.getInstance = function() {
  if(this.instance) {
    return this.instance;
  }
  
  this.instance = new Singleton();
  return this.instance;
}
Copy code

In the above code, the Singleton class mounts a static method getInstance. If you want to get an instance object, you can only get it through this method. This method will check whether there is an existing instance object. If there is one, return it. If not, create a new one.

Instance: global data store object

If we have such a need now:

We need to manage a global data object. There can only be one object. If there are multiple objects, the data will not be synchronized.

This requirement requires that there is only one data storage object globally, which is a typical scenario suitable for single instance mode. We can directly apply the above code template, but the above code template must be called getInstance to get instance. If a user directly calls Singleton() or new There will be a problem with singleton (). This time, we will change the writing method to make it compatible with singleton () and new Singleton(). It is more stupid to use it:

function store() {
  if(store.instance) {
    return store.instance;
  }
  
  store.instance = this;
}
Copy code

The above code supports the use of new For the call of store(), we use a static variable, instance, to record whether it has been instantiated. If it has been instantiated, it will return to this instance. If it is not instantiated, it is the first call, so we will assign this to this static variable. Because it is a new call, at this time, this refers to the instantiated object, and it will be implicit at last This is returned by.

If we also want to support the direct call of store(), we can use the methods used in the previous factory mode to check whether this is an instance of the current class. If not, we can help him call it with new:

function store() {
  // Add an instanceof detection
  if(!(this instanceof store)) {
    return new store();
  }
  
  // The bottom is the same as the front
  if(store.instance) {
    return store.instance;
  }
  
  store.instance = this;
}
Copy code

Then we call it in two ways to detect:

Example: Vue router

In fact, Vue router also uses the singleton mode, because if a page has multiple routing objects, it may cause state conflicts, and the singleton implementation of Vue router is a little different, The following code is from the Vue router source code:

let _Vue;

function install(Vue) {
  if (install.installed && _Vue === Vue) return;
  install.installed = true

  _Vue = Vue
}
Copy code

Every time we call vue.use In fact, the user will execute the install method of the Vue router module. If the user accidentally calls it multiple times vue.use(vueRouter) will cause multiple executions of install, resulting in incorrect results. When the install of Vue router is executed for the first time, the installed property is written as true, and the current Vue is recorded, so that the subsequent execution of install in the same Vue will return directly, which is also a singleton mode.

We can see that the three codes here are all single instance patterns. Although they have different forms, the core idea is the same. They use a variable to mark whether the code has been executed. If it has been executed, the last execution result will be returned. This ensures that multiple calls will get the same result.

Prototype mode

The most typical application of prototype pattern is JS itself. The prototype chain of JS is prototype pattern. Can be used in JS Object.create Specify an object as the prototype to create the object:

const obj = {
  x: 1,
  func: () => {}
}

// Create a new object based on obj
const newObj = Object.create(obj);

console.log(newObj.__proto__ === obj);    // true
console.log(newObj.x);    // 1
Copy code

In the above code, we take obj as the prototype, and then use Object.create All new objects created will have properties and methods on this object, which is actually a prototype mode. In fact, the object-oriented of JS is the embodiment of this pattern. For example, inheritance of JS can be written as follows:

function Parent() {
  this.parentAge = 50;
}
function Child() {}

Child.prototype = new Parent();
Child.prototype.constructor = Child;      // Note reset constructor

const obj = new Child();
console.log(obj.parentAge);    // 50
Copy code

The inheritance here is actually to let the subclass Child.prototype H. proto__ To get the methods and properties of the parent class. There are many object-oriented contents in JS. I will not expand them here. There is an article devoted to this problem.

summary

  1. Many open-source libraries that are easy to use have good encapsulation. Encapsulation can isolate the internal environment from the external environment, and the external environment is easier to use.
  2. There are different encapsulation schemes for different scenarios.
  3. Components that need to generate a large number of similar instances can be considered to be encapsulated with factory patterns.
  4. The internal logic is more complex, and there are not many instances needed for external use. You can consider using builder pattern to encapsulate.
  5. The global can only have one instance, which needs to be encapsulated by the singleton pattern.
  6. If there may be inheritance relationship between new and old objects, we can consider using prototype pattern to encapsulate. JS itself is a typical prototype pattern.
  7. When using the design pattern, do not copy the code template mechanically. More importantly, master the idea. The same pattern can have different implementation schemes in different scenarios.

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 GitHub. Your support is the author's driving force for continuous creation.

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

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

Tags: JQuery Vue github Windows

Posted on Sat, 23 May 2020 06:39:33 -0400 by conor.higgins