Definition of Design Patterns: Simple and Elegant Solutions for Specific Problems in Object-Oriented Software Design
Design patterns are some of the solutions that predecessors summed up to solve a specific scenario. Maybe it's normal to feel that the design pattern is not so well understood when you first get into contact with programming without much experience. There are some simple design patterns that we sometimes use, but we don't realize that they exist.
Learning design patterns can help us to provide more and faster solutions when dealing with problems.
Of course, the application of design patterns will not start in a moment and a half. In many cases, the business logic we write does not use design patterns or does not need specific design patterns.
Adapter mode
This design pattern, which we often use, is also one of the simplest design patterns. The advantage is that it can keep the data structure of the original interface unchanged.
The Adapter Pattern serves as a bridge between two incompatible interfaces.
Example
The adapter pattern is well understood, assuming that we define an interface data structure with the back end as (can be understood as an old interface):
[ { "label": "Option 1", "value": 0 }, { "label": "Option 2", "value": 1 } ]
But for other reasons behind the back end, you need to define the structure of the return (which can be understood as the new interface):
[ { "label": "Option 1", "text": 0 }, { "label": "Option 2", "text": 1 } ]
Then we use the front end to the back end interface in several ways, so I can adapt the new interface field structure to the old interface, without modifying the field everywhere, as long as the source data is well adapted.
Of course, the above is a very simple scenario, which is often used. Maybe you would think that back-end processing is not better, it's certainly better, but that's not what we're talking about.
Singleton pattern
Singleton mode, literally, is also very easy to understand, that is, instantiations will only have one instance many times.
Some scenarios can be instantiated once to achieve caching effect and reduce memory footprint. There are also scenarios where you have to instantiate only once, otherwise instantiations will override previous instances many times, resulting in bug s (which are relatively rare scenarios).
Example
One way to realize the bullet-box is to create the bullet-box and hide it. This will waste some unnecessary DOM overhead. We can create the bullet-box when we need it. At the same time, we can realize only one instance in combination with the singleton mode, thus saving some DOM overhead. The following is part of the login box code:
const createLoginLayer = function() { const div = document.createElement('div') div.innerHTML = 'Log in to the Floating Box' div.style.display = 'none' document.body.appendChild(div) return div }
Decoupling the singleton pattern and creating the bomb code
const getSingle = function(fn) { const result return function() { return result || result = fn.apply(this, arguments) } } const createSingleLoginLayer = getSingle(createLoginLayer) document.getElementById('loginBtn').onclick = function() { createSingleLoginLayer() }
proxy pattern
Definition of proxy pattern: Provide a substitute or placeholder for an object to control its access.
Proxy object has all the functions of ontology object, but it can also have functions outside. Moreover, the proxy object and the ontology object have the same interface and are user-friendly.
Virtual Agent
The following code uses proxy mode to realize image preloading. It can be seen that the proxy mode skillfully separates the created image from the preloading logic, and in the future, if there is no need for preloading, just change to request ontology instead of request proxy object.
const myImage = (function() { const imgNode = document.createElement('img') document.body.appendChild(imgNode) return { setSrc: function(src) { imgNode.src = src } } })() const proxyImage = (function() { const img = new Image() img.onload = function() { // http images will not be executed until they have been loaded myImage.setSrc(this.src) } return { setSrc: function(src) { myImage.setSrc('loading.jpg') // Local loading pictures img.src = src } } })() proxyImage.setSrc('http://loaded.jpg')
Cache proxy
Adding the result caching function to the original function belongs to the caching proxy.
Originally, there was a function of reverseString, so without changing the existing logic of reverseString, we can optimize performance by using caching proxy mode, and of course we can handle other logic when the value changes, such as the use of Vue computed.
function reverseString(str) { return str .split('') .reverse() .join('') } const reverseStringProxy = (function() { const cached = {} return function(str) { if (cached[str]) { return cached[str] } cached[str] = reverseString(str) return cached[str] } })()
Subscription Publishing Mode
Subscription publishing makes the front-end commonly used data communication methods, asynchronous logical processing and so on, such as React setState and Redux is the subscription publishing mode.
But the subscription publishing mode should be used reasonably, otherwise it will cause data confusion. The one-way data flow idea of redux can avoid the problem of data flow confusion.
Example
class Event { constructor() { // All eventType listener callback functions (arrays) this.listeners = {} } /** * Subscription events * @param {String} eventType Event type * @param {Function} listener Publish action-triggered callback function after subscription, with parameters of published data */ on(eventType, listener) { if (!this.listeners[eventType]) { this.listeners[eventType] = [] } this.listeners[eventType].push(listener) } /** * Publishing events * @param {String} eventType Event type * @param {Any} data Contents published */ emit(eventType, data) { const callbacks = this.listeners[eventType] if (callbacks) { callbacks.forEach((c) => { c(data) }) } } } const event = new Event() event.on('open', (data) => { console.log(data) }) event.emit('open', { open: true })
Observer model
The observer pattern defines a one-to-many dependency that allows multiple observer objects to simultaneously monitor a target object. When the state of the target object changes, all observer objects are notified so that they can be updated automatically.
The data driver of Vue uses the observer mode, and mbox also uses the observer mode.
Example
Imitate Vue data-driven rendering mode (just similar, simple imitation).
Firstly, setter and getter are used to monitor data changes:
const obj = { data: { description: '' }, } Object.defineProperty(obj, 'description', { get() { return this.data.description }, set(val) { this.data.description = val }, })
Then add goals and observers.
class Subject { constructor() { this.observers = [] } add(observer) { this.observers.push(observer) } notify(data) { this.observers.forEach((observer) => observer.update(data)) } } class Observer { constructor(callback) { this.callback = callback } update(data) { this.callback && this.callback(data) } } // Create Observer ob1 let ob1 = new Observer((text) => { document.querySelector('#dom-one').innerHTML(text) }) // Create Observer ob2 let ob2 = new Observer((text) => { document.querySelector('#dom-two').innerHTML(text) }) // Create target sub let sub = new Subject() // Target sub adds observer ob1 (target and observer establish dependencies) sub.add(ob1) // Target sub adds observer ob2 sub.add(ob2) // Target sub trigger event (target actively notifies observer) sub.notify('It's changed here.')
That's how it comes together.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover" /> <title></title> </head> <body> <div id="app"> <div id="dom-one"> //The original value </div> <br /> <div id="dom-two"> //The original value </div> <br /> <button id="btn">change</button> </div> <script> class Subject { constructor() { this.observers = [] } add(observer) { this.observers.push(observer) } notify() { this.observers.forEach((observer) => observer.update()) } } class Observer { constructor(callback) { this.callback = callback } update() { this.callback && this.callback() } } const obj = { data: { description: '' }, } // Create Observer ob1 const ob1 = new Observer(() => { console.log(document.querySelector('#dom-one')) document.querySelector('#dom-one').innerHTML = obj.description }) // Create Observer ob2 const ob2 = new Observer(() => { document.querySelector('#dom-two').innerHTML = obj.description }) // Create target sub const sub = new Subject() // Target sub adds observer ob1 (target and observer establish dependencies) sub.add(ob1) // Target sub adds observer ob2 sub.add(ob2) Object.defineProperty(obj, 'description', { get() { return this.data.description }, set(val) { this.data.description = val // Target sub trigger event (target actively notifies observer) sub.notify() }, }) btn.onclick = () => { obj.description = 'Change' } </script> </body> </html>
Decorator Model
Decorator Pattern allows new functions to be added to an existing object without changing its structure.
The decorator grammar proposal for ES6/7 is the decorator pattern.
Example
class A { getContent() { return 'The first line' } render() { document.body.innerHTML = this.getContent() } } function decoratorOne(cla) { const prevGetContent = cla.prototype.getContent cla.prototype.getContent = function() { return ` //What precedes the first line <br/> ${prevGetContent()} ` } return cla } function decoratorTwo(cla) { const prevGetContent = cla.prototype.getContent cla.prototype.getContent = function() { return ` ${prevGetContent()} <br/> //The second line ` } return cla } const B = decoratorOne(A) const C = decoratorTwo(B) new C().render()
Strategic model
In Strategy Pattern, an action or its algorithm can be changed at run time.
Assuming that our performance is divided into four levels: A, B, C and D, the rewards of the four levels are different. In general, our code is implemented as follows:
/** * Winning the Year-end Award * @param {String} performanceType Types of performance, * @return {Object} Year-end awards, including bonuses and prizes */ function getYearEndBonus(performanceType) { const yearEndBonus = { // bonus bonus: '', // Prize prize: '', } switch (performanceType) { case 'A': { yearEndBonus = { bonus: 50000, prize: 'mac pro', } break } case 'B': { yearEndBonus = { bonus: 40000, prize: 'mac air', } break } case 'C': { yearEndBonus = { bonus: 20000, prize: 'iphone xr', } break } case 'D': { yearEndBonus = { bonus: 5000, prize: 'ipad mini', } break } } return yearEndBonus }
The use of policy patterns can be as follows:
/** * Winning the Year-end Award * @param {String} strategyFn Performance Strategy Function * @return {Object} Year-end awards, including bonuses and prizes */ function getYearEndBonus(strategyFn) { if (!strategyFn) { return {} } return strategyFn() } const bonusStrategy = { A() { return { bonus: 50000, prize: 'mac pro', } }, B() { return { bonus: 40000, prize: 'mac air', } }, C() { return { bonus: 20000, prize: 'iphone xr', } }, D() { return { bonus: 10000, prize: 'ipad mini', } }, } const performanceLevel = 'A' getYearEndBonus(bonusStrategy[performanceLevel])
Here, each function is a strategy. Modifying one of the strategies does not affect the other strategies, and can be used independently. Of course, this is just a simple example, just for illustration.
The obvious feature of the policy pattern is that it can reduce if statements or switch statements.
Responsibility chain model
As the name implies, the Chain of Responsibility Pattern creates a chain of recipient objects for requests. This pattern gives the type of request and decouples the sender and receiver of the request. This type of design pattern belongs to behavioral pattern.
In this mode, usually each receiver contains a reference to another receiver. If an object cannot process the request, it passes the same request to the next recipient, and so on.
Example
function order(options) { return { next: (callback) => callback(options), } } function order500(options) { const { orderType, pay } = options if (orderType === 1 && pay === true) { console.log('500 Yuan Deposit Pre-purchase, Get 100 yuan coupon') return { next: () => {}, } } else { return { next: (callback) => callback(options), } } } function order200(options) { const { orderType, pay } = options if (orderType === 2 && pay === true) { console.log('200 Yuan Deposit Pre-purchase, Get 50 yuan coupon') return { next: () => {}, } } else { return { next: (callback) => callback(options), } } } function orderCommon(options) { const { orderType, stock } = options if (orderType === 3 && stock > 0) { console.log('Ordinary Purchase, No coupons') return {} } else { console.log('Insufficient stock, Unable to buy') } } order({ orderType: 3, pay: true, stock: 500, }) .next(order500) .next(order200) .next(orderCommon) // Print out "Ordinary Purchase, No Coupon"
The above code decouples order-related, order 500, order 200, order Common and so on can be invoked separately.