Design pattern is a relatively macro concept, design pattern definition is the solution of some representative problems faced by software developers in the process of software development. These solutions have been developed by numerous software developers over a long period of trial and error. Js functions, classes, components and so on are actually realized the reuse of code, so the design pattern can be said to be the reuse of experience. Of course, the requirements can also be achieved without design patterns in actual development, but in the case of complex business logic, the code readability and maintainability become worse. So as business logic expands, understanding common design patterns is a must to solve problems. Design patterns can literally be divided into two parts. One is design, the other is pattern. Design is design principle and pattern is design pattern. Design patterns generally follow design principles. So what are the design principles? As follows:
- Single Responsibility Principle
A class should have only one reason to change. In short, each class is only responsible for its own part, and the complexity of the class is reduced.
-
The Open Closed Principle
- Software entities such as classes, modules, and functions should be open for extension and closed for modification
-
All places that reference a base class must be able to use objects of its subclass transparently, that is, subclass objects can replace objects of their parent class without program execution.
-
The Law of Demeter, also known as The Least Knowledge Principle, states that The less a class knows about another class, The better. This means that an object should know as little as possible about other objects, and should correspond only with friends and not talk to strangers.
-
Interface Segregation Principle
- Multiple specific client interfaces are better than one universal master interface
-
Upper-level modules should not depend on lower-level modules, they should all depend on abstractions. Abstract should not depend on details, details should depend on the abstract
Should all designs follow all design principles in real development? In many cases, it is not entirely bound by design principles. Specific situations need to be analyzed on a case-by-case basis, and we will find that some programming actually violates some design principles, so we need to be flexible when using it. Next comes design patterns, which can be divided into three broad categories: 1. Creation. 2. Structural. 3. Behavioral.
The singleton pattern
- The Singleton Pattern, also known as the Singleton Pattern, guarantees that a class has only one instance and provides a global access point to it. That is, the second time you create a new object using the same class, you should get exactly the same object that you created the first time.
Implementing an example can be done with static attributes as follows:
class Person{
static instance;
constructor(name){
if(Person.instance){
return Person.instance;
}else{
Person.instance = this;
}
this.name = name; }}Copy the code
Singletons are implemented by recording the instantiation state of the Person class with the static attribute instance. However, this implementation is not flexible. If we have multiple classes that need to implement singletons, we need to write static members to each class to preserve the instantiation state. Is there a universal way to do this? In fact, we can record the instantiation state of a class through higher-order functions and the caching feature of closures. The specific code is as follows:
class Person{
constructor(name){
this.name = name; }}class Animal{
constructor(name){
this.name = name; }}function getSingle(fn){
let instance;
return function(. args){
if(! instance){ instance =newfn(... args); }returninstance; }}let PSingle = getSingle(Person);
let PAnimal = getSingle(Animal);
let zhangsan = new PSingle("Zhang");
let lisi = new PSingle("Bill");
console.log(zhangsan,lisi);
Copy the code
The code above uses the getSingle higher-order function to implement a multi-class singleton. Singleton advantage: The singleton mode saves memory cost and performance cost during instantiation, saving performance. Of course, there are drawbacks to singletons: singletons don’t scale very well. We need to use the singleton pattern when we only need one instance to design a program. For example, the document and window objects of native JS are actually a kind of embodiment of singletons, and there is only one instance in the global scope.
The factory pattern
Encapsulate the specific instance creation logic and process, and the external only needs to return different instances according to different conditions. Such as:
class Luban{
constructor(){
this.name = "Ban"; }}class Yase{
constructor(){
this.name = "Arthur"; }}function Factory(type){
if(type==="Ban") {return new Luban();
}else if(type==="Arthur") {return newYase(); }}let luban = Factory("Ban");
console.log(luban);
Copy the code
In many cases, the factory pattern can be replaced by constructors and classes. Its advantages are code reuse, good encapsulation, abstract logic. Disadvantages also significantly increase the complexity of the code.
Decorator mode
Use a more flexible way to dynamically add additional information to an object/function, etc. That is, extending functionality or properties on top of the original object or functionality. In line with the design principles of the open and closed principle, through the decorative extension function can be as far as possible to ensure the purity of the interior, to ensure that the interior is less modified or not modified. The code is as follows:
class Yase {
constructor(){
this.name = "Arthur";
}
release(){
console.log("Unleashing skills"); }}Copy the code
As mentioned above, the Yase class wants to extend the release method functionality. We have several options: 1. Modify the extension directly in the Release method, which violates the open/close principle. 2. It is also possible to extend release through extends. 3. You can decorate the Release method with decorators of your choice to make it more functional. The code is as follows:
function hurt(){
console.log("Deal 100 damage.");
}
Function.prototype.Decorator = function(fn){
this(a); fn(); }let yase = new Yase();
// yase.release();
yase.release.Decorator(hurt); // Cast spells that deal 100 damage
Copy the code
This extends the Hurt method through the Decorator Decorator, making the Release method even more powerful. The goal is to keep the purity of the original class and avoid excessive side effects. Of course, the drawback is that the extended functionality is also independent of the original class.
Observer model
Defines a dependency between an object and other objects so that when an object changes, all other objects that depend on it are updated. The code is as follows:
export default class GameEvent{
constructor() {
this.handle = {};
}
addEvent(eventName, fn) {
if (typeof this.handle[eventName] === 'undefined') {
this.handle[eventName] = [];
}
this.handle[eventName].push(fn);
}
trigger(eventName) {
if(! (eventNamein this.handle)) {
return;
}
this.handle[eventName].forEach(v= >{ v(); }}})Copy the code
The advantages of observer mode can be seen from 1. Support one-to-many relationship. 2. You can delay the execution of events. 3. Decouple the two objects. The observer mode is widely used in native JS, NodeJS, Vue, and React.
The proxy pattern
The proxy pattern provides a proxy for other objects to control access to that object, similar to a mediation in life. In fact, ES6 Proxy is a representation of the Proxy mode. You can use Proxy objects to control object access. For example, implement the following code as a mediation:
let zhangsan = {
sellhouse(num){
console.log("Sold:" +num + "Yuan"); }}let proxySeller = {
sellhouse(hasSold,num){
if(hasSold){
zhangsan.sellhouse(num-10);
}else{
zhangsan.sellhouse(num);
}
}
}
proxySeller.sellhouse(true.100);
Copy the code
The above code uses the intermediary, that is proxySeller, to sell The house for Zhangsan and control the output of Zhangsan. The example is easy to understand. In JS, the anti-shake function also uses the proxy mode, as follows:
function debounce(fn, time) {
time = time || 200;
let timer;
return function (. arg) {
clearTimeout(timer);
timer = setTimeout(() = > {
fn.apply(this, arg);
}, time)
}
}
let newFn = debounce(fn,50);
window.onresize = function () {
// fn();
// console.log(newFn)
newFn();
}
Copy the code
Control the fn’s execution frequency by proxy using a new function returned by the debounce high-order function. The advantage is also in line with the open – close principle of extended control function. The disadvantage is that the proxy outcome is affected by the proxy.
Adapter mode
A bridge between two incompatible interfaces that converts the interface of one class into the desired interface of another. The adapter pattern makes it possible for classes to work together that would otherwise not work because of interface incompatibilities. The simple idea is to make two interfaces compatible through an adapter. Such as:
function getUsers(){
return[{name:"zhangsan".age:20}, {name:"lisi".age:30}}]function Adaptor(users){
let arr = [];
for(let i=0; i<users.length; i++){ arr[users[i].name] = users[i].age; }return arr;
}
let res = Adaptor(person());
console.log(res);
Copy the code
A similar {zhangsan: 20} data structure is expected in the above code. We use the Adaptor class to realize axiOS in the node end and front-end adaptation work to achieve data conversion. Advantages of the adapter pattern: 1. You can run any two unrelated classes together. 2. Improved class reuse. 3. Increased class transparency. 4. Good flexibility. The downside: too much use of adapters will make the system very cluttered and not easy to grasp as a whole. In axiOS source code through the Adaptor class to achieve AXIOS in the node end and front-end adaptation.
With model
Mixins are classes that can easily be inherited by a subclass or group of subclasses for functional reuse. Inheriting mixins is a way to extend functionality, and it is also possible to inherit from multiple Mixin classes. For example, there are now two classes, one Yase and one Skills. As follows:
class Yase{
constructor(name){
this.name = name; }}class Skills{
hurt(){
console.log("Deal 100 damage.");
}
walk(){
console.log("Walk...");
}
run(){
console.log("Running..."); }}Copy the code
If you want to inherit both the yase class and the Skills class, of course you can have Skills inherit the Yase class, but there is no logical connection. At this time, we can consider mixing the functions of the two classes through mixin mode as follows:
function mixin(receivingClass,givingClass){
if(arguments[2]) {for(let i=2; i<arguments.length; i++){ receivingClass.prototype[arguments[i]] = givingClass.prototype[arguments[i]]
}
}
}
mixin(Yase,Skills,"run"."walk");
let yase = new Yase("Arthur");
console.log(yase);
yase.walk();
Copy the code
As above, the code is designed to be more readable. Successfully mixing functionality from multiple classes. Mixin pattern benefits improve readability and increase code reuse. The downside is that injecting functionality into a prototype can lead to a confusing class or method orientation and no source for extending methods. Mixin mode has the shadow of mixin in vUE and React.
Design patterns were mainly used in back-end applications at first, but with the complexity of front-end engineering and business logic, many design patterns were gradually used in the front-end. Understanding the front-end design pattern is the only way to improve the front-end programming thinking. Of course, there’s more to design patterns than that. Later and then gradually updated. At this point, it’s time to leave work. Gone, gone…
- This is element3, the open source project of our Flower Mountain front end team
- A front-end component library that supports VUE3