We developers often say, “Talk is cheap, show me the code.” When it comes to writing code that is pleasing to the eye, I think it’s important to use the right design patterns.

Psychological massage part

The code we write is our calling card, but pulling out code that is not designed is not only hard to read, it also makes people question our abilities.

And hinder our progress is when you go to read someone else’s framework or source code, the author often USES a large number of design patterns, if we can’t design pattern is sensitive enough, is likely to waste a lot of time, or even stuck in one place, and you won’t come and design patterns are also accounted for a large part of the interview process.

Therefore, we should learn design patterns well, whether for future code review, or for your own growth and career development. Nonsense is not more to say, let’s start formal learning together.

Design principles and ideas

All put aside the design principle to speak of the behavior of the design pattern is shameless deception plays with the female feelings of the man play rogue behavior, will only let you enjoy for a while, can never have their own happiness, finally fell into a bottomless hole (cough cough, off topic).

Instead of having a hammer in your hand and looking for nails everywhere, you need to know why you’re using this design pattern, what problems you’re solving, and what application scenarios are there. This is the key, if you clearly understand these design principles, you can even assemble your own design patterns for scenarios.

Without further discussion, we should also have a general understanding of some of the more classic design patterns, such as SOLID, KISS, YAGNI, DRY, LOD, etc. We’ll go through them one by one.

The principle of SOLID

The SOLID principle is not a single principle, but consists of five design principles, which are single responsibility principle, open and close principle, internal substitution principle, interface isolation principle and dependency inversion principle, corresponding to the S, O, L, I and D in SOLID. Let’s look at each of them.

SRP- Single responsibility principle

  • Full name: Single Responsibility Principle
  • Definition: A class or module should have A single responsibility.
  • Understanding: Each class should have a clear definition, not large and complete class design, design small granularity, single-function class.
  • What it does: Improves the cohesion of a class or module by avoiding coupling unrelated code together.

OCP- Open close principle

  • Full name: Open Closed Principle
  • Definition: software entities (modules, classes, functions, etc.) should be open for extension , Software entities (modules, classes, methods, etc.) should be “open for extension, closed for modification”.
  • Description: Adding a new feature should extend the existing code (add modules, classes, methods, etc.), not modify the existing code (modify modules, classes, methods, etc.).
  • What it does: Increases class extensibility.

Lsp-li replacement principle

  • Full name: Liskov Substitution Principle
  • Definition: Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing It.==> Subclass objects can replace superclass objects anywhere in the program, and ensure that the original logical behavior and correctness of the program is not broken.

So the definition is still a little abstract, so let’s do an example

class GetUser { constructor(id) { this.id = id } getInfo() { const params = {id: this.id} //... code here } } class GetVipUser extends GetUser { constructor(id, vipLevel) { super(id) this.id = id this.level = vipLevel } getInfo() { const params = {id: this.id} if (this.level ! = void 0) { params.level = this.level } super.getInfo(params) } } class Demo { getUser(user) { Console. log(user.getInfo())}} const u = new Demo() u.geuser (new GetUser()) u.geuser (new GetVipUser())Copy the code

We can see that GetVipUser is designed to comply with the rule of In-substitution, which can replace the parent class wherever it appears, without breaking the original code’s logical behavior and correctness.

A lot of people look at this and say, well, aren’t you just taking advantage of the polymorphism of the class? Yeah, they look a little bit alike, but they’re totally different things, so let’s tweak the code a little bit.

class GetUser { constructor(id) { this.id = id } getInfo() { const params = {id: this.id} //... code here } } class GetVipUser extends GetUser { constructor(id, vipLevel) { super(id) this.id = id this.level = vipLevel } getInfo() { const params = {id: this.id} if (this.level == void 0) { throw new Error('level should not undefind') } super.getInfo(params) } } class Demo {getUser(user) {console.log(user.getinfo ())}} const u = new Demo() u.geuser (new getUser()) u.geuser (new)  GetVipUser())Copy the code

After the change, we can clearly see that the parent class does not make errors at runtime, but the subclass throws back errors when it does not accept level. The logic of the whole program is different from that of the parent class, so it is not in accordance with the rule of In-substitution.

Just to summarize a little bit. Although polymorphism and li substitution are somewhat similar in terms of definition description and code implementation, they focus on different aspects. Polymorphism is a feature of object-oriented programming and a syntax of object-oriented programming languages. It’s an idea of code implementation. The interior substitution is a design principle, which is used to guide how to design the subclass in the inheritance relationship. The design of the subclass should ensure that when replacing the parent class, the logic of the original program is not changed and the correctness of the original program is not damaged.

The Li substitution principle is a principle that guides how subclasses should be designed in inheritance relationships. To understand the principle of Chinese substitution, the most important thing is to understand the words “design by contract”. The superclass defines the “conventions” (or protocols) of a function, and the subclass can change the internal implementation logic of the function, but not the original “conventions” of the function. The conventions here include: function declaration to implement the function; Conventions for inputs, outputs, and exceptions; Even any special instructions listed in the notes.

Lsp-interface isolation principle

  • Full name: Interface Segregation Principle
  • Clients should not be forced to depend upon interfaces that they do not need. (The “client” can be understood as the caller or consumer of the interface).
  • Description: TyepScript development partners may be more familiar with the Interface, but the simple understanding of the Interface is also relatively one-sided, we say that the Interface can include these three aspects
    • A collection of API interfaces
    • A single API interface or function
    • Interface concepts in OOP

This principle is similar to the single responsibility principle, except that it focuses more on interfaces.

  • If “interface” is understood as a set of interfaces, it can be the interface of a class library, etc. If part of the interface is used only by part of the callers, we need to isolate that part of the interface and give it to that part of the callers without forcing other callers to rely on the part of the interface that is not used.

  • If the “interface” is understood as a single API interface or function, and some callers need only some of the functions in the function, then we need to break the function into more fine-grained functions so that the caller only depends on the fine-grained function it needs.

  • If “interface” is understood as an interface in OOP, it can also be understood as an interface syntax in object-oriented programming languages. The interface design should be as simple as possible, so that the implementation class and the caller of the interface do not rely on interface functions that are not needed.

Dip-dependency inversion principle

  • Dependency Inversion Principle
  • Definition: high-level modules do not depend on low-level modules. High-level and low-level modules should depend on each other through abstraction. In addition, abstractions do not depend on concrete implementation details, which depend on abstractions. The plain language is interface-oriented programming that relies on abstraction rather than concrete
  • Understanding: Programming based on interfaces rather than implementations

KISS principle

There are several versions of the KISS principle in English

  • Keep It Simple and Stupid.
  • Keep It Short and Simple.
  • Keep It Simple and Straightforward.

Try to keep it simple. This is an “all-purpose” design principle that can be applied not only to software development, but also to a wide range of product design and system design, such as refrigerators and washing machines. When you look at the Steve Jobs bricks at the time, you get the impression that he was living up to this principle. So how should we practice this principle in development

  • Try not to implement code using techniques your colleagues may not understand
  • Don’t reinvent the wheel. Be good at using existing toollibraries
  • Don’t over-optimize, don’t over-use a few tricks

The principle of YAGNI

The full English name of the YAGNI principle is: You Ain’t Gonna Need It. You won’t need it. This principle is a cure-all. When used in software development, it means: Don’t design features that are not currently used; Don’t write code that isn’t currently needed. In fact, the core idea of this rule is: Don’t overdesign.

DRY principle

Its English description is: Don’t Repeat Yourself. Don’t repeat yourself. When applied to programming, it can be interpreted as: Don’t write repetitive code. It seems simple, but in fact we unconsciously write a lot of duplicate code in our work, such as

  • Implement logical repetition
  • Functional semantic repetition
  • Code execution repetition

Demeter’s rule

From the name alone, we have no idea what this principle is about. But it also has a more flattering name: The Least Knowledge Principle. Popular is: should not have direct dependencies between classes, do not have dependencies; Try to rely on only the necessary interfaces (” limited knowledge “in the definition) between classes that have dependencies. Demeter’s law is a magic weapon to achieve high cohesion and loose coupling. So what are high cohesion and loose coupling?

High cohesion means that similar functions should be placed in the same class, and unrelated functions should not be placed in the same class. Similar functions are often modified at the same time, in the same class, the changes are more centralized, the code is easy to maintain.

Loose coupling means that in your code, the dependencies between classes are simple and clear. Even if two classes have dependencies, code changes in one class will not or rarely result in code changes in the dependent class.

Don’t have dependencies between classes that shouldn’t have direct dependencies; Try to rely on only the necessary interfaces between classes that have dependencies. Demeter’s rule wants to reduce coupling between classes and make them as independent as possible. Each class should know less about the rest of the system. Once a change occurs, there are fewer classes that need to know about it.

conclusion

Having said that, we should master these principles so that we can understand what ideas and problems the following design pattern paradigm is following. Remind yourself that design patterns are not the point, and that writing high-quality code is the way to go.

Design patterns and paradigms

Design patterns fall into three categories: creative patterns, structural patterns, and behavioral patterns

Creative design patterns

Among the creation patterns, singleton, factory (also divided into simple factory and abstract factory), and prototype patterns are more commonly used than builder patterns.

The singleton pattern

Singleton Design patterns are simple to understand. A class that allows only one object (or instance) to be created is a singleton class. This design pattern is called the singleton design pattern, or singleton for short.

The singleton pattern is also relatively simple to implement, and two implementations are presented below

Class GetSeetingConfig {static instance = null constructor() {console.log('new')} getConfig() {... } static getInstance () { if (this.instance == void 0) { this.instance = new GetSeetingConfig() } return this.instance } } const seeting1 = GetSeetingConfig. GetInstance () const seeting2 = GetSeetingConfig. GetInstance () / / print only one new seeting1 twice Class GetSeetingConfig {constructor() {console.log('new')} getConfig() {... } } GetSeetingConfig.getInstance = (function() { let instance return function() { if (! instance){ instance = new GetSeetingConfig() } return instance } })() const seeting1 = GetSeetingConfig.getInstance() Const seeting2 = GetSeetingConfig. GetInstance () / / twice only print a new seeting1 = = = seeting2 / / trueCopy the code

Advantages:

  • The singleton pattern ensures global uniqueness and reduces named variables
  • The singleton pattern can save memory under certain circumstances, reducing the memory and runtime required for excessive class generation
  • The code is maintained in a single class, achieving high cohesion

Disadvantages:

  • Singletons are unfriendly to OOP feature support

    Singletons do not support abstraction, inheritance, and polymorphism very well

  • Singletons hide dependencies between classes

  • Singletons are not testability friendly to code

  • Singletons do not support constructors that hold arguments

Classic scene:

  • Modal dialog
  • Store in state management libraries (Redux, MOBx, Vuex)

The factory pattern

We take the factory literally, for the consumer, we don’t care about your production process, we care about the final product.

Therefore, in order to make the code logic clearer and more readable, we should be good at packaging the independent function of the code block into a single responsibility class or module, this kind of abstraction based thinking is the origin of the factory pattern.

Factory pattern is divided into simple factory pattern, factory method pattern and abstract factory pattern.

Simple Factory model
Class User {constructor(role, name) {this.name = name; this.role = role } } class Admin { constructor(role, name) { this.name = name; this.role = role } } class SuperAdmin { constructor(role, name) { this.name = name; This. Role = role}} class RoleFactory {static createUser(role) {if (role === 'user') {return new user (role,' user')} else if (role === 'admin') { return new Admin(role, Else if (role === 'superadmin') {return new superadmin (role, }}} const user = roleFactory.createUser ('user')Copy the code

The advantage of a simple factory is that you only need one correct parameter to get the object you need, without knowing the details of its creation. But when the internal logic gets complicated this function can become huge and difficult to maintain.

Factory method pattern

So when a simple factory becomes too complex, we can consider replacing it with a factory approach. The core of the factory approach is to defer the actual creation of the object to subclasses.

class UserFactory { constructor(role, name) { this.name = name; this.role = role; } init() {// We can split the complex code in a simple factory into each concrete class // code here //... return new User(this.role, this.name) } } class AdminFactory { constructor(role, name) { this.name = name; this.role = role; } init() {// We can split the complex code in a simple factory into each concrete class // code here //... return new Admin(this.role, this.name) } } class SuperAdminFactory { constructor(role, name) { this.name = name; this.role = role; } init() {// We can split the complex code in a simple factory into each concrete class // code here //... return new SuperAdmin(this.role, This.name)}} class RoleFactory {static createUser(role) {if (role === 'user') {return new UserFactory(role,' user')} else if (role === 'admin') { return new AdminFactory(role, Else if (role === 'superadmin') {return new SuperAdminFactory(role, }}} const user = roleFactory.createUser ('user')Copy the code

So when should you use the factory method pattern rather than the simple factory pattern?

The reason why a code block is separated into a function or class is that the logic of the code block is too complex, which makes the code clearer, more readable and maintainable. However, if the code block itself is not complex, just a few lines of code, there is no need to break it up into separate functions or classes. Based on this design idea, when the object creation logic is more complex, it is ok to not just a simple new, but to combine other class object, doing all sorts of initialization, we recommend using a factory method pattern, will create logical split into multiple complex factory class, let each factory class is not too complicated. With the simple factory pattern, putting all the creation logic into a single factory class leads to a complex factory class.

Abstract Factory pattern

In simple factories and factory methods, there is only one way to classify classes. In the above example, we divide them according to the roles of users and, but if we need to divide the mobile phone number or email address used by users when registering according to business needs, we also need to divide them. If we use the factory method, we need the above three kinds of factories, each of which is divided into mobile phone number or email address, altogether 9. If we add another classification form, we can find that it is an exponential growth trend. Our factory will blow up one of these days. Abstract factories were born for this very specific scenario. Instead of creating just one type of object, we can have a factory responsible for creating many different types of objects. This effectively reduces the number of factory classes.

Class Factory {createUserParser(){createLoginParser(){createLoginParser(){createLoginParser(){createLoginParser(){createLoginParser(){createLoginParser(){createLoginParser(){createLoginParser(){createLoginParser(){createLoginParser(){ Class UserParser extends Factory {createUserParser(role, name) {return new UserFactory(role, name) name) } createLoginParser(type) { if (type === 'email'){ return new UserEmail() } else if (type === 'phone') { return new UserPhone() } } } class AdminParser extends Factory { createUserParser(role, name) { return new AdminFactory(role, name) } createLoginParser(type) { if (type === 'email'){ return new AdminEmail() } else if (type === 'phone') { return new AdminPhone() } } } class SuperAdminParser extends Factory { createUserParser(role, name) { return new SuperAdminFactory(role, name) } createLoginParser(type) { if (type === 'email'){ return new SuperAdminEmail() } else if (type === 'phone') { return new SuperAdminPhone() } } }Copy the code
conclusion

In addition to the cases just mentioned, if the object creation logic is not complicated, we can create the object directly through New, without using the factory pattern.

Now, let’s take the factory model to the next level of thinking, and it does nothing more than these four things. This is the essential reference for deciding whether to use the factory pattern.

  • Encapsulating changes: Creating logic can change, and after encapsulating it as a factory class, changes to creating logic are transparent to callers.
  • Code reuse: Create code that can be reused after being pulled out into a separate factory class.
  • Isolation complexity: Encapsulates complex creation logic without requiring callers to know how to create objects.
  • Control complexity: Separate the code you create from the function or class to make it simpler and simpler.

Builder model

The pattern of breaking up a complex object into multiple simple objects for construction, separating the complex construction layer from the presentation layer so that the same construction process can create different representations is the Builder pattern.

Let’s take a concrete example. Let’s create a CaKe class that takes name, color, Shape, and sugar. Of course we can. We just need

class Cake {
	constructor(name, color, shape, suger) {
    	this.name = name;
        this.color = color;
        this.shape = shape;
        this.suger = suger;
    }
}

new Cake('cake', 'white', 'circle', '30%')
Copy the code

Cake now has only four configurable items, and only four parameters corresponding to the constructor, which is a small number of parameters. However, if the number of configurable items increases to eight, ten, or more, the constructor argument list becomes very long and the code becomes less readable and easier to use if we continue with the current design. When using constructors, it is easy to get the arguments in the wrong order and pass in the wrong values, resulting in a very insidious bug.

Another way to do this is to add a set method to each attribute, place the required attributes in the constructor, expose the set method to the non-required, and let the user choose to fill in the set method himself or not. (For example, our name and color are mandatory, others need not be filled in)

class Cake { consotructor(name, color) { this.name = name; this.color = color; } validName() { if(this.name == void 0) { console.log('name should not empty') return false } return true } validColor()  { if (this.color == void 0) { console.log('color should not empty') true } return true } setShape(shape) { if (this.validName() && this.validColor()) { this.shape = shape; } } setSugar(sugar) { if (this.validName() && this.validColor()) { this.sugar = sugar; }} / /... }Copy the code

At this point, we are still not using the builder pattern, and we can achieve our design requirements by setting mandatory items through the constructor and optional configuration items through the set() method. But let’s make it harder

  • There are a lot of required configuration items, and if you put them all in a constructor, the constructor will have a long argument list.
  • If there are dependencies between configuration items, for example, we add an iSuger property, which must be set when true, or we add a cakeSize and boxSize, which must always be smaller than boxSize. If we continue with our current design, there will be no place for the dependency or constraint verification logic between these configuration items.
  • And if we want Cake to be immutable, that is, after the object is created, it can’t change its internal property values. To do this, we cannot expose the set() method in the Cake class.

To solve these problems, the Builder pattern comes in handy.

class Cake { constructor(name, color, shape, suger) { this.name = name; this.color = color; this.shape = shape; this.suger = suger; } } class CakeBuilder { valid() { //valid all params... } setName() { this.valid() //... return this; } setColor() { this.valid() //... return this; } setShage() { this.valid() //... return this; } setSuger() { this.valid() //... return this; } build() { const cake = new Cake() cake.shape = this.setShape() cake.suger = this.setSuger() cake.name = this.setName()  cake.color = this.setColor() return cake } } const cake1 = new CakeBuilder() .setName('cake') .setColor('yellow') .setShape('heart').setsugar ('70%').builder() Function diractor(builder) {return builder.setName ('cake').setcolor ('yellow').setShape('heart') .setSugar('70%') .builder() } const cakeBuilder = new CakeBuilder() const cake2 = diractor(cakeBuilder)Copy the code

The member variables in the Cake class are redefined in the Builder class, and you can see that the use of the Builder pattern is appropriate for creating extremely complex objects only. In the real business of the front end, without the creation of such extremely complex objects, objects should be created directly using object literals or factory patterns.

The prototype pattern

If the cost of creating an object is high and there is little difference between objects of the same class (most fields are the same), we can save time by copying (or copying) the existing object (prototype) to create a new object. This method of creating objects based on prototypes is called Prototype Design Pattern (Prototype Pattern for short).

class Person {
  constructor(name) {
    this.name = name
  }
  getName() {
    return this.name
  }
}
class Student extends Person {
  constructor(name) {
    super(name)
  }
  sayHello() {
    console.log(`Hello, My name is ${this.name}`)
  }
}

let student = new Student("xiaoming")
student.sayHello()
Copy the code

The prototype pattern is a common development pattern for front-end programmers. This is because, unlike class-based object-oriented programming languages such as Java and C++, JavaScript is a prototype-based object-oriented programming language. Even JavaScript now introduces the concept of classes, but it’s just syntactic sugar based on prototypes.

Structural design pattern

Structural patterns are divided into 7 categories, among which proxy pattern, decorator pattern, adapter pattern and bridge pattern are used more.

The proxy pattern

The principle and code implementation of Proxy Design Pattern are not difficult to master. It adds functionality to the original (or proxied) class by introducing a proxy class without changing its code. Proxy mode virtual proxy and cache proxy are commonly used in the front end.

Virtual agent

We use virtual proxy to implement lazy loading of an image

class MyImg { static imgNode = document.createElement("img") constructor(selector) { selector.appendChild(this.imgNode);  } setSrc(src) { this.imgNode = src } } const img = new MyImg(document.body) img.setSrc('xxx')Copy the code

Take a look at the code above, which defines a MyImg class, receives a selector, and then creates an IMG tag under the selector and exposes a setSrc method.

If the connection speed is slow and the picture is very large, the placeholder for the tag will initially be blank. So we’re going to use image preloading.

The characteristic of virtual proxies is that both the proxy class and the real class expose the interfaces so that they are insensitive to the caller.

Constructor (selector) {this.img = new Image this.myImg = new MyImg(selector) this.myImg.setSrc(this.src) } setSrc(src) { this.img.src = src this.img.onload = () => { this.myImg.setSrc(src) } } } const img = new ProxyMyImg(document.body) img.setSrc('xxx')Copy the code

ProxyMyImg controls the client’s access to MyImg and adds some additional actions, such as setting the SRC of the IMG node to a local loading image before the image is loaded.

The advantage of this is to decouple the addition of img nodes and Settings from pre-loading. Each class goes to one task, which is consistent with the single responsibility principle. If one day the network speed is fast enough that there is no need for preloading at all, we can simply remove the proxy, which is also in accordance with the open and closed principle.

The caching proxy

The caching proxy can provide temporary caching for some expensive results, and then return the cached results directly on the next operation if the parameters passed are the same as before.

Let’s say we have a function that computes the product

const mult = (... args) => { console.log('multing... ForEach (item => {res*=item}) return item} mult(2,3) //6 mult(2,3,5)//30Copy the code

Add a caching proxy function

const mult = (... args) => { console.log('multing... ') let res = 1 args.forEach(item => { res*=item }) return res } const proxyMult = (() => { const cache = {} return (... args) => { const key = [].join.call(args, ',') if (key in cache) { return cache[args] } return cache[key] = mult.apply(null, Args)}})() proxyMult(1,2,3,4)// multing... 24 proxyMult (1, 2, 3, 4) / / 24Copy the code
Proxy

The Proxy class Proxy is added to ES6

grammar

const p = new Proxy(target, handler)

Target The target object (which can be any type of object, including a native array, a function, or even another Proxy) to be wrapped with a Proxy.

Handler An object, usually with functions as attributes, that define the behavior of agent P when performing various operations.

For more details, see developer.mozilla.org/zh-CN/docs/…

Let’s use Proxy to implement the caching Proxy example

const mult = (args) => { console.log('multing... ') let res = 1 args.forEach(item => { res*=item }) return res } const handler = { cache: {}, apply: function(target, thisArg, args) { const key = [].join.call(args, ',') if(key in this.cache) { return this.cache[key] } return this.cache[key] = target(args) } } const proxyMult = new The Proxy (mult, handler) proxyMult (1, 2, 3, 4) / / multing... / / 24 proxyMult (1, 2, 3, 4) / / 24Copy the code

Decorator pattern

The decorator pattern can dynamically add additional responsibilities to an object without affecting other objects derived from that class.

Constructor (Plan) {this.plan = Plan} fire() {constructor(Plan) {this.plan = Plan} fire() {console.log() {constructor(Plan) {this.plan = Plan} fire() { This.plan.fire () console.log(' Launch missile ')}} const plan = new plan () const newPlan = new PlanDecorator(plan) newplan.fire () // Fire bullets to fire missilesCopy the code

If you’re familiar with TypeScript, its code structure looks like this

interface IA { init: () => void } class A implements IA { public init() { //... }} class ADecorator implements IA {constructor (a: IA) {this.a = a} init() {// implements IA {constructor (a: IA) {this.a = a} init() {// implements IA {constructor (a: IA) {this.a = a} init() {// implements IA {constructor (a: IA) {this.a = a}Copy the code

The well-known AOP is implemented through the decorator pattern

before
Function. The prototype. Before = Function (beforeFn) {const _this = this / / save Function reference return Function () {/ / return contains a Function and a new Function of "proxy Function" Beforefn. apply(this, arguments)// Execute new function, fix this return _this.apply(this, arguments)// Execute original function and return result of original function, this is not hijacked}}Copy the code
after
Function.prototype.after = function(afterFn) {
	const _this = this
    return function() {
    	const res = _this.apply(this, arguments)
        afterFn.apply(this, arguments)
        return res
    }
}
Copy the code
Around (Circular notification)
Function.prototype.around = function(beforeFn, aroundFn) { const _this = this return function () { return _this.before(beforeFn).after(aroundFn).apply(this, Arguments)// Use the previously written before and after to implement around}}Copy the code
test
Const log = (val) => {console.log(' log output ${val} ')} const beforFn = () => {console.log(' output ${new before output Date().getTime()} ')} const afterFn = () => {console.log(' ${new Date().getTime()} ')} const preLog = log.before(beforFn) const lastLog = log.after(afterFn) const aroundLog = log.around(beforeFn, afterFn) preLog(11) lastLog(22) aroundLog(33)Copy the code
When AOP encounters decorators

Support for decorators, which modify or enhance the behavior of a class, has been added to ES7, making it easier to use decorator patterns in JS

Class User {@checklogin getUserInfo() {console.log(' getUserInfo ')}} function checkLogin(target, name, descriptor) { let method = descriptor.value descriptor.value = function (... If (validate(args)) {method.apply(this, args)} else {console.log(' No login, about to go to the login page... ') } } } let user = new User() user.getUserInfo()Copy the code

There are also higher-order components typically found in React

function HOCDecorator(WrappedComponent){ return class HOC extends Component { render(){ const newProps = {param: 'HOC'}; return <div> <WrappedComponent {... this.props} {... newProps}/> </div> } } } @HOCDecorator class OriginComponent extends Component { render(){ return <div>{this.props.param}</div> } }Copy the code

If you are familiar with Mobx, you will notice that all the functions in mobx support decorators, but we can also use decorators in Redux to implement the connect function, which is very convenient. Specific can see Ruan Yifeng teacher’s explanation es6.ruanyifeng.com/#docs/decor…

Adapter mode

The English translation of Adapter Pattern is Adapter Design Pattern. As the name suggests, this pattern is for adaptation. It converts incompatible interfaces into compatible ones, allowing classes that would otherwise not work together due to incompatible interfaces to work together. An oft-cited example of this pattern is that of a USB adapter acting as an adapter that converts two incompatible interfaces into working together.

Class GooleMap {show() {console.log(' console.log ')}} class BaiduMap {display() {console.log(' console.log ')}} class GaodeMap { Show () {console.log(' render map ')}} Class BaiduAdaapterMap {show() {return new BaiduMap().display()}}Copy the code

Therefore, the application scenarios of the adapter mode are as follows

  • Unify the interface design of multiple classes
  • Replace dependent external systems
  • Compatible with older interfaces
  • Adapt to different formats of data

The bridge model

Take a very simple example, there are now two latitude Car (Mercedes, BMW, Audi, etc.) Transmission range type (automatic, manual, manual, etc.) According to the inherited design mode, Car is a base class, assuming that there are M Car brands, N gears we’re going to write M by N classes to describe all the combinations of cars and gears. When we use bridge mode, I first new a specific Car (such as Mercedes), and then new a specific Transmission (such as automatic). So this schema only has M+N classes that can describe all types, so that’s an M by N explosion of inherited classes reduced to an M+N combination.

class Car { constructor(brand) { this.brand = brand } speed() { //... } } class Transmission { constructor(trans) { this.trans = trans } action() { ... } } class AbstractCar { constructor(car, transmission) { this.car = car this.transmission } run () { this.car.speed() this.traansmission.action() //... }}Copy the code

Facade pattern

Facade Pattern, also known as appearance Pattern, is the full English name of Facade Design Pattern. In GoF’s book Design Patterns, the facade pattern is defined as providing a unified set of interfaces for subsystems and defining a set of high-level interfaces to make the subsystems more usable.

Suppose you have A system A that provides interfaces A, B, C, and D. To complete A service function, system B needs to invoke interfaces A, B, and D of system A. Using the facade pattern, we provide a facade interface X wrapped around interface A, B, and D to be used directly by system B.

If it is a background student, it indicates that the interface granularity is too fine. We can ask it to encapsulate the ABD interface into one interface for our use, which will improve part of the performance. However, due to various reasons, the background does not change, we can also encapsulate the ABD interface together, which also makes our code has high cohesion, low coupling characteristics.

Let’s take the simplest example

const myEvent = { // ... stop: e => { e.stopPropagation(); e.preventDefault(); }}Copy the code

Portfolio model

The composition pattern is defined as grouping a group of objects into a tree structure to represent a partial-whole hierarchy. The application of the composite pattern is based on the tree structure of its data.

Suppose we had a need to design a class to represent a directory in a file system that could easily do the following:

  • Dynamically add or remove subdirectories or files from a directory
  • Statistics the number of files in a specified directory
  • Statistics the total size of files in a specified directory
class FileSystemNode { constructor(path) { this.path = path } countNumOfFiles() {} countSizeOfFiles() {} getPath() { return this.path } } class File extends FileSystemNode { constructor(path) { super(path) } countNumOfFiles () { return 1 } countSizeOfFiles() {// use NodejsApi to get file from path... } } class Directory extends FileSystemNode{ constructor(path) { super(path) this.fileList = [] } countNumOfFiles () { / /... } countSizeOfFiles() { //... } addSubNode(fileOrDir) { this.fileList.push(fileOrDir) } removeSubNode(fileOrDir) { return this.fileList.filter(item =>  item ! /** */ Leon */ Leon /aa.txt */ Leon /bb */ Leon /bb/cc.js const root = new Directory('/') const dir_leon = new Directory('/leon/') root.addSubNode(leon) const file_aa = new File('/leon/aa.txt') const dir_bb = new Directory('leon/bb') dir_leon.addSubNode(file_aa) dir_leon.addSubNode(dir_bb) const file_cc = new File('/leon/bb/cc.js')  dir_bb.addSubNode(file_cc)Copy the code

The downside of the composite pattern is that it is possible to accidentally create a large number of objects that degrade performance or are difficult to maintain, so the meta-pattern we’ll talk about is a solution to this problem.

The flyweight pattern

The so-called “enjoy yuan”, as the name implies, is the unit to be shared. The purpose of the share mode is to reuse objects and save memory, provided that the share object is immutable.

The definition of “immutable object” means that its state (the object’s member variables or properties) cannot be changed once it has been initialized through the constructor. Therefore, immutable objects cannot expose any methods such as set() that modify internal state.

Specifically, when there are a large number of duplicate objects in a system, if these repeated objects are immutable objects, we can use the share pattern to design the objects as share elements, keeping only one instance in memory, which can be referenced by multiple codes. This saves memory by reducing the number of objects in memory. In fact, not only the same objects can be designed as shares, but similar objects can also be designed as shares by extracting the same parts (fields) of those objects and having them referenced by a large number of similar objects.

For example, let’s say you want to make a simple rich text editor (just record text and formatting).

class CharacterStyle{ constructor(font, size, color) { this.font = font this.size = size this.color = color } equals(obj) { return this.font === obj.font && this.size  === obj.size && this.color = obj.color } } class CharacterStyleFactory { static styleList = [] getStyle(font, size, color) { const newStyle = new CharacterStyle(font, size, color) for(let i = 0, style; style = this.styleList[i++];) { if (style.equals(newStyle)) { return style } } CharacterStyleFactory.styleList.push(newStyle) return newStyle } } class Character { constructor(c, style) { this.c = c this.style = style } } class Editor { static chars = [] appendCharacter(c, font, size, color) { const style = CharacterStyleFactory.getStyle(font, size, color) const character = new Character(c, style) Editor.chars.push(character) } }Copy the code

If we don’t extract the style, each time we type the text, we call the appendCharacter() method in the Editor class to create a new Character object and store it in the Chars array. If there are tens of thousands, hundreds of thousands, hundreds of thousands of words in a text file, we need to store that many Character objects in memory. Is there a way to save some memory? In practice, there aren’t too many font formats to use in a text file, since it’s unlikely that someone will format every text differently. So, for font format, we can design it as a share element, let different text shared use.

Behavioral design patterns

Observer model

The Observer Design Pattern, also known as the public-subscribe Design Pattern, defines a one-to-many dependency between objects. When an object’s state changes, all dependent objects are automatically notified.

In general, the dependent object is called an Observable and the dependent object is called an Observer. However, in actual project development, the names of these two objects are flexible and there are various names, such as: Subject-observer, publisher-subscriber, producer-consumer, EventEmitter EventListener, dispatcher-listener. Whatever you call it, an application scenario that fits the definition just given can be considered an observer pattern.

Let’s implement a simple but classic implementation. Write out the base class using the idea of a template pattern

Class Subject {registerObserver() {throw new Error(' subclass needs to override parent method ')} class Subject {registerObserver() {throw new Error(' subclass needs to override parent method ')} NotifyObservers () {throw new Error(' observers need to override the superclass ')}} class Observer{update() {throw new Error(' observers need to override the superclass ')}}Copy the code

Then according to the template to specific implementation

class ConcreteSubject extends Subject { static observers = [] registerObserver(observer) { ConcreteSubject.observers.push(observer) } removeObserver(observer) { ConcreteSubject.observers = ConcreteSubject.observers.filter(item => item ! == oberser) } notifyObservers(message) { for(let i = 0,observer; observer = ConcreteSubject.observers[i++];) {observer.update(message)}} class ConcreteObserverOne extends Observer {update(message) { Execute your logic console.log(message) //... }} Class ConcreteObserverTwo extends Observer {update(message) {//TODO get message notification, execute logic console.log(message) //... } } class Demo { constructor() { const subject = new ConcreteSubject() subject.registerObserver(new ConcreteObserverOne()) subject.registerObserver(new ConcreteObserverTwo()) subject.notifyObservers('copy that') } } const demo = new Demo()Copy the code

In fact, the above is just a general principle and thinking, the actual observer mode is much more complicated, for example, you need to consider synchronous blocking or asynchronous non-blocking issues, namespace issues, and must register before publishing? Moreover, using a large number of observer patterns in a project can lead to increased coupling and reduced cohesion, making the project difficult to maintain.

Template pattern

The template method pattern defines an algorithm skeleton in a method and postpones some steps to subclasses. The template method pattern lets subclasses redefine certain steps in an algorithm without changing the overall structure of the algorithm.

The “algorithm” here can be understood as “business logic” in a broad sense, and does not specifically refer to the “algorithm” in data structure and algorithm. The algorithm skeleton here is the “template”, and the method that contains the algorithm skeleton is the “template method”, which is how the template method pattern gets its name.

For example, we want to make a coffee machine program, can help us to make various flavors of coffee through the program Settings, its process is roughly like this

  • Add espresso
  • The sugar
  • With milk
  • ice
  • Add water

There may be some that you do not need to add, so we can write a hook to control each variable, for example, we can add a hook to add ice or not

Class Tea {addCoffee() {console.log(' addCoffee ')} addSuger() {throw new Error(' subclass needs to override method of parent ')} addMilk() {throw new Error(' subclass overwrites parent method ')} addIce() {console.log(' addIce ')} isIce() {return false // default no ice} addWater() {console.log(' addWater ') } init() { this.addCoffee() this.addSuger() this.addMilk() if (this.isIce()) { this.addIce() } } }Copy the code

That’s the template. We’re gonna make a latte

Class Latte extends Tea {addSuger() {console.log(' console.log ')} addMilk() {console.log(' console.log ')} isIce() {return true}} const ice_latte = new Latte() ice_latte.init()Copy the code

As you can see, we not only encapsulate the algorithm framework of the subclass in the parent class, but also implement some methods that do not change in the parent class so that the subclass can inherit directly. In fact, when WE talked about composition patterns, we also used template method patterns, so you can go back and look at them.

And this is what we talked about when we talked about design principles: interface based programming, not implementation based programming, you can think of it as abstract based programming, not implementation based programming. So before development, to do a good design, will let us get twice the result with half the effort.

Js to write a template or a diu diu problems, for example, we throw an error in some way in the parent class method can realize but not beautiful, if you are familiar with Ts, it would be very well done, you can go to write an abstract class, subclass to realize this abstract class, or interface (interface), parent and child classes are based on interface programming, If you’re interested, you can do it yourself.

The strategy pattern

The full name of Strategy Pattern is Strategy Design Pattern. Define a family of algorithm classes and encapsulate each algorithm individually so that they can be replaced with each other. Policy patterns can make changes to algorithms independent of the clients that use them (by client I mean the code that uses the algorithm).

As we know, the factory pattern decouples the creation and use of objects, and the observer pattern decouples the observer and observed. The policy pattern is similar in that it decouples the definition, creation, and use of the policy.

Definition of policy
// Since all policy classes implement the same interface, AlgorithmInterface () {}} Class ConcreteStrategyA extends Strategy {algorithmInterface() {// Concrete implementation //... }} Class ConcreteStrategyB extends Strategy {algorithmInterface() { }} / /...Copy the code
Policy creation

Because the policy pattern contains a set of policies, when using them, the type is used to determine which policy to create and use. To encapsulate the creation logic, we need to mask the creation details from the client code. We can take the logic for creating policies based on Type and put it in a factory class.

class StrategyFactory {
      strategies = new Map()
      constructor () {
      	this.strategies.set("A", new ConcreteStrategyA())
        this.strategies.set("B", new ConcreteStrategyB())
        //...
      }
      getStrategy(type) {
      	  return type && this.strategies.get(type)
      }
      
}
Copy the code

In actual project development, this pattern is often used. It is most commonly used to avoid lengthy if-else or switch branch judgments. But it does more than that. It can also provide extension points for the framework, and so on, as the template pattern does.

Chain of Responsibility model

The English translation Of Chain Of Responsibility Design Pattern. Decouple the sending and receiving of a request so that multiple receiving objects have the opportunity to process the request. These receive objects are strung together in a chain and the request is passed along the chain until one of the receiving objects on the chain can handle it.

In layman’s terms, in the chain of responsibility pattern, multiple processors (the “receiving object” in the definition) process the same request in turn. A request is processed by processor A, which then passes to processor B, which then passes to processor C, and so on, forming A chain. Each processor in the chain has its own processing responsibility, so it is called the responsibility chain pattern.

There are two ways to implement the responsibility chain. Let’s first look at the one that is easy to understand. The HandlerChain class uses an array to hold all the handlers and calls the handle() function of each handler in turn in the Handle () function of the HandlerChain.

Class IHandler {handle() {throw new Error(' subclass needs to override this method ')}} Class HandlerA extends IHandler {handle() {let handled = false //... return handled } } class HandlerB extends IHandler { handle() { let handled = false //... return handled } } class HandlerChain { handles = [] addHandle(handle) { this.handles.push(handle) } handle() { this.handles.for(let i= 0, handler; handler = this.handles[i++];) { handled = handler.handle() if (handle) { break } } } } const chain = new HandlerChain() chain.addHandler(new HandlerA()) chain.addHandler(new HandlerB()) chain.handle()Copy the code

The second method uses the implementation of linked lists

class Handler { successor = null setSuccessor(successor) { this.successor = successor } handle() { const isHandle = this.doHandle() if (! isHandle && !! Forerunner) {this.forego.handle}} doHandle() {throw new Error(' subclasses need to overwrite this method ')}} class HandlerA extends Handler  { doHandle() { let handle = false //... return handle } } class HandlerB extends Handler { doHandle() { let handle = false //... return handle } } class HandlerChain { head = null tail = null addHandler(handler) { handler.setSuccessor(null) if (head  === null) { head = handler tail = handler return } tail.setSuccessor(handler) tail = handler } handle() { if (!! head) { head.handle() } } } const chain = new HandlerChain() chain.addHandler(new HandlerA()) chain.addHandler(new HandlerB()) chain.handle()Copy the code

Remember when we talked about AOP in the Decorator pattern, we can tweak it a little bit and also turn it into a chain of responsibility approach.

Function.prototype.after = function(afterFn) {
	let self = this
    return function() {
    	let res = self.apply(this, arguments)
        if (res === false) {
        	afterFn.apply(this, arguments)
        }
        return ret
    }
}

const res = fn.after(fn1).after(fn2).after(fn3)
Copy the code

As you can see, the afterFn function we passed in, if it returns false, will continue through the chain until the last one, and once it returns true, the request will not be passed forward.

Iterator pattern

Iterator pattern, also known as cursor pattern. It is used to traverse the collection object. When we say “collection objects”, we can also call “containers” and “aggregate objects”, they are actually objects containing a group of objects, such as arrays, linked lists, trees, graphs, and jump tables.

A complete iterator pattern usually involves two parts: container and container iterator. To achieve interface-based programming rather than implementing programming, containers contain container interfaces and container implementation classes, and iterators contain iterator interfaces and iterator implementation classes. The iterator() method needs to be defined in the container to create iterators. The iterator interface needs to define hasNext(), currentItem(), and next(). Container objects are passed into the iterator class through dependency injection.

class ArrayIterator { constructor( arrayList) { this.cursor = 0 this.arrayList = arrayList } hasNext() { return this.cursor ! == this.arrayList.length } next() { this.cursor++ } currentItem() { if(this.cursor > this.arrayList.length) { throw new Error('no such ele') } return this.arrayList[this.cursor] } }Copy the code

In the above code implementation, we need to pass the container object to be iterated through to the iterator class via the constructor. In fact, to encapsulate the details of iterator creation, we can define an iterator() method in the container that creates the corresponding iterator. To enable interface-based programming rather than implementation programming, we also need to define this method in the ArrayList interface. Specific code implementation and use examples are as follows:

class ArrayList {
	constructor(arrayList) {
    	this.arrayList = arrayList
    }
	iterator() {
    	return new ArrayIterator(this.arrayList)
    }
}

const names = ['lee', 'leon','qing','quene']
const arr = new ArrayList(names)
const iterator = arr.iterator()

iterator.hasNext()
iterator.currentItem()
iterator.next()
iterator.currentItem()

Copy the code

Above we only implemented array iterators. For object iterators, the principle is similar. You can implement the following on your own.

Iterators have three advantages over for loop traversal:

  • The iterator pattern encapsulates the complex data structure inside the collection. The developer does not need to know how to traverse, but can directly use the iterators provided by the container.
  • The iterator pattern separates the iterator operation from the collection class, making the responsibility of the two more single.
  • The iterator pattern makes it easier to add new traversal algorithms and is more open and closed. In addition, because iterators are implemented from the same interface, it becomes easier to replace iterators in development based on interface rather than implementation programming.

The state pattern

State patterns are generally used to implement state machines, which are commonly used in game, workflow engine and other system development. However, there are many ways to implement the state machine. In addition to the state mode, branch logic method and table lookup method are commonly used.

Have you played super Mario Games? In the game, Mario can take many forms, such as Small Mario, Super Mario, Fire Mario, Cape Mario, and so on. In different scenarios, each form will convert to another, and increase or decrease points accordingly. For example, starting as little Mario, eating mushrooms turns you into Super Mario and adds 100 points.

In effect, Mario’s form transition is a state machine. Mario’s various forms are the “states” in the state machine, the game’s plot (eating mushrooms, for example) is the “events” in the state machine, and the points added or subtracted are the “actions” in the state machine. Eating mushrooms, for example, triggers a state shift: from Minor Mario to Super Mario, and triggers the execution of the action (plus 100 points).

For the sake of the rest of the presentation, I’ve simplified the background, keeping only a few states and events. The state transfer after simplification is shown in the figure below:

class MarioStateMachine { constructor() { this.score = 0 this.currentState = new SmallMario(this) } obtainMushRoom() { this.currentState.obtainMushRoom() } obtainCape() { this.currentState.obtainCape() } obtainFireFlower() { this.currentState.obtainFireFlower() } meetMonster() { this.currentState.meetMonster() } getScore() { return this.score } getCurrentState() { return this.currentState } setScore(score) { this.score = score } setCurrentState(currentState) { this.currentState = currentState } } class Mario { getName() {} obtainMushRoom() {} obtainCape(){} obtainFireFlower(){} meetMonster(){} } class SmallMario extends Mario { constructor(stateMachine) { super() this.stateMachine = stateMachine } obtainMushRoom() { this.stateMachine.setCurrentState(new SuperMario(this.stateMachine)) this.stateMachine.setScore(this.stateMachine.getScore() + 100) } obtainCape() { this.stateMachine.setCurrentState(new CapeMario(this.stateMachine)) this.stateMachine.setScore(this.stateMachine.getScore() + 200) } obtainFireFlower() { this.stateMachine.setCurrentState(new FireMario(this.stateMachine)) this.stateMachine.setScore(this.stateMachine.getScore() + 300) } meetMonster() { // do something } } class SuperMario extends Mario { constructor(stateMachine) { super() this.stateMachine = stateMachine } obtainMushRoom() { // do nothing... } obtainCape() { this.stateMachine.setCurrentState(new CapeMario(this.stateMachine)) this.stateMachine.setScore(this.stateMachine.getScore() + 200) } obtainFireFlower() { this.stateMachine.setCurrentState(new FireMario(this.stateMachine)) this.stateMachine.setScore(this.stateMachine.getScore() + 300) } meetMonster() { this.stateMachine.setCurrentState(new SmallMario(this.stateMachine)) this.stateMachine.setScore(this.stateMachine.getScore() - 100) } } //CapeMario // Use const Mario = new MarioStateMachine() mario.obtainmushroom () mario.getScore()Copy the code

MarioStateMachine has a bidirectional dependency with each state class. It is natural for MarioStateMachine to depend on individual state classes, but, conversely, why should individual state classes depend on MarioStateMachine? This is because each status class needs to update two variables in MarioStateMachine, Score and currentState.

There are also some problems with the code implementation of the state pattern. For example, all event functions are defined in the state interface, resulting in the implementation of all event functions even if a state class does not need to support any or all of the events. Not only that, add an event to the status interface, and all the status classes need to be modified accordingly.

The mediation patterns

A mediation pattern defines a single (mediation) object that encapsulates the interaction between a set of objects. Delegate the interaction between this set of objects to the mediation object to avoid direct interaction between objects.

In fact, the mediation pattern is designed much like the middle layer, which transforms the interactions (or dependencies) between a set of objects from many-to-many (mesh) to one-to-many (star) by introducing the mediation. An object used to interact with N objects, but now only needs to interact with one mediation object, which minimizes the interaction between objects, reduces the complexity of the code, and improves the readability and maintainability of the code.

When it comes to the intermediary model, there is a classic example that I have to say, that is air traffic control.

In order for planes to fly without interfering with each other, each plane needs to know the position of the other planes at all times, which requires constant communication with the other planes. The communications networks formed by aircraft communications would be infinitely complex. At this time, we introduce the “tower” such an intermediary, so that each aircraft can only communicate with the tower and send its position to the tower, which is responsible for the route scheduling of each aircraft. This greatly simplifies the communication network.

Here we use code to implement the following:

class A {
    constructor() {
        this.number = 0
    }
    setNumber(num, m) {
        this.number = num
        if (m) {
            m.setB()
        }
    }
}
class B {
    constructor() {
        this.number = 0
    }
    setNumber(num, m) {
        this.number = num
        if (m) {
            m.setA()
        }
    }
}
class Mediator {
    constructor(a, b) {
        this.a = a
        this.b = b
    }
    setA() {
        let number = this.b.number
        this.a.setNumber(number * 10)
    }
    setB() {
        let number = this.a.number
        this.b.setNumber(number / 10)
    }
}

let a = new A()
let b = new B()
let m = new Mediator(a, b)
a.setNumber(10, m)
console.log(a.number, b.number)
b.setNumber(10, m)
console.log(a.number, b.number)


Copy the code

Visitor pattern

Allows one or more operations to be applied to a group of objects, decoupling the operations from the objects themselves. The visitor pattern is a classic design patterns in 23 in one of the hardest to understand design patterns, because it is hard to understand, hard to realize, application of it can lead to the readability and maintainability of a code, so that the visitor pattern is rarely used in the actual software development, in the absence of special need, I suggest you do not use the visitor pattern.

And the visitor pattern requires function overloading, which is a thankless task to implement in JS. In order to ensure the integrity of this article, we use Java to implement it, you can have a look at it.

public interface Visitor { void visit(Engine engine); void visit(Body body); void visit(Car car); } public class PrintCar implements Visitor { public void visit(Engine engine) { System.out.println("Visiting engine"); } public void visit(Body body) { System.out.println("Visiting body"); } public void visit(Car car) { System.out.println("Visiting car"); } } public class CheckCar implements Visitor { public void visit(Engine engine) { System.out.println("Check engine"); } public void visit(Body body) { System.out.println("Check body"); } public void visit(Car car) { System.out.println("Check car"); } } public interface Visitable { void accept(Visitor visitor); } public class Body implements Visitable { @Override public void accept(Visitor visitor) { visitor.visit(this); } } public class Engine implements Visitable { @Override public void accept(Visitor visitor) { visitor.visit(this); } } public class Car { private List<Visitable> visit = new ArrayList<>(); public void addVisit(Visitable visitable) { visit.add(visitable); } public void show(Visitor visitor) { for (Visitable visitable: visit) { visitable.accept(visitor); } } } public class Client { static public void main(String[] args) { Car car = new Car(); car.addVisit(new Body()); car.addVisit(new Engine()); Visitor print = new PrintCar(); car.show(print); }}Copy the code

In general, the visitor pattern targets a set of objects of different types. However, although the types of these objects are different, they inherit the same parent class or implement the same interface. In different application scenarios, we need a series of the subject matter of this set of related business operations (extract text, compression, etc.), but in order to avoid keep adding functions, leading to swelling, single job less and less, and avoid frequent add functions, leading to frequent code changes, we use the visitor pattern, decoupling the objects and operation, Isolate these business operations and define them in a separate, segmented visitor class.

Memo mode

To capture the internal state of an object and save the state outside of the object so that the object can be restored to its previous state later, without violating the encapsulation principle.

This model is not difficult to understand and master, the code is more flexible, the application scenario is also more clear and limited, mainly used to prevent loss, undo, recovery and so on.

Constructor (content){this.content = content} getContent(){return this.content}} // Memento{constructor(content){this.content = content CareTaker { constructor(){ this.list = [] } add(memento){ this.list.push(memento) } get(index){ return this.list[index] Constructor (){this.content = null} setContent(content){this.content = content} getContent(){ return this.content } saveContentToMemento(){ return new Memento(this.content) } GetContentFromMemento (memento){this.content = memento.getContent()}} let editor = new editor () let careTaker =  new CareTaker() editor.setContent('111') editor.setContent('222') careTaker.add(editor.saveContentToMemento()) editor.setContent('333') careTaker.add(editor.saveContentToMemento()) editor.setContent('444') console.log(editor.getContent()) //444 editor.getContentFromMemento(careTaker.get(1)) console.log(editor.getContent()) //333 editor.getContentFromMemento(careTaker.get(0)) console.log(editor.getContent()) //222Copy the code

Memo mode is also called snapshot mode. Specifically, it captures the internal state of an object and saves the state outside the object without violating the encapsulation principle, so that the object can be restored to its previous state later. The definition of this schema expresses two parts: one is to store copies for later recovery; The other part is to back up and restore objects without violating the encapsulation principle.

The application scenarios of the memo mode are also clear and limited. It is mainly used to prevent loss, cancellation, and recovery. It’s very similar to what we call a “backup.” The main difference between the two is that the memo pattern focuses more on code design and implementation, while backup focuses more on architectural or product design.

Command mode

The command pattern encapsulates a request (command) as an object, which allows you to parameterize other objects with different requests (injecting different request dependencies into other objects), and supports queueing, logging, undo, and other (additional control) functions for requests (commands).

// Class Receiver {execute() {console.log(' Receiver performs the request ')}} // Class Command {constructor(Receiver) { This.receiver = receiver} execute () {console.log(' command '); This.receiver. Execute () {constructor(command) {this.receiver = command} invoke() {this.receiver. Console. log(' start ') this.man.execute ()}} // developer const developer = new Receiver(); // sales office const order = new Command(developer); // const client = new Invoker(order); client.invoke()Copy the code

In some object-oriented languages, functions cannot be passed as arguments to other objects, nor can they be assigned to variables. With command mode, functions can be wrapped as objects, so that functions can be used as objects. But in JS functions as parameters are passed is very simple things, so the above code we can also directly use the function to achieve.

Interpreter mode

The interpreter pattern defines a syntactic (or grammatical) representation of a language and defines an interpreter to handle the syntax. For example, if we want to implement a calculation of addition, subtraction, multiplication and division, these four operation symbols can also be used if we simply use if-else to determine the operator, but if we want to implement a scientific calculation later, we will need more if-else judgment, which will be a bit bloated

class Context { constructor() { this._list = []; This. _sum = 0; } get sum() {return this._sum} set sum(newValue) {this._sum = newValue} add(expression) {this._sum = newValue} add(expression) { this._list.push(expression) } get list() { return this._list } } class PlusExpression { interpret(context) { if (! (context instanceof Context)) { throw new Error("TypeError") } context.sum = ++context.sum } } class MinusExpression { interpret(context) { if (! (context instanceof Context)) { throw new Error("TypeError") } context.sum = --context.sum; } } class MultiplicationExpression { interpret(context) { if (! (context instanceof Context)) { throw new Error("TypeError") } context.sum *= context.sum } } class DivisionExpression {  interpret(context) { if (! (context instanceof Context)) { throw new Error("TypeError") } context.sum /= context.sum } } // MultiplicationExpression and DivisionExpression omit /** * const context = new context (); / / in turn add: addition | | addition subtraction expression context. The add (new PlusExpression ()); context.add(new PlusExpression()); context.add(new MinusExpression()); context.add(new MultiplicationExpression()); context.add(new MultiplicationExpression()); context.add(new DivisionExpression()); / / in sequence: addition | | addition subtraction expression context. The list. The forEach (expression = > expression. Interpret (context)); console.log(context.sum);Copy the code

Interpreter mode code implementation is flexible, there is no fixed template. As we said earlier, the application design pattern deals primarily with code complexity, and the interpreter pattern is no exception. The core idea of its code implementation is to break parsing into smaller classes to avoid large, all-parsed classes. A common practice is to break the grammar rules into small, independent units, then parse each unit, and finally merge the parsing of the whole grammar rule.

conclusion

Every design pattern should be composed of two parts: the first part is the application scenario, which is what kind of problem the pattern can solve; The second part is the solution, that is, the design ideas and concrete code implementation of this pattern. However, the code implementation is not a mandatory part of the pattern. If you focus solely on the solution, or even the code implementation, you get the illusion that most of the patterns look similar.

The principle and implementation of most design patterns are very simple. The hard part is to understand the application scenarios and figure out what problems to solve.

Applying design patterns is just a means. The ultimate goal, the original goal, is to improve the quality of your code. Specifically, improve code readability, extensibility, maintainability, etc. All the design is done around this original intention.

So, when you’re designing code, it’s important to ask yourself why you’re designing it this way, why you’re applying this design pattern, whether it actually improves the quality of your code, and in what ways it improves the quality of your code. If it is difficult to explain clearly, or if the reasons given are farfetched and not overwhelming, then you can basically conclude that this is a kind of over-design, design for design’s sake.

In fact, design principles and ideas are the mind, and design patterns are only moves. Master the mind, to the same should be all changes, there is no recruit to win. Therefore, design principles and ideas are more universal and important than design patterns. By mastering design principles and ideas, we can better understand why we use a design pattern, better apply the design pattern, and even create new design patterns ourselves.