Javascript-5 common design patterns
preface
There are many design patterns, but how do they apply to the front end? What are some common design patterns in front-end coding practices?
Definition of design patterns
Design patterns are simple and elegant solutions to specific problems in object-oriented software design. Design patterns may be implemented differently in different programming languages. In statically compiled languages like Java, for example, you can’t dynamically add responsibilities to existing objects, so the decorator pattern is typically implemented by wrapping classes. But in JavaScript, assigning responsibilities to objects dynamically is so simple that the decorator pattern of the JavaScript language focuses not on assigning responsibilities to objects dynamically, but on assigning responsibilities to functions dynamically.
Five common design patterns for the front end
- The factory pattern
- The singleton pattern
- The proxy pattern
- The strategy pattern
- Observer model
The factory pattern
The factory pattern, a common design pattern used to create objects, does not expose the concrete logic of creating objects, but encapsulates the logic in a function. That is, this function can be regarded as a factory, and the factory mode can be divided into:
- Simple factory
- The factory method
- The abstract factory
Here are some simple examples of simple factories and factories being used in JavaScript.
Simple factory
A simple factory, also known as a static factory, is a factory object that decides to create an instance of a product object class. It is used to create objects of the same class.
For example, in actual project development, we might need to render different pages based on user permissions, and some pages owned by users with advanced permissions cannot be viewed by users with lower permissions. Therefore, we can save the page that can be seen by the user of different permission levels in the constructor.
function UserFactory(role){
function SuperAdmin(){
this.name = 'superManager'
this.viewPage = ['home'.'userManage'.'orderManage'.'appManage'.'permManage']}function Admin(){
this.name = 'admin'
this.viewPage = ['home'.'orderMange'.'appManage']}function NormalUser(){
this.name = 'normalUser'
this.viewPage = ['home'.'orderManage']}switch(role){
case 'superAdmin':
return new SuperAdmin()
break
case 'admin':
return new Admin()
break
case 'user':
return new NormalUser()
break
default:
throw new Error('Parameter error, optional parameters :superAdmin, admin, user')}}let superAdmin = UserFactory('superAdmin')
let admin = UserFactory('admin')
let normalUser = UserFactory('user')
Copy the code
summary
In the example above, UserFactory is a simple factory with three constructors for different users. When we call the factory function, we only need to pass the superAdmin, admin, and user optional arguments to get a corresponding instance object.
- Advantage: With only one correct argument, you can get the object you need without knowing the details of the creation.
- Disadvantages: the function contains the creation logic (constructor) and the judgment logic code for all objects. Every time a new constructor is added, the judgment logic code needs to be modified. If the above object is 30 or more, the function will become a huge super function, which is difficult to maintain. Simple factories can only be used when a small number of objects are being created and the creation logic of objects is not complex
The factory method
The factory method is to defer the actual creation of the object to the subclass, so that the core class becomes an abstract class, but in JavaScript, it’s very difficult to create abstract classes in the traditional object-oriented way, so in JavaScript we just refer to the core idea of it, We can think of a factory method as a factory class that instantiates an object.
For example, in the example above, we use the factory method to transform. Factory method, we just think of it as a factory for instantiating objects, it only does one thing, instantiating objects, we create objects in safe mode.
function UserFactory(role){
if(this instanceof UserFactory){
if(typeof this[role] ! = ='function') throw new Error('Parameter error, optional parameters :superAdmin, admin, user')
const s = new this[role]();
return s;
}else {
return new UserFactory(role)
}
}
UserFactory.prototype = {
SuperAdmin: function (){
this.name = 'superManager'
this.viewPage = ['home'.'userManage'.'orderManage'.'appManage'.'permManage']},Admin: function (){
this.name = 'admin'
this.viewPage = ['home'.'orderMange'.'appManage']},NormalUser: function (){
this.name = 'normalUser'
this.viewPage = ['home'.'orderManage']}}const superAdmin = UserFactory('SuperAdmin');
const admin = UserFactory('Admin')
const normalUser = UserFactory('NormalUser')
const user = UserFactory('user')
Copy the code
summary
In a simple factory, if we add a new user type, we need to change the code in two places:
- Add a new user constructor
- Add judgment for new users to logical judgment
In the abstract factory method, we just add it to userFactory. prototype.
The singleton pattern
The singleton pattern ensures that a class has only one instance and provides a global access point to access it
In some specific requirements scenarios, we need to ensure that only one object is required, for example:
- The thread pool
- Global cache
- The Window object in the browser
- The login window etc.
A variable is used to identify whether an object has been created for a class. If so, the next time an instance of the class is retrieved, the object is returned.
The advantages of this are:
- It can be used to partition namespaces and reduce the number of global variables
- Can be instantiated and instantiated only once
Let’s look at a simple example where we can implement this pattern using closures in JavaScript:
var cls = (function(){
var instance;
function getInstance(){
if(instance === undefined){
instance = new Construct()
}
return instance;
}
function Construct(){
/ /... The constructor
}
return {
getInstance: getInstance
}
})()
Copy the code
summary
In the above code, we can use cls.getInstance to get the singleton and get the same singleton every time we call it.
In our normal development, we often use this model, such as when we click the login button, the page will appear a login box, and the floating window is the only, no matter how many times you click the login button, the floating window can only be created once, so the login window is suitable to use the singleton pattern.
The proxy pattern
The proxy pattern is a very meaningful pattern, you can find many proxy pattern scenarios in life.
For example, celebrities are represented by agents. If you want a star to do a commercial show, you have to contact his agent. The agent will negotiate the details of the commercial performance and the payment before handing the contract to the star to sign. The key to the proxy model is to provide a surrogate object to control access to an object when it is inconvenient or not necessary for the client to directly access it. After the proxy object does some processing of the request, it passes the request to the ontology object.
The virtual proxy implements image preloading
In Web development, image preloading is a common technique. If you directly set the SRC attribute to an IMG tag node, the position of the image will be blank for a period of time due to the large image or poor network. A common practice is to load the image asynchronously with a loading image, and then fill the image into the IMG node after the image is loaded. This scenario is suitable for using a virtual agent.
var myImage = (function(){
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
return function(src){
imgNode.src = src;
}
})();
var proxyImage = (function(){
var img = new Image;
img.onload = function(){
myImage(this.src);
};
return function(src){
myImage("file:// /C:/Users/sven/Desktop/loading.gif");
img.src = src;
}
})();
proxyImage("http://img/cache/com/music/dklddsafla.jpg");
Copy the code
The virtual proxy merges HTTP requests
Perhaps the biggest overhead in Web development is network requests. If we are doing a file synchronization function, when we select a checkbox, its corresponding files will be synchronized to another standby server. When we checked the three checkboxes, we sent three requests to the server to synchronize the file. Clicking is not a very complicated operation. It is not difficult to click 4 checkboxes in one second. Such frequent network requests will bring considerable overhead. The solution is to use a proxy function, proxySynchronousFile, to collect requests over a period of time and finally send them to the server once. For example, we wait for 2 seconds before packaging the file ID that needs to be synchronized within 2 seconds and send it to the server. If the system is not very real-time demanding, the 2-second delay will not bring too much side effect, but can greatly reduce the pressure on the server. The code is as follows:
var synchronousFile = function(id){
console.log("Start file synchronization with id:" + id);
};
var proxySynchronousFile = (function(){
var cache = [];
var timer;
return function(id){
cache.push(id);
if(timer){
return;
}
timer = setTimeout(function(){
synchronousFile(cache.join(","));
clearTimeout(timer);
timer = null;
cache.length = 0;
}, 2000); }; }) ();var checkbox = document.getElementsByTagName("input");
for(var i= 0, c; c=checkbox[i++];) { c.onclick =function(){
if(this.checked === true){
proxySynchronousFile(this.id); }}}Copy the code
The strategy pattern
The strategy mode refers to defining a series of algorithms and encapsulating them one by one. The purpose is to separate the use of algorithms from the implementation of algorithms, avoid multiple judgment conditions, and have more expansibility.
For example, now there is an event in the supermarket, VIP gets 50% discount, regular customers get 30% discount, and ordinary customers don’t get any discount. Calculate the amount to be paid at the end. If the strategy mode is not used, our code may be the same as the following:
function Price(personType, price) {
/ / VIP 5 fold
if (personType == 'vip') {
return price * 0.5;
}
else if (personType == 'old') {// Regular customers get a 30% discount
return price * 0.3;
} else {
return price; // All others are full price}}Copy the code
In the above code, we need a lot of judgments, and if there are a lot of preferences, we need to add a lot of judgments. This violates the open and close principle of the six principles of design patterns. If we use the policy pattern, our code can be written like this:
class Customer{
constructor(name,discount){
this.name = name
this.discount = discount
}
getPrice(price){
return this.discount * price
}
hello(){
throw new Error("Method not implemented!")}}class VipCustomer extends Customer{
constructor(username){
super('vip'.0.5)
this.username = username
}
hello(){
return 'Dear VIP users,The ${this.username}Hello! `}}class OldCustomer extends Customer{
constructor(username){
super('normal'.0.89)
this.username = username
}
hello(){
return Dear old users,The ${this.username}Hello! `}}const vip = new VipCustomer('a little')
window.console.log(vip.hello())
window.console.log("The checkout price for VIP customers is :", vip.getPrice(200))
const old = new OldCustomer('small army')
window.console.log(old.hello())
window.console.log("The closing price for ordinary customers is :", old.getPrice(200))
Copy the code
summary
Summary: In the above code, through the strategy mode, make the customer discount and algorithm decouple, and make the modification and expansion can be carried out independently.
When we have multiple judgment branches in our code, and each conditional branch causes the specific behavior of the “class” to change in different ways, we can use policy patterns to improve the quality of our code and make it better for unit testing.
Observer model
A common implementation of the publish-subscribe pattern
Real life example – web site login
Let’s say we’re developing a shopping site with a header, nav navigation, message list, shopping cart, etc. The common premise for rendering of these modules is that an Ajax asynchronous request must be made to obtain the user’s login information. There is no certainty as to when an Ajax request will successfully return user information. Now the plot looks very much like the sales office example, Xiao Ming does not know when the developer’s sales procedures can be successfully done down. More importantly, we don’t know what modules will need to use this user information in the future besides header headers, NAV navigation, message lists, and shopping carts. If they are strongly coupled to user information modules, such as the following:
login.succ(function(data){
header.setAvatar(data.avatar); // Set the header image
nav.setAvatar(data.avatar); // Set the image of the navigation module
message.refresh(); // Refresh the message list
cart.refresh(); // Refresh the shopping cart list
});
Copy the code
The header module cannot change the name of the setAvatar method at will, nor can it change its name to header1 or header2. This is a classic example of implementation-specific programming, which is frowned upon. After being rewritten in publish-subscribe mode, business modules interested in user information will subscribe to a successful login message event themselves. When the login is successful, the login module only needs to publish the successful login message, and the business side will start their respective business processing after receiving the message. The login module does not care about what the business side wants to do, and does not want to know their internal details. Under the improved code:
$.ajax("http://xxx.com?login".function(data){ // Login succeeded
login.trigger("loginSucc", data); // Publish a successful login message
});
// And the module listens for a successful login message
var header = (function(){ / / the header module
login.listen("loginSucc".function(data){
header.setAvatar(data.avatar);
});
return {
setAvatar: function(data){
console.log("Set header module avatar!"); }}}) ();var nav = (function(){ / / nav module
login.listen("loginSucc".function(data){
nav.setAvatar(data.avatar);
});
return {
setAvatar: function(avatar){
console.log("Set the avatar of the nav module!"); }}}) ();var address = (function(){ // Shipping address module
login.listen("loginSucc".function(obj){
address.refresh(obj);
});
return {
refresh: function(avatar){
console.log("Refresh the shipping address list!"); }}}) ();Copy the code
Global publish-subscribe objects
In a program, the publish-subscribe model can be implemented with a global Event object. Subscribers do not need to know which publisher the message comes from, and publishers do not know which subscribers the message will be pushed to. Events act as a kind of “intermediary”, connecting subscribers and publishers. See the following code:
However, there is another problem to be aware of here. If modules communicate with each other in too many global publish-subscribe modes, the module-to-module connections can be hidden in the background. We end up not knowing which module the message is coming from, or which module the message is going to, which can cause some trouble in our maintenance. Perhaps the purpose of a module is to expose interfaces to other modules.
Naming conflicts for global events
In the publish-subscribe model we know, the subscriber subscribs to a message before receiving the message published by the publisher. If the order were reversed, the publisher would publish a message first, and no object had subscribed to it before then, the message would surely disappear into the universe. In some cases, we need to save the message and re-publish it to subscribers when an object subscribes to it. Just like the offline message in QQ, the offline message is saved in the server, and the recipient can receive the message again after the next login. This kind of need relief exists in actual projects. For example, in the website of discount Mall, the user navigation module can be rendered only after obtaining user information, and the operation of obtaining user information is an Ajax asynchronous request. When the Ajax request returns successfully, an event is published, and the user navigation module that subscribed to the event can receive the user information. But this is the ideal situation, because the cause of the asynchronous, we cannot guarantee an ajax request to return to the time, sometimes it returns faster, while the user navigation module code has not been loaded (haven’t subscribe the corresponding events), especially in with some modular lazy loading technique, it is likely to happen. Perhaps we also need a solution that gives publish-subscribe capabilities to our publish-subscribe objects. In order to meet this requirement, we need to build a stored offline event stack, when events are published, if haven’t the subscriber to subscribe to this event, we’ll leave the publish event action wrapped in a function, the packaging function will be deposited in the stack, until finally have object to subscribe to this event, We will traverse the stack and execute these wrapper functions in turn, republishing the events inside. Of course, the life cycle of offline events is only once, just like QQ’s unread information will only be re-read once, so we can only carry out the operation just now once.