Shared goals

  • For beginners or those who have not studied before – can get started

  • Notice some of the design patterns being used in daily development

  • Those of you who have studied theory, but don’t know how to apply it in practice – can you master some of the techniques

  • There’s no guarantee that you’ll be able to write code after listening to it. After all, a design pattern is an idea that needs to be combined with a specific business scenario.

  • The master of all ways -🍉 happy to eat melon

  • Consider the old and learn the new

advertising

It is highly recommended that you look at what has been shared before: starting with factory patterns, a brief talk about the application of design patterns in the front end

It describes the application of factory mode, singleton mode, policy mode, observer mode/subscription-publish mode in the front end, very detailed and examples are also close to daily development

What are design patterns?

Official explanation: A design pattern is a reusable solution to common problems encountered in software design.

To put it bluntly, a design pattern is a routine. In the process of software design and development, we can apply specific design patterns for specific problems and scenarios to better solve problems, just like recipes for cooking and game guides.

The core idea of the design pattern is to encapsulate change

The background of the emergence of design patterns is the increasing complexity of software design, which is mostly caused by change.

An 🌰

Write a service, initial version V1.0. After a few decades, it is v1.0 with or without iteration and optimization. Then the business can be written almost anywhere and the main goal is to achieve functionality, to some extent without thinking about maintainability, scalability.

But, in actual development, it can be said that there is no project that does not change. After all, our daily routine is: new requirements, version iteration, upgrade and optimization

What we can do is to minimize the impact of this change as we develop — look at the change and the unchange in your overall logic, and then separate the change and the unchange, making sure that the change is flexible and the unchange is stable.

This process is called “encapsulation change”; The resulting code is what we call “robust” code that can withstand changes. The point of design patterns is to help us write code like this.

Twenty years ago a magic book, Design Patterns, came out of nowhere. The 23 most classic design patterns presented in the book

Mama ah! That many? Don’t panic, everybody. Some design patterns can only be familiar

Back to encapsulating change, whether creative, structural, or behavioral, these specific design patterns encapsulate different types of change in their own way

The creation pattern encapsulates the changes that occur during object creation

The structural pattern encapsulates the changes of the combination of objects and aims to flexibly express the coordination and dependence between objects

Behavioral patterns decouple the ever-changing behavior of objects, making it safer and easier to change behavior.

Creation: Constructor pattern, factory pattern

Constructor pattern

Let’s start with the constructor pattern, which we use almost every day.

Function User(name, age, career){this.name = name this.age = age this.career = career} const User = New User(' ', 18, 'Front End Engineer ')Copy the code

Constructors such as User that are used to initialize newly created objects are called constructors. In JS, we use constructors to initialize objects, using the constructor pattern.

Who changes and who doesn’t during the creation of a user?

  • Constant: Every user has a name, an age, a job, and so on

  • Variation: name, age, type of work these values, this is the user’s personality

The process of assigning constructor attributes to objects is encapsulated to ensure that each object has these attributes and the commonness remains unchanged. Meanwhile, the value operations of name, age and career are open to ensure the flexibility of personality.

Simple Factory model

If you want to implement different jobs with different responsibilities, add personalized fields to the users of each job to describe what they do. It would be unreasonable to write multiple constructors at this point

Function Coder(name, age) {this.name = name this.age = age this.career = 'Coder' this.work = [' Coder ',' requirement check ', } function ProductManager(name, Age) {this.name = name this.age = age this.career = 'product manager' this.work = [' c ', 'c ',' c ']}Copy the code

In creating a Coder and a ProductManager, who changes and who doesn’t?

  • Invariable: Each user still has attributes such as name, age, job type, and responsibility

  • Variation: The value of each attribute is different, and the work field needs to change with the value of the CAREER field

Encapsulate the same object assignment logic in the User class, separating the changed parts into factory functions.

function User(name , age, career, Work) {this.name = name this.age = age this.career = career this.work = work Factory(name, age, career) { let work switch(career) { case 'coder': Work = [' write code ',' requirement review ',' fix Bug'] break case 'product manager': work = [' book conference room ',' write PRD', 'push '] break case 'boss': Return new User(name, age, career, work)} const User = new Factory(' work ', 18, 'coder') //user. work = [' write code ',' review requirements ',' fix bugs ']Copy the code

Benefits: You don’t have to write countless constructors, you don’t have to worry about assigning different constructors to different jobs, you just care about passing parameters.

Again, understand that the creative pattern encapsulates the changes that occur during object creation

For the factory function, during the creation of a User

  • Unchanged: Each user still has attributes such as name, age, job type, and responsibility, as well as responsibilities for each job type

  • Variation: name, age, job, etc

The factory model is like a processing plant, passing in different parameters (raw materials) to obtain specific objects (products)

Structural: Decorator mode, adapter mode, proxy mode

Decorator mode

The function of decorator mode is to add functions to the object without changing the original object, so that the original object can meet the user’s more complex needs by packaging and expanding it

An 🌰

There’s a button you can click to jump to. Now you need to add click to report data

Const btnClick = function() {this.jump = function() {console.log(' page jump '); }; };Copy the code

longhand

Const btnClick = function() {this.jump = function() {console.log(' page jump '); }; This.log = function() {console.log(' enable log reporting '); }; };Copy the code

Decorator pattern practices

Const btnClick = function() {this.jump = function() {console.log(' page jump '); }; }; // const Decorator = function(old) {this.jump = old.jump; This.log = function() {console.log(' enable log reporting '); }; this.newClick = function() { this.jump(); this.log(); }; }; const oldBtnClick = new btnClick(); const decorator = new Decorator(oldBtnClick); //decorator.newClick();Copy the code

Adapter mode

Converting a class’s interface to the interface we want can help us solve incompatibilities

The computer only supports Type-c port, so adapters are needed to connect our monitor, mouse, etc. A converter is an adapter

Application scenarios

  • The data format returned by the back end is not the same as that used for page rendering

    [{” day “, “Monday”, “uv” : 6300}, {” day “:” Tuesday “, “uv” : 7100}, {” day “:” on Wednesday “, “uv” : 4300}, {” day “:” on Thursday, “” uv” : 3300}, {” day “:” Friday “, “uv” : 8300}, {” day “:” Saturday “, “uv” : 9300}, {” day “:” Sunday “, “uv” : 11300}]

    Echarts graphs need data format: [” Tuesday “, “Tuesday “,” Wednesday “, “Thursday “,” Friday “, “Saturday “,” Sunday “] // X-axis data

    [6300.7100, 4300, 3300, 8300, 9300, 11300] // Data of coordinate points

    Function echartXAxisAdapter(res) {return res.map(item => item.day); }

    Function echartDataAdapter(res) {return res.map(item => item.uv); }

Create two functions to format the data according to the required data format respectively to solve the problem; These two methods are essentially an adapter that throws in the specified data to output the expected data format according to the specified rules.

  • The adapter mode source guide is also used in AXIos -> core logic

    function getDefaultAdapter() { var adapter; If (typeof XMLHttpRequest! == ‘undefined’) {// For Browsers, question the XHR-based adapter adapter = require(‘./ Adapters/XHR ‘); } else if (typeof process ! == ‘undefined’ && Object.prototype.toString.call(process) === ‘[object process]’) { // For node use HTTP adapter For node, call the node specific HTTP adapter adapter = require(‘./ Adapters/HTTP ‘); } return adapter; }

    // HTTP adapter: module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolvePromise, RejectPromise) {// Specific logic}} // XHR adapter: module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, Reject) {// Specific logic}}

Adapter implementation logic: adapter source code

The input parameter config is the same as the output parameter Promise. All the details about HTTP module and XHR implementation are encapsulated by Adapter into its own complex underlying logic, exposed are very simple unified things — unified interface, unified input, unified output, unified rules

Good adapters keep variation to themselves and uniformity to the user. One thing to be clear about, however, is that adapters are not silver bullets, and the cumbersome code is always there, but you’ve got it all together so it doesn’t bother you while you’re writing the specific business.

The proxy pattern

The proxy mode is used to control access to objects. In some cases, due to various considerations/limitations, we do not manipulate the original object directly, but rather delegate it to a broker. Requests are preprocessed or forwarded to the real object by the broker.

There are three common types of proxy: protected proxy and virtual proxy, and cache proxy.

Protection agency

Help principals filter out requests

🌰 : Click to register for the event. Instead of calling the method to sign up for an activity directly, delegate to the agent, who decides whether to sign up

Virtual agent

Delay the creation of expensive objects until they are really needed.

Realize image preloading

Var imageNode = (function(){var imageNode = document.createElement('img'); document.body.appendChild(imageNode); return { setSrc: function(src){ imageNoode.src = src; }}}) (); Var proxySetImage = (function(){var img = new Image(); Img. onload = function(){myimage.setsrc (this.src); } return { setSrc: function(src){ myImage.setSrc('loading.gif'); Img. SRC = SRC; }}}) (); proxySetImage('xxxxx');Copy the code

The caching proxy

It can provide temporary storage for some expensive operation results. In the next operation, if the parameters passed in are the same as before, it can directly return the previously stored operation results.

var createProxyFactory = function(fn){ var cache = {}; / / cache the return function () {var args = Array. Prototype. Join the call (the arguments, ', '); If (args in cache){return cache[args]; } return cache[args] = fn.apply(this, arguments); }} var proxyA = createProxyFactory(A); ProxyA (1, 2, 3); ProxyA (1, 2, 3); // Fetch the result in the cacheCopy the code

Behavioral: Chain of responsibility mode, observer mode

Blame chain model

The chain of responsibility pattern is a chain structure in which requests are passed from node to node in the chain until an object can handle the request. If there are no objects to handle the request, the request is dropped from the chain.

Application scenario: After we apply for the device, we need to select the delivery address and the responsible person, and the last one must be successful before we can execute the next ~

longhand

Function applyDevice(data) {// Handle... let devices = {}; let nextData = Object.assign({}, data, devices); // execute selectAddress(nextData); } function selectAddress(data) {// handle... let address = {}; let nextData = Object.assign({}, data, address); // Select the responsible person selectChecker(nextData); } function selectChecker(data) {// handle... let checker = {}; let nextData = Object.assign({}, data, checker); // There is more}Copy the code

Ctrl C + Ctrl V, that doesn’t feel good either

Responsibility chain pattern implementation

Const applyDevice = function() {if(XXX){// Specific logic}else{// If a node can't handle the request, A specific string // nextsucceeded is returned to indicate that the request needs to continue to pass return "nextsucceeded "; }} const selectAddress = function() {} const selectChecker = function() {} const selectChecker = function() { function(fn) { this.fn = fn; this.successor = null; SetNext = function(antecedent) {return (antecedent = antecedent); }; This.run = function() {var ret = this.fn.apply(this, arguments); if (ret === 'nextSuccessor') { return this.successor && this.successor.passRequest.apply(this.successor, arguments); } return ret; }; }; Const chainApplyDevice = new Chain(applyDevice); const chainSelectAddress = new Chain(selectAddress); const chainSelectChecker = new Chain(selectChecker); Chainapplydevice.setnext (chainSelectAddress).setNext(chainSelectChecker); // Finally pass the request to the first node: chainapplyDevice.run ();Copy the code

advantages

  1. The relation of each node is decoupled, and the processing functions of each node do not affect each other. Before, you put B in A and C in B, but this is different, you can write nothing in B.

  2. Node objects in the chain can be flexibly split and reorganized to define the order of execution. The new addition process is also flexible

    const applyLincense = function() {} const chainApplyLincense = new Chain(applyLincense);

    / / using the chain of responsibility pattern implementation functions above chainApplyLincense. SetNext (chainSelectChecker); chainApplyLincense.run();

disadvantages

  1. There is no guarantee that a request will be processed by a node in the chain, so a guaranteed receiver node needs to be added at the end of the chain to handle requests that are leaving the chain;

  2. Perhaps in a certain request transfer process, most nodes do not play a substantial role, its role is just to let the request transfer, from the performance aspect, we should avoid the performance loss caused by too long responsibility chain;

Observer model

The observer pattern defines a one-to-many dependency that allows multiple observer objects to listen to a target object at the same time. When the state of the target object changes, all observer objects are notified so that they can update automatically

An 🌰

The product manager put together a book group

  1. On Monday, a student of front-end development was pulled into a flying book group – “XXX Requirements Development”, and there were two students of back-end and testing in the group

  2. Everyone looked at the names of the group and began to work, but found no demand. The product says, everyone busy, demand after the service

  3. Everyone is ready to develop new requirements, waiting for the product call

  4. On Wednesday, the product roared to the “XXX requirements Development” group: “Requirements documentation!” “Followed by a” Requirements document “link and @everyone. The technical students in the group immediately opened the group to check the group news and group documents, and then entered their own development state according to the demand information provided by the group news and group documents.

This process is a typical observer model, also known as publish-subscribe

There is only one publisher of the requirements document (target object) – the product manager

There are multiple recipients of demand information — front-end, back-end and test students. They need to carry out their follow-up work according to the demand information, so they have to pay close attention to the group message reminder of the group. They are subscribers, namely observers.

In the above flying book group, one requirement information object corresponds to multiple observers (technical students). When the state of the requirement information object changes (from nothing to nothing), the product manager notifies all the students in the group so that they can receive the information and carry out work:

Role division -> state changes -> publisher notifies subscribers, this is the “trope” of observer mode.

The code to understand

The publisher class

Need to pull (add subscribers), kick (remove subscribers) @ all (notify subscribers)

// Define a Publisher class: Publisher {constructor() {this.observers = [] console.log('Publisher created')} // Add subscribers Add (observer) {console.log(' publishe.add invoked') this. Observers. Push (observer)} // Remove subscriber remove(observer) { console.log('Publisher.remove invoked') this.observers.forEach((item, i) => { if (item === observer) { this.observers.splice(i, 1)}})} // Notify all subscribers notify() {console.log(' publishe.notify invoked') this. Observers. ForEach ((observer) => { observer.update(this) }) } }Copy the code

The subscriber class

Be told to execute (be called, need to define methods)

Class Observer {constructor() {console.log('Observer created')} update() {console.log(' observer.update ' invoked') } }Copy the code

Publishing class for requirements Document (PRD)

Class PrdPublisher extends Publisher {constructor() {super() // Initialize the requirements document this.prdState = null // I haven't pulled the group yet, This.observers = [] console.log('PrdPublisher created')} // This method is used to get the current prdState getState() { Console. log(' prdpubler.getState ') return this.prdState} // This method is used to change the value of prdState setState(state) { Console. log(' prdpublisher.setState invoked') // PRD value changed this.prdState = State // Requirement document change, notify all developers this.notify()}} immediatelyCopy the code

As subscribers, the developer’s task becomes concrete: receive the requirements document and get to work:

Class DeveloperObserver extends Observer {constructor() {super() // The requirements document doesn't exist at first, This.prdstate = {} console.log('DeveloperObserver created')} // Override a specific update method update(publisher) { Console.log (' developerobserver.update invoked') // Updates the requirement document this.prdState = publicer.getState () // calls the working function this.work()} Const PRD = this.prdstate // Start moving bricks based on the information provided by the requirements document... . console.log('996 begins... ')}}Copy the code

New a PrdPublisher object (product manager) that can update the requirements document by calling the setState method. Each update to the requirements document is followed by a call to notify all developers

Const liLei = new DeveloperObserver() const A = new DeveloperObserver() const B = new DeveloperObserver() Const hanMeiMei = new PrdPublisher() const PRD = { Add (A) hanmeimei.add (B) hanmeimei.setState (PRD) // The product sends the requirements document and @everyoneCopy the code

Observer mode ≠ publish-subscribe mode

The observer pattern: The product pulls the group, giving the document to each group member, and the publisher directly reaches the subscribers

Publish-subscribe mode: the product does not join the group, but uploads the document to Feishu platform. Feishu platform perceives the change of the file and automatically notifies every developer who subscribed to the file. The publisher does not directly touch the subscriber, but completes the actual communication by a unified third party

If you have yu, how can you have liang? Why do you need a publish-subscribe model when you have an observer model?

The observer mode, which addresses the coupling problem between modules, enables data communication even between two separate, unrelated modules. But merely reducing coupling does not completely solve the coupling problem

The observed needs to maintain a list of observers

The observer must implement a unified method for the observer to call

Publish-subscribe, on the other hand, is a quick fix — the publisher is not aware of the subscriber at all, does not care how it implements the callback method, and is only responsible for notifying changes. Events are registered and triggered on third-party platforms (event centers) that are independent of both parties. Complete decoupling is achieved in publish-subscribe mode.

This does not mean that the publish-subscribe model is “superior” to the observer model. In practical development, our module decoupling appeal does not always require complete decoupling. If the relationship between the two modules is stable and necessary, then the observer pattern is sufficient. We tend to use publish-subscribe when there is a lot of independence between modules and there is no need to create dependencies between the two purely for data communication.

thinking

  1. Do you really need to learn design patterns, as if you can write code without knowing anything about it?
    1. Many of the best frameworks and libraries use design patterns to some extent. If you do not understand, look at the source will be confused, not to the point
    2. Design patterns are a hurdle that must be overcome in the process of technological improvement. The more complex the business logic, the more you need to think about whether you can apply a design pattern to avoid the temptation to jump right in. After writing found coupling strong logic chaos, bug thieves. Not to mention post-maintenance and development iterations
  2. There are many design patterns, and most of them are abstract. How to learn them and how to use them
    1. Essence is a set of solutions to different problems. There are commonalities and correlations between patterns, so we should learn to understand by analogy
    2. You don’t have to learn everything. Not every kind of life to memorize so, more important is to master the idea
    3. For common design patterns, summarize what problems they solve, the scenarios they use, and classic cases. Let the problem map to the design pattern (solution) in your mind. Like math problems, when you see a problem, you can think of which design mode you can use. If you are familiar with it, you can write directly. If you are not familiar with it, you can turn over a book in Chrome
  3. Avoid designing patterns for the sake of designing patterns
    1. Applying design patterns to appropriate problems can improve code quality and enhance readability.
    2. Design patterns are just good solutions to problems that people have learned through practice. Don’t be superstitious, don’t force inappropriate scenarios, increase code complexity and learning costs.