javascript Design Pattern

Posted by hwttdz on Mon, 07 Mar 2022 21:55:47 +0100

The content comes from javascript Design Patterns and development practice

1, Singleton mode

Definition: ensure that a class has only one instance and provide a global access point to access it.
For example, when we click the login button, a login floating window will appear in the page, and this login floating window is unique. No matter how many times we click the login button, this floating window will only be created once, so this login floating window is suitable for creating in singleton mode.

Implementation: a variable is used to mark whether an object has been created for a class. If so, the previously created object will be returned directly the next time the instance of the class is obtained.

const SingleTon = function(name) {
  this.name = name
}

SingleTon.prototype.getName = function() {
  console.log(this.name)
}

const instance = null
SingleTon.getInstance = function(name) {
  if(!this.instance) {
    this.instance = new SingleTon(name)
  }
  return this.instance
}

const a = SingleTon.getInstance('a')
const b = SingleTon.getInstance('b')
console.log(a===b) // true
console.log(a) // SingTon { name: 'a' }
console.log(b) // SingTon { name: 'a' }

General inert single example:

const getSingle = function(fn) {
  const result
  return function () {
    return result || (result = fn.apply(this, arguments))
  }
}
const bindEvent = getSingle(function() {
  document.getElementById('div1').addEventListener = function() {
    alert('click')
  }
  return true
})

const render = function() {
  bindEvent()
}

2, Strategy mode

Definition: define a series of algorithms and encapsulate them into policy classes. The algorithms are encapsulated in the methods inside the policy class. When a customer makes a request for Context, Context always delegates the request to one of these policy objects for calculation.

Calculate bonus using strategy mode:

// In the JavaScript language, functions are also objects. So define the policy class as a function
const strategies = {
  'S': function(salary) {
    return salary * 4
  },
  'A': function(salary) {
    return salary * 3
  },
  'B': function(salary) {
    return salary *2
  }
}
// Use the calculateBonus function as the Context to accept the user's request
const caculateBonus = function(level, salary) {
  return strategies[level](salary)
}

const a = caculateBonus('S', 10000)
console.log(a) // 4000

3, Agent mode

Definition: proxy mode is to provide a substitute or placeholder for an object to control access to it.
Virtual agent realizes picture preloading:

 const myImage = (function() {
      const image = document.createElement('img')
      document.body.appendChild(image)
      return {
        setSrc: function(src) {
          image.src = src
        }
      }
    })()
    const proxyImage = (function() {
      const image = new Image()
      image.onload = function() {
        myImage.setSrc(this.src)
      }
      return {
        setSrc: function(src) {
          myImage.setSrc('loadingsrc')
          image.src = src
        }
      }
    })()
    proxyImage.setSrc('src')

Meaning: the principle of single responsibility: a class should have only one reason for its change. If the object has multiple responsibilities, it may cause multiple changes. Object oriented design encourages the distribution of behavior among fine-grained objects. If an object undertakes too many responsibilities, it is equivalent to coupling these responsibilities together. This coupling will lead to fragile and low cohesion design. When changes occur, the design may be accidentally damaged. Responsibilities are defined as "causes of change".
And in object-oriented programming, in most cases, if it violates any other principles, it will violate the open closed principle at the same time.
Consistency of agent and ontology interfaces:
The user can safely request the agent. He only cares about whether he can get the desired result.
Proxies can be replaced wherever ontologies are used.
Cache agent:
The cache agent can provide temporary storage for some expensive operation results. During the next operation, if the parameters passed in are consistent with the previous ones, the previous operation results can be returned directly.
Cache calculation product:

const add = function() {
  let a = 0
  for(let i=0; i<arguments.length; i++) {
    a+=arguments[i]
  }
  return a
}

const mult = function() {
  let a = 1
  for(let i=0; i<arguments.length; i++) {
    a*=arguments[i]
  }
  return a
}

const createProxyFactory = function(fn) {
  const cache = {}
  return function() {
   const args = Array.prototype.join.call(arguments, '') // Using call to borrow array
   if(cache[args]) {
     return cache[args]
   }
   return cache[args] = fn.apply(this, arguments)
  }
}

const afunc = createProxyFactory(add)
const bfunc = createProxyFactory(mult)
console.log(afunc(1,2,3,4))
console.log(bfunc(1,2,3,4))

4, Iterator mode

definition:
The iterator pattern refers to providing a way to access the elements of an aggregate object sequentially without exposing the internal representation of the object.

5, Publish subscribe mode

Definition: publish subscribe mode, also known as observer mode, defines a one to many dependency between objects. When the state of an object changes, all objects that depend on it will be notified
Global publish subscribe objects:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <button id='count'>click me</button>
  <div id='show'></div>
</body>
<script>
    // Publish subscribe global Event object
    const Event = (function() {
     let clientList = {}, // Cache list to store subscriber's callback function
     trigger, listen, remove
     listen = function(key, fn) {
       if(!clientList[key]) {
         clientList[key] = [] // If you have not subscribed to such messages, create a cache list for such messages
       }
       clientList[key].push(fn) // The subscribed messages are added to the message cache list
     }

     trigger = function() {
       const key = Array.prototype.shift.call(arguments) //Fetch message type
       const fns = clientList[key] // Retrieve the subscriber callback function corresponding to this type
       if(!fns || !fns.length) { // If there is no subscription message, return
         return false
       }
       for(let i = 0; i<fns.length; i++) {
         const fn = fns[i]
         fn.apply(this, arguments)
       }
     }

     remove = function(key, fn) {
       const fns = clientList[key]
       if(!fns) { //If the message corresponding to the key is not subscribed, it will be returned directly
         return false
       }
       if(!fn) { // If no specific function is specified, all subscriptions to the message corresponding to the key will be cancelled
         fns && (fns.length = 0)
       } else {
         for(let i=fns.length; i>0; i--) { // Reverse traverse subscription list
           const _fn=fns[l]
           if(fn === _fn) {
             fns.splice(1,1) // Delete subscriber's callback function
           }
         }
       }
     }

     return {
       listen,
       trigger,
       remove
     }
   })()


   const a = (function(){
     let count = 0
     const button = document.getElementById('count')
     button.onclick = function() {
       Event.trigger('add', count++)
     }
   })()
   const b = (function(){
     const div = document.getElementById('show')
     Event.listen('add', function(count){
       div.innerHTML = count
     })
   })()
 </script>
</html>

6, Command mode

definition:
Sometimes you need to send a request to some objects, but you don't know who the receiver of the request is or what the requested operation is. Using the command pattern can enable the request sender and the request receiver to eliminate the coupling relationship between each other

Example: menu program

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <button id='button1'>refresh</button>
  <button id='button2'>add</button>
  <button id='button3'>delete</button>
</body>
  <script>
    // Implementing command mode with closures
    const btn1 = document.getElementById('button1')
    const btn2 = document.getElementById('button2')
    const btn3 = document.getElementById('button3')
    // setCommand is responsible for installing commands on the button
    const setCommand = function(button, command) {
      button.onclick = () => {
        command.execute()
      }
    }
    // The behavior that the button needs to perform
    const MenuBar = {
      refresh: () => {
        alert('refresh')
      }
    }
    const SubMenu = {
      add: () => {
        alert('add')
      },
      del: () => {
        alert('delete')
      }
    }
    // Encapsulate behavior into command classes
    const RefreshMenuBarCommand = function(receiver) {
      return {
        execute: function() {
          receiver.refresh() // The receiver is enclosed in the context of closure generation
        }
      }
    }
    const AddSubMenuCommand = function(receiver) {
      return {
        execute: function() {
          receiver.add()
        }
      }
    }
    const DelSubMenuCommand = function(receiver) {
      return {
        execute: function() {
          receiver.del()
        }
      }
    }
    // Pass the command receiver into the command object and install the command object on the button
    const refreshMenuBarCommand = RefreshMenuBarCommand(MenuBar)
    const addSubMenuCommand = AddSubMenuCommand(SubMenu)
    const delSubMenuCommand = DelSubMenuCommand(SubMenu)
    setCommand(btn1, refreshMenuBarCommand)
    setCommand(btn2, addSubMenuCommand)
    setCommand(btn3, delSubMenuCommand)
  </script>
</html>

Example: redo

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <button id="replay">Play video</button>
</body>
<script>
  const Ryu = { 
    attack: function(){
      console.log('attack')
    },
    defense: function(){
      console.log('defense');
    },
    jump: function(){
      console.log('jump');
    },
    crouch: function(){
      console.log( 'Get down' );
    }
  }

  const makeCommand = function(receiver, state) { //Create command
    return function() {
      receiver[state] && receiver[state]()
    }
  }

  const commands = {
    "119": "jump", // W
    "115": "crouch", // S
    "97": "defense", // A
    "100": "attack" // D
  }

  const commandStack = [] //Stack for saving commands
  document.onkeypress = function(e) {
    const code = e.keyCode
    const command = makeCommand(Ryu, commands[code])
    if (command) {
      command() // Execute command
      commandStack.push(command) // Save command
    }
  }

  document.getElementById('replay').onclick = function() {
    let command
    while(command = commandStack.shift()) {
      command()
    }
  }
</script>
</html>

7, Combination mode

definition:
Composite mode is to use small sub objects to build larger objects, and these small sub objects themselves may be composed of smaller "grandchildren". We can apply the same operation to composite objects and single objects. In most cases, we can ignore the differences between composite objects and individual objects and deal with them in a consistent way.

Example: scan folders

// folder
const Folder = function(name) {
  this.name=name
  this.files=[]
}

Folder.prototype.add = function(file) {
  this.files.push(file)
}

Folder.prototype.scan = function() {
  console.log('start to scan folder', this.name)
  for(let i=0; i<this.files.length; i++) {
    const file = this.files[i]
    file.scan()
  }
}

//file
const File = function(name) {
  this.name = name
}

File.prototype.add = function() {
  throw new Error('cannot add for file')
}

File.prototype.scan = function() {
  console.log('start to scan file', this.name)
}

var folder = new Folder('Learning materials')
var folder1 = new Folder('JavaScript')
var folder2 = new Folder ('jQuery')
var file1 = new File('JavaScript Design pattern and development practice')
var file2 = new File('master jQuery')
var file3 = new File('Reconstruction and pattern')
folder1.add(file1)
folder2.add(file2)
folder.add(folder1)
folder.add(folder2)
folder.add(file3)
// In the process of adding a batch of files, the customer does not need to distinguish whether they are files or folders.
folder.scan() // Scan from the top of the tree

Operation results:

matters needing attention:
1. The combination mode is not a parent-child relationship
The combination pattern is a HAS-A (aggregation) relationship, not IS-A. The composite object contains a set of Leaf objects, but the Leaf
Is not a subclass of Composite. The Composite object delegates the request to all the leaf objects it contains, which is the key to their cooperation
Have the same interface.
2. Consistency of operations on leaf objects
In addition to requiring that the composite object and leaf object have the same interface, the composite mode also has a necessary condition, that is, for a group of objects
The operation of leaf objects must be consistent.
3. Bidirectional mapping relationship
The combination pattern is not suitable when the relationship between objects is not a strict hierarchy
4. Use the responsibility chain mode to improve the performance of the combination mode
With the help of responsibility chain mode, we can avoid traversing the whole tree and improve performance

7, Sharing mode

definition:
Sharing meta mode requires that the attributes of an object be divided into internal state and external state (attributes). The goal of meta sharing mode is to minimize the number of shared objects.
Division of internal and external states:

  • The internal state is stored inside the object.
  • The internal state can be shared by some objects.
  • The internal state is independent of the specific scene and usually does not change.
  • The external state depends on the specific scene and changes according to the scene. The external state cannot be shared.

8, Responsibility chain model

definition:
Make multiple objects have the opportunity to process the request, connect these objects into a chain, and pass the request along the chain until one object processes it

realization:
Define a constructor Chain. The parameters passed during new Chain are the functions that need to be wrapped. At the same time, it also has an instance attribute this Success, indicating the next node in the Chain.
Chain.prototype.setNextSuccessor specifies the next node in the chain
Chain.prototype.passRequest passes the request to a node

var order500 = function( orderType, pay, stock ){ // ordertype: order type, pay: pay or not, stock: inventory
  if ( orderType === 1 && pay === true ){ 
  console.log( '500 Advance purchase with RMB deposit and get 100 coupons' ); 
  }else{ 
  return 'nextSuccessor'; // I don't know who the next node is. Anyway, pass the request to the back
  } 
 }; 
 var order200 = function( orderType, pay, stock ){ 
  if ( orderType === 2 && pay === true ){ 
  console.log( '200 Advance purchase with a deposit of RMB and get 50 coupons' ); 
  }else{ 
  return 'nextSuccessor'; // I don't know who the next node is. Anyway, pass the request to the back
  } 
 }; 
 var orderNormal = function( orderType, pay, stock ){ 
  if ( stock > 0 ){ 
  console.log( 'Ordinary purchase, no coupons' ); 
  }else{ 
  console.log( 'Insufficient mobile phone inventory' ); 
  } 
 };

const Chain = function(fn) {
  this.fn = fn
  this.successor = null //Next node in the chain
}

Chain.prototype.setNextSuccessor = function(successor) { //Specifies the next node in the chain
  return this.successor = successor
}

Chain.prototype.passRequest = function() { // Pass a request to a node
  const ret = this.fn.apply(this, arguments)
  if(ret === 'nextSuccessor') {
    return this.successor && this.successor.passRequest.apply(this.successor, arguments)
  }
  return ret
}

// Wrap the order function into a node of the responsibility chain
var chainOrder500 = new Chain( order500 )
var chainOrder200 = new Chain( order200 )
var chainOrderNormal = new Chain( orderNormal )

// Set the order of node responsibility chain
chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)

order500( 1 , true, 500); // Output: 500 yuan deposit for advance purchase and 100 coupons
order500( 1, false, 500 ); // Output: ordinary purchase without coupons
order500( 2, true, 500 ); // Output: 200 yuan deposit, 500 coupons
order500( 3, false, 500 ); // Output: ordinary purchase without coupons
order500( 3, false, 0 ); // Output: insufficient mobile phone inventory

Implement responsibility chain with AOP:

var order500 = function( orderType, pay, stock ){ // ordertype: order type, pay: pay or not, stock: inventory
  if ( orderType === 1 && pay === true ){ 
  console.log( '500 Advance purchase with RMB deposit and get 100 coupons' ); 
  }else{ 
  return 'nextSuccessor'; // I don't know who the next node is. Anyway, pass the request to the back
  } 
 }; 
 var order200 = function( orderType, pay, stock ){ 
  if ( orderType === 2 && pay === true ){ 
  console.log( '200 Advance purchase with RMB deposit and get 50 coupons' ); 
  }else{ 
  return 'nextSuccessor'; // I don't know who the next node is. Anyway, pass the request to the back
  } 
 }; 
 var orderNormal = function( orderType, pay, stock ){ 
  if ( stock > 0 ){ 
  console.log( 'Ordinary purchase, no coupons' ); 
  }else{ 
  console.log( 'Insufficient mobile phone inventory' ); 
  } 
 };

 Function.prototype.after = function( fn ){ 
  var self = this; 
  return function(){ 
  var ret = self.apply( this, arguments ); 
  if ( ret === 'nextSuccessor' ){ 
  return fn.apply( this, arguments ); 
  } 
  return ret; 
  } 
 };

var order = order500.after( order200 ).after( orderNormal ); 
order( 1, true, 500 ); // Output: 500 yuan deposit for advance purchase and 100 coupons
order( 2, true, 500 ); // Output: 200 yuan deposit, 50 coupons
order( 1, false, 500 ); // Output: ordinary purchase without coupons

9, Intermediary mode

definition:
After adding a mediator object, all related objects communicate through the mediator object instead of referencing each other. Therefore, when an object changes, you only need to notify the mediator object.

10, Decorator mode

definition:
Decorator mode can dynamically add responsibilities to objects during program operation without changing the object itself. Compared with inheritance, decorators are a more light and flexible approach, which is a "pay as you go" way

Simulate the decorator pattern of traditional object-oriented language:

// Original aircraft
const Plane = function() {}
Plane.prototype.fire = function() {
  console.log('Send ordinary bullets')
}
// Add two decoration categories, namely missile and atomic bomb
const MissileDecorator = function(plane) {
  this.plane = plane
}
MissileDecorator.prototype.fire = function() {
  this.plane.fire()
  console.log('Missile launch')
}

const AtomDecorator = function(plane) {
  this.plane = plane
}
AtomDecorator.prototype.fire = function() {
  this.plane.fire()
  console.log('Launch an atomic bomb')
}

let plane = new Plane()
plane = new MissileDecorator(plane)
plane = new AtomDecorator(plane)
plane.fire()
// //Output respectively: Launch ordinary bullets, launch missiles and launch atomic bombs

The constructors of missile class and atomic bomb class both accept the parameter plane object and save this parameter. In their fire methods, in addition to performing their own operations, they also call the fire method of plane object.
This way of dynamically adding responsibilities to objects does not really change the object itself, but puts the object into another object. These objects are referenced in a chain to form an aggregate object. These objects all have the same interface (fire method). When the request reaches an object in the chain, the object will perform its own operation, and then forward the request to the next object in the chain.
Because the decorator object and the object it decorates have the same interface, they are transparent to the customers who use the object, and the decorated object does not need to know that it has been decorated. This transparency allows us to nest any number of decorator objects recursively

Rewriting an object or a method of an object does not use "class" to implement decorator mode:

var plane = { 
 fire: function(){ 
 console.log( 'Fire ordinary bullets' ); 
 } 
} 
var missileDecorator = function(){ 
 console.log( 'Missile launch' ); 
} 
var atomDecorator = function(){ 
 console.log( 'Launch an atomic bomb' ); 
} 
var fire1 = plane.fire; 
plane.fire = function(){ 
 fire1(); 
 missileDecorator(); 
} 
var fire2 = plane.fire; 
plane.fire = function(){ 
 fire2(); 
 atomDecorator(); 
} 
plane.fire(); 
// Output respectively: Launch ordinary bullets, launch missiles and launch atomic bombs

11, State mode

definition:
Allows an object to change its behavior when its internal state changes, and the object appears to have modified its class. The first sentence means to encapsulate the state into independent classes and delegate the request to the current state object. When the internal state of the object changes, it will bring different behavior changes. For example, when the electric light is in two different states: off and on, we click the same button and get different behavior feedback. The second sentence is from the customer's point of view. The object we use has different behavior in different states. This object seems to be instantiated from different classes. In fact, this is the effect of using delegation.

12, Adapter mode

definition:
The function of adapter mode is to solve the problem of incompatible interface between two software entities. After using the adapter mode, two software entities that could not work due to incompatible interfaces can work together.
There are many such scenarios in program development: when we try to call an interface of a module or object, we find that the format of this interface does not meet the current requirements. At this time, there are two solutions. The first is to modify the original interface implementation. However, if the original module is very complex, or the module we get is a compressed code written by others, it seems unrealistic to modify the original interface. The second way is to create an adapter to convert the original interface into another interface that the customer wants. The customer only needs to deal with the adapter.
Example: display map
Suppose Google map uses the show method to display the map and Baidu map uses the display method

var googleMap = { 
 show: function(){ 
 console.log( 'Start rendering Google Maps' ); 
 } 
 }; 
var baiduMap = { 
 display: function(){ 
 console.log( 'Start rendering Baidu map' ); 
 } 
 }; 
var baiduMapAdapter = { 
 show: function(){ 
 return baiduMap.display();
 } 
 }; 
renderMap( googleMap ); // Output: start rendering Google Maps
renderMap( baiduMapAdapter ); // Output: start rendering Baidu map

Packaging mode:
Adapter mode, decorator mode, proxy mode and appearance mode all belong to "packaging mode", in which one object wraps another object. The key to distinguishing them is still the intention of the model.

  • The adapter pattern is mainly used to solve the problem of mismatch between two existing interfaces. It does not consider how these interfaces are implemented or how they may evolve in the future. The adapter pattern can make them work together without changing the existing interfaces.
  • Decorator mode and proxy mode will not change the interface of the original object, but the function of decorator mode is to add functions to the object. Decorator patterns often form a long decorative chain, while adapter patterns are usually packaged only once. Proxy mode is used to control access to objects, which is usually wrapped only once.
  • The role of the appearance pattern is similar to that of the adapter. Some people regard the appearance pattern as a set of object adapters, but the most prominent feature of the appearance pattern is the definition of a new interface.

Topics: Design Pattern