Some common design patterns of JavaScript

Keywords: Javascript Mac Vue Programming React

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.

Reference Articles

Posted by antoine on Sun, 18 Aug 2019 23:25:49 -0700