preface
I recently read JavaScript Design Patterns and Development Practices, which opened my eyes a lot, so I wrote this article to summarize the common patterns.
The singleton pattern
The singleton pattern is a very common pattern. The singleton pattern is defined as ensuring that a class has only one instance and providing a global access point to access it.
Application scenarios
Singletons are used for unique popovers, global loadbars, binding callbacks, and so on. The following are common uses that save instances with closures to reduce pollution to the global environment.
function singleObj() {
this.name = 'hello'
}
const getSingleObj = (function single() {
let instance = null
return function() {
if(! instance) { instance =new singleObj()
}
return instance
}
})()
Copy the code
Inert singleton
The lazy singleton example is actually the above example, meaning that it is created at the actual first reference, rather than at the beginning. This has the advantage of reducing page load time.
Generic lazy singleton
Around the principle of separating invariable and variable logic, the act of creation can be detached.
const getSingle = function(fn) {
let result = null
return function() {
return result || (result = fn.apply(this.arguments))}}Copy the code
The strategy pattern
The policy pattern is also a very common pattern. The definition of a policy pattern is to encapsulate each algorithm independently and isolate the immutable parts as the subject of the design pattern, separating the method of use from the method of implementation.
Policy pattern in JS
Because JS is a classless language, so directly write the set of algorithms as objects.
Application scenarios
For example, when calculating the salary of each position, the algorithm corresponding to each position is independent and different. In this case, we can use the strategy pattern to optimize our code. (Instead of writing a bunch of ifs and else to write all the code together)
const calculateBonusStrategies = {
'S': function( salary ){
return salary * 4
},
'A': function( salary ){
return salary * 3}}function calculateBonus(level, salary) {
return calculateBonusStrategies[level](salary)
}
Copy the code
Once this writing method is used, when we add new types of employees in the future, we can increase our code without modifying the source code. We only need to add algorithm codes corresponding to new types of employees in calculateBonusStrategies.
The policy pattern in the form
Using the idea of strategy pattern, we can realize form judgment simply
const validatorStrategies = {
isNonEmpty: function(value, errorMsg) {
if (value === ' ') return errorMsg
},
minLength: function(value, length, errorMsg) {
if (value.length > length) return errorMsg
},
isMobile: function(value, errorMsg) {
if (!/ (a ^ 1 [3 | | 5 8] [0-9] {9} $) /.test(value)) {
return errorMsg
}
}
}
const Validator = function() {
this.cache = []
}
Validator.prototype.add = function(dom, rules) {
const self = this
const triggerObj = {}
for (let i = 0, len = rules.length; i < len; i++) {
(function(rule) {
const { strategy, errorMsg, trigger } = rule
if(! triggerObj[trigger]) { triggerObj[trigger] = [] }const strategyArray = strategy.split(':')
const type = strategyArray.shift()
strategyArray.push(errorMsg)
strategyArray.unshift(dom.value)
function validator() {
strategyArray[0] = dom.value
return validatorStrategies[type].apply(dom, strategyArray)
}
triggerObj[trigger].push(validator)
self.cache.push(validator)
})(rules[i])
}
for (const key in triggerObj) {
const funcArray = triggerObj[key]
dom[`on${key.toLocaleLowerCase()}`] = function() {
for (let i = 0, len = funcArray.length; i < len; i++) {
const errorMsg = funcArray[i]()
if (errorMsg) {
alert(errorMsg)
return
}
}
}
}
}
Validator.prototype.start = function() {
for (let index = 0, len = this.cache.length; index < len; index++) {
const errorMsg = this.cache[index]()
if (errorMsg) {
return errorMsg
}
}
}
/* * uses */
const form = document.getElementById('form')
const validateFunc = function() {
const validator = new Validator()
validator.add(form.userName, [
{ strategy: 'isNonEmpty'.errorMsg: 'Cannot be empty'.trigger: 'blur' },
{ strategy: 'isNonEmpty'.errorMsg: 'Cannot be empty'.trigger: 'change' }
])
validator.add(form.password, [
{ strategy: 'minLength:6'.errorMsg: 'Password must not be less than 6'.trigger: 'change'}])const errorMsg = validator.start()
return errorMsg
}
Copy the code
Js in another way of the strategy pattern
Above we used the object approach to implement the policy pattern. But in JS, because functions can also be assigned to transfer, so we may abstract the algorithm into a function, and then through the way of higher-order functions, the function as a variable in the implementation
var S = function(salary){
return salary * 4
}
var A = function(salary){
return salary * 3;
}
var B = function(salary){
return salary * 2
}
var calculateBonus = function(func, salary){
return func( salary )
}
calculateBonus(S, 10000)
Copy the code
The proxy pattern
The proxy pattern provides a proxy or placeholder for an object to control access to it. The advantage of the proxy pattern is that the proxy of the object is used instead of the object itself, and the client does not have to worry about internal changes when calling the object.
Application scenarios
Here is an example of loading images virtually.
const imageLoad = (function() {
const img = document.createElement('img')
document.body.appendChild(img)
return {
setImage: function(src) {
img.src = src
}
}
})()
const proxyImageLoad = (function() {
const image = new Image()
image.onload = function() {
imageLoad.setImage(this.src)
}
return {
setImage: function(src) {
imageLoad.setImage('loading.gif')
image.src = src
}
}
})()
proxyImageLoad.setImage('hello.jpg')
Copy the code
For the customer, proxyImageLoad controls the customer’s access to imageLoad and adds some additional operations (chrysanthemum diagram). The point of the agency model is to allow us to better adhere to the single responsibility principle. Split the two functions, load chrysanthemum graph and load image.
Iterator pattern
The iterator pattern is something we use a lot, which is essentially the various iterator functions implemented by Array.
Iterators are divided into outer iterators and inner iterators.
Inner iterator
An inner iterator means that the iterator does not expose the iteration process, but the iteration rules are already implemented inside the iterator. Common examples are Array each and so on.
External iterator
An external iterator is an iterator that exposes the iteration mode to the user, allowing the user to explicitly request the iteration of the next element. The following is a simple external iterator.
const Iterator = function(obj) {
let current = 0
const next = function() {
current++
}
const isDone = function() {
return current >= obj.length
}
const getCurrItem = function() {
return obj[current]
}
return {
next,
isDone,
getCurrItem
}
}
Copy the code
Observer model
The observer pattern is a one-to-many dependency. When an object changes state, all objects that depend on that object are notified of the change. The most common example of this pattern is DOM event binding.
Implementation steps
To implement the Observer pattern, follow these steps
- Start by defining the publisher
- Add a cache list to the publisher object to hold the subscriber object
- Add a publish message function to the observed. When running this function, the observed iterates through the cache list and calls the stored observer object again and again.
const event = {
notSend: {},
clientList: {},
listen: function(key, fn) {
if (!this.clientList[key]) {
this.clientList[key] = []
}
const notSend = this.notSend[key]
if (notSend) {
fn.apply(this, notSend)
}
this.clientList[key].push(fn)
},
trigger: function() {
const key = Array.prototype.shift.call(arguments)
const fns = this.clientList[key] || []
const len = fns.length
if (len === 0) {
if (!this.notSend[key]) {
this.notSend[key] = []
}
this.notSend[key] = arguments
return false
}
for (let i = 0, len = fns.length; i < len; i++) {
const fn = fns[i]
fn.apply(this.arguments)}},remove: function(key, fn) {
const fns = this.clientList[key]
if(! fns) {return false
}
if(! fn) { fns && (fns.length =0)}else {
const len = fns.length
for (let i = 0; i < len; i++) {
const _fn = fns[i]
if (_fn === fn) {
fns.splice(i, 1)}}}}}Copy the code
To solve the problem of messages being sent before the subscription, a notSend object is added to the observer schema to store data for messages that are not sent successfully. Wait until you subscribe to send it.
Publish and subscribe
The publish-subscribe model is very similar to the observer model, but there are significant differences. In the subscription model, the publisher and subscriber are strangers to each other. They communicate with each other through a middleman, a broker. In other words, the subscription model is not loosely coupled, but completely decoupled. In the Observer mode, a set of observers should be maintained inside the observed, that is, the Observer and the observed are in fact loosely coupled. This shows that, in fact, the two are completely different.
Command mode
Command mode refers to a command to do something specific. Unlike strategic models, where the goals are often the same, they are just different means to the same end. However, the command mode does not care about the selection of policies and faces a wider range of problem domains. At the same time, the command mode also has undo and queuing functions. In practice, command mode differs from policy mode in that a command object acts as an intermediate layer between the client and the algorithm to reference the algorithm.
Application scenarios
Sometimes you need to send a request to some object, but you don’t know who the recipient of the request is or what the requested operation is, so you want to design the software in a loosely coupled way so that the request sender and the request receiver can decouple from each other.
Portfolio model
Combinatorial mode is a mode that disassembles a subject into multiple self. The composite mode performs all operations on the children of the subject through a single operation on the subject. Note that the composition pattern is not A parent-child relationship, but A ** has-A (aggregate) ** relationship.
The actual use
Personal understanding, in fact, in JS when we iterate dom tree or some tree structure data, if you need to do the same operation (for sub-gymnastics consistent interface), we can use the combination mode to develop.
Performance optimization
When we modify or delete a child node from the parent node, we often need to traverse the tree again to find the parent node. If you record the object of the current parent in the child node, you don’t need to traverse the entire tree again. The parent property in the neutron element of the DOM tree is the object of the parent node.
Template pattern
The template method pattern is a very simple pattern that can be implemented using inheritance alone. It is divided into two parts: the first part is abstract superclass; The second part is a subclass of the concrete implementation. The advantage of using the template pattern is that you can separate out the same logic and reduce duplicate code. However, JS does not really have inheritance, so if implemented in JS, template patterns can be implemented as higher-order functions.
Heng yuan mode
Henyuan pattern is a performance optimization pattern. If objects can be reused, try not to create new objects. The primitive pattern requires that the attributes of an object be divided into internal state and external state. The following four guidelines can be used to separate external and internal states.
- Internal state is stored inside an object.
- Internal state can be shared by several objects.
- The internal state is independent of the specific scenario and usually does not change.
- The external state depends on and changes according to the specific scenario. The external state cannot be shared.
Application scenarios
- A large number of similar objects are used in a program
- Because of the large number of objects used, memory overhead is high
- Most of the state of an object can be changed to an external state
- You can replace a large number of objects by stripping out shared objects.
File Uploading Example
const Upload = function(uploadType) {
this.uploadType = uploadType
}
Upload.prototype.delFile = function(id) {
UploadManager.setExternalState(id, this)
if (this.fileSize < 3000) {
delete UploadManager.uploadDatabase[id]
return this.dom.parentNode.removeChild(this.dom)
}
}
const UploadFactory = (function() {
const createUploadObj = {}
return {
create: function(uploadType) {
if (createUploadObj[uploadType]) {
return createUploadObj[uploadType]
}
return createUploadObj[uploadType] = new Upload(uploadType)
}
}
})()
const UploadManager = (function() {
const uploadDatabase = {}
return {
add: function(id, uploadType, fileName, fileSize) {
const upload = UploadFactory.create(uploadType)
const dom = document.createElement('div')
dom. innerHTML = '<span> file name:${fileName}, file size${fileSize}</span> <button class="del-file"> Delete </button> '
dom.querySelector('.del-file').onclick = function() {
upload.delFile(id)
}
uploadDatabase[id] = {
fileName: fileName,
fileSize: fileSize,
dom: dom
}
return uploadDatabase[id]
},
setExternalState: function(id, obj) {
const uploadData = uploadDatabase[id]
for(const key in uploadData) {
obj[key] = uploadData[key]
}
}
}
})()
Copy the code
Henyuan schema with no internal state
A hengyuan pattern without internal state is a singleton pattern.
Hengyuan schema with no external state
The hengyuan pattern with no external state actually turns the place where external objects are stored into an object pool.
Object pooling
The object pool maintains a pool of free objects, and when objects are needed, instead of new, they are fetched from the object pool. If there are no free objects in the object pool, a new object is created, and when the acquired object has done its job, it is put into the pool to wait for the next acquisition.
Generic object pool
const objectPoolFactory = function(fn) {
const objPool = []
return {
create: function() {
const obj = objPool.length === 0 ?
fn.apply(this.arguments) : objPool.shift()
return obj
},
recover: function(obj) {
objPool.push(obj)
}
}
}
Copy the code
Chain of Responsibility model
The chain of responsibility pattern avoids coupling between the sender and receiver of the request by giving multiple objects the opportunity to process the request, linking the objects into a chain and passing the request along the chain until one object processes it. Unlike the strategy pattern, in which the algorithms are independent of each other, in the chain of responsibility pattern, the algorithms are passed down layers. The advantage of using the responsibility chain mode is that you can break up a bunch of if else statements into separate node steps, but the disadvantage is that you may lose performance because the responsibility chain is too long.
The practical application
The Interceptors in AXIos are implemented using the chain of responsibility pattern.
The mediation patterns
The mediation mode is to add a mediation object to the communication of many-to-many relationship objects, and change it into one-to-many relationship mode, so that each object can be decoupled. The mediation pattern is a realization of the least knowledge principle (Demeter’s law). When the code is highly coupled and grows exponentially as the project changes, consider using the mediator pattern for decoupling.
Decorator pattern
Decorator pattern is a pattern that dynamically adds responsibilities to objects at run time. This approach is more flexible than inheritance.
Decorator pattern in js
Using AOP, you can easily add decorators to the target function without affecting the original function.
Function.prototype.before = function(beforeFn) {
const self = this
return function() {
if (beforeFn.apply(this.arguments) = = =false) {
return
}
return self.apply(this.arguments)}}Function.prototype.after = function(afterFn) {
const self = this
return function() {
const ret = self.apply(this.arguments)
afterFn.apply(this.arguments)
return ret
}
}
Copy the code
The decorator pattern is similar to the proxy pattern in that it is an indirect reference to an object. However, the design intention and purpose of the two modes are different. The function of the decorator mode is to dynamically add new functions to the object ontology, while the agent mode is to add some more intelligent functions on the basis of the object’s function target remains unchanged. The proxy pattern usually has only one layer of proxy — references to the ontology, whereas the decorator pattern often forms a long chain of decorators
The practical application
Let’s look at an example login.
const form = document.getElementById('form')
const { username, password } = form
const btn = document.getElementById('submit')
const validate = function() {
if (username.value === ' ' || password === ' ') {
return false}}const handleSubmit = function() {
const param = {
username: username.value,
password: password.value
}
ajax('http://xxx.xxxx.xxx/login', param)
}.before(validate)
btn.onclick = function() {
handleSubmit()
}
Copy the code
The decorator pattern allows for a good separation of the logic of validation and submission.
The state pattern
State mode: Allows an object to change its behavior when its internal state changes. The object appears to modify its class. From this definition, we can understand: encapsulate each state of an object into independent classes with uniform behavior and different functions, and delegate these independent classes to state objects for switching and changing. Consider the following example of a light bulb that switches on and off.
const OffLightState = function(light) {
this.light = light
}
OffLightState.prototype.pressed = function() {
console.log('off')
this.light.setState(this.light.onLightState)
}
OnLightState.prototype.pressed = function() {
console.log('turn on the light')
this.light.setState(this.light.offLightState)
}
function Light() {
this.offLightState = new OffLightState(this)
this.onLightState = new OnLightState(this)
}
Light.prototype.init = function() {
this.currState = this.offLightState
}
Light.prototype.setState = function(state) {
this.currState = state
}
Light.prototype.pressed = function() {
this.currState.pressed()
}
Copy the code
The advantages and disadvantages
Advantages:
- The state pattern defines the relationship between states and behaviors and encapsulates them in a class. Adding new states does not affect the logic code of other states.
- Too many conditional branches are removed.
- The request action in the Context and the behavior encapsulated in the state class can easily change independently of each other.
Disadvantages:
- Because there will be a lot of state classes, you will add a lot of objects.
- Because the logic is scattered, you can’t see the entire logic of the state machine in one place.
The state mode is similar to the policy mode, but the difference is that each algorithm in the policy mode is independent, whereas in the state mode, algorithm switching is state-dependent and not freely available.
Adapter mode
The adapter pattern is a pattern that solves the problem of incompatible interfaces between two software entities. It’s a “never too late” model because no one will use it in the first place. The adaptor pattern is generally used when the old interface is not suitable for the new service and the old interface is difficult to maintain.
Usage scenarios
For example, data packets in XML format were used for data communication in the past. As business and technology change, JSON type data will be used. Therefore, it is good to add a layer of XML-to-JSON adapter to the old interface for output XML results without changing the business code. Or if there are many places to fetch data, but each data format is different, then adapters should be used to unify the data structure and store it.
conclusion
Each pattern has its pros and cons, but the essence of each pattern is that it allows us to better decouple and optimize our code to avoid ancestral crap.
reference
- JavaScript Design Patterns and Development Practices