citation

Design pattern is the predecessors through continuous practice, summed up similar to the formula of martial arts secrets, is a certain scene to solve the problem of elegant way.

Forward thinking

Before introducing the specific design mode, the author would like to introduce several important design ideas.

  1. The open-closed principle (OCP) is open to extension, closed to modification. In the process of requirements change and iteration, modules often need to be updated. The easiest way to do this is to modify the source code, but modifying the code is dangerous and can lead to fixing a bug and introducing the possibility of countless bugs. Extension modules are a better choice.

  2. Single responsibility principle (SRP) For a function, there should be only one responsibility, only one cause for change.

  3. Encapsulation change is a constant theme of design patterns. In a module, there are always changing parts and stable parts. In the evolution of a module, you only need to replace the changed parts, which are relatively easy to replace if they are already packaged. The front end often responds to the processing of events. The processing of events, which varies from time to time and often changes, can be encapsulated into a callback function to increase the expansibility of the module.

  4. Of the 23 design patterns proposed by GOF, the UML class diagrams between some design patterns are almost identical. But the intent is so different that it becomes a new pattern rather than a variation of the previous one.

After reading what I think are the most important ideas, let’s explore some common front-end design patterns with my thoughts

The strategy pattern

The purpose of the policy pattern is to separate the use of algorithms from the implementation of algorithms.

The requirement of form verification is common in front-end development, such as verification of user name, password and mobile phone number

<script>
handleSubmit() {
  const { userName, password, tele } = formValues
  if (userName === ' ') {
    alert('User name cannot be empty')
    return false
  }
  if(password? .length <6) {
    alert('Password length must not be less than 6 characters')
    return false
  }
  if (!/ (a ^ 1 [3 | | 5 8] [0-9] {9} $) /.test(tele)) {
    alert('Incorrect format of mobile number')
    return false
  }
  // pass validate, do something
}
</script>
Copy the code

Our usual approach is to take streaming verification. However, it violates the open-closed principle. If you add or change rules, or add a form, you need to modify the specific logic of handleSubmit, and if other forms require similar validation, you need to copy the code

<script>
  // Split the implementation of the policy algorithm into a single policy class or policy object
  // Is the embodiment of encapsulation changes
  const strategies = {
    isNonEmpty: function(errorMsg, value) {
      if (value === ' ') {
        return errorMsg
      }
    },
    minLength: function(errorMsg, value, lenght) {
      if (value.length < length) {
        return errorMsg
      }
    },
    isMobile: function(errorMsg, value) {
      if (!/ (a ^ 1 [3 | | 5 8] [0-9] {9} $) /.test(value)) {
        return errorMsg
      }
    }
  }
</script>
Copy the code

You can extract the required validation rules (each policy) into a policy object (encapsulating policy changes)

function Validator() {
  this.validateList = []
}
// Add the verification rule
Validator.prototype.add = function(dom, rule, errorMsg) {
  const args = rule.split(':') // minLength:6 for this scenario
  this.validateList.push(
    function() {
      const strategy = args.shift() // Extract policy
      args.unshift(dom.value) // The value to be verified
      args.push(errorMsg) // Add an error message
      return strategies[strategy].apply(dom, args) // Delegate to the policy object})}// Validates all form validations
Validator.prototype.validate = function() {
  // is the use part of the algorithm
  const validateList = this.validateList 
  for(let i = 0; i < validateList.length; i++) {
    const { msg } = validateList[i]()
    if (msg) {
      return msg
    }
  }
}
Copy the code

You then create a Validator class that acts as a context for receiving validation requests from the user and delegating to the policy object to execute the specific policy

<script>
handleSubmit() {
  const { userName, password, tele } = formValues
  const validator = new Validator()
  validator.add('userName'.'isNonEmpty'.'User name cannot be empty')
  validator.add('password'.'minLength:6'.'Password length must not be less than 6 characters')
  validator.add('tele'.'isMobile'.'Incorrect format of mobile number')
  const errorMsg = validator.validate()
  if (errorMsg) {
    alert(errorMsg)
    return false
  }
  // pass validate, do something
}
</script>
Copy the code

The state pattern

The key to state models is to distinguish between internal states of things, because changes in internal states often lead to changes in the behavior of things

For example, an electric lamp has off, low light and strong light. Pressing the switch will switch between these states in turn. We might write code like this:

function handleButtonPressed() {
  switch(this.state) {
    case 'off':
      console.log('weak light')
      this.state = 'weakLight'
      break
    case 'weakLight':
      console.log('light')
      this.state = 'strongLight'
      break
    case 'strongLight':
      console.log('off')
      this.state = 'off'
      break
    default:
      break}}Copy the code

The code above first violates the -closed principle by changing the code for the handleButtonPressed method every time you add or modify the state. Second, the state-related behavior is encapsulated in handleButtonPressed (the behavior can be split and handleButtonPressed is only used as a distribution center), causing code to swell. Finally, there are many switch-case branches.

This can be improved through state patterns

<script>
  const FSM = {
    off: {
      handleButtonPressed() {
        console.log('weak light')
        this.curState = FSM.weakLight
      }
    },
    weakLight: {
      handleButtonPressed() {
        console.log('light')
        this.curState = FSM.strongLight
      }
    },
    strongLight: {
      handleButtonPressed() {
        console.log('off')
        this.curState = FSM.off
      }
    }
 }
 export default {
    data() {
      return {
        curState: FSM.off
      }
    },
    methods: {
      handleButtonPressed() {
        this.curState.handleButtonPressed.call(this)
      }
    }
  }
</script>
Copy the code

The benefit of the improved code is that the relationship between the state and the corresponding behavior is localized and there is not much if-else or switch-case. When you need to add a new state, you simply add a state object to the FSM and change the state transition code slightly.

Reviewing the policy patterns described above, both have some policy or state that is delegated by context to the policy or state object that is actually triggered, and if you draw class diagrams of the policy and state patterns, you will find that they are almost identical. However, the two modes are different in intent. Each policy of the former is equal and parallel, and there is no connection between each policy, for example, there is no connection between non-null check and mobile phone number policy. In the latter, the state and behavior have long been encapsulated, and the switch between the states has long been defined, there is a connection between the states and can be state transition. This is the idea that the key to discerning patterns is intention rather than structure.

Finite State Machine

There are many states involved in the lamp, and there are transitions between states, which is actually a finite state machine concept.

Github has a third-party library for creating finite state machines. For details, see javascript-state-machine

Observer model

The observer mode is divided into two roles: observer and observed object (referred to as object). The observer observes the object, stores itself in the object, and notifies all observers observing the object when the object changes. It is a one-to-many relationship.

XiaoF and xiaoY are addicted to watching TV dramas, but they don’t want to open the APP to check the updates all the time, so they turn on the update reminder of TV dramas.

// Show object
function TVSubject(name) {
  this.name = name
  this.observerList = []
}

// Add TV update listener
TVSubject.prototype.addObserver = function(observer) {
  this.observerList.push(observer)
}

// TV series update notification
TVSubject.prototype.notify = function() {
  // Iterate through the list of observers, triggering updates in turn
  this.observerList.forEach(observer= > {
    observer.update(this.name)
  })
}

/ / observer
function Observer() {}
Observer.prototype.update = function(. args) {
  console.log(`${args[0]}The TV series has been updated)}const tvSubject = new TVSubject('The world')
const xiaoF = new Observer() // xiaoF, a drama follower
const xiaoY = new Observer() // xiaoY, a fan of the drama
tvSubject.addObserver(xiaoF) // xiaoF adds the update reminder
tvSubject.addObserver(xiaoY) // xiaoY adds an update reminder

setTimeout(() = > {
  // After some time, the episode is updated to alert all observers
  tvSubject.notify()
}, 1000)
Copy the code

Publish and subscribe model

The publish-subscribe model is different from the observer model.

XiaoF and xiaoY have not only been watching TV series recently, but also have reached marriageable age and need to prepare to buy a wedding house. There happens to be a number of properties nearby that are about to go on sale, but the exact timing is unknown. XiaoF and xiaoY ask the sales manager to inform them when the property is on sale

/ / developer
function Publisher() {}
// Bind to the sales department for later notification to the corresponding sales department
Publisher.prototype.connect = function(salesOfficer) {
  this.salesOfficer = salesOfficer
}
// Inform the sales department of the commercial housing for sale
Publisher.prototype.notify = function() {
  this.salesOfficer.trigger()
}

/ / sales department
function SalesOfficer() {
  this.purchaserList = {}
}
// Subscribe to the sale of the property, area purchased area
SalesOfficer.prototype.subscribe = function(area, fn) {
  if (!this.purchaserList[area]) {
    this.purchaserList[area] = []
  }
  this.purchaserList[area].push(fn) // Save the subscriber to the buyer list
}
// Inform those who have intention to buy, the property is open for sale
SalesOfficer.prototype.trigger = function(. args) {
  const area = args[0]
  const fns = this.subscriberList[area]
  if(! fns || fns.length ===0) {
    return false
  }
  fns.forEach(fn= > {
    fn.call.apply(this, args)
  })
}

/ / buyers
function Purchaser() {}
Purchaser.prototype.call = function(. args) {
  console.log('Hey, hey, the building is on sale! Now or never ')}const saleManager = new SalesOfficer() // Sales Manager A
const publisher = new Publisher() // Unscrupulous developer B
publisher.connect(saleManager)
const xiaoF = new Purchaser() / / mortgage slave xiaoF
const xiaoY = new Purchaser() / / mortgage slave xiaoY
saleManager.subscribe(100, xiaoF) // I would like to buy a house of 100 square meters
saleManager.subscribe(80, xiaoY) // I hope to buy a house of 80 square meters

setTimeout(() = > {
  // After a period of time, the publisher informs the sales department that the property is available for sale
  publisher.notify()
}, 1000)
Copy the code

There are three kinds of roles, one is the publisher of the sale: developers, a transfer station is equivalent to the sales manager, in the receipt of developers to inform the sale, notice the relevant buyers. Finally, buyers subscribe to the news of the sale. There is no coupling between developers and buyers, developers and buyers, do not care about each other, through the sales department to contact. A developer building can have multiple buyers, a buyer can also buy properties in multiple places, it is a many-to-many relationship.

The observant reader may notice that many-to-many can be split into two one-to-many. The separation of developer and sales manager and sales manager and buyer can be regarded as the embodiment of two pairs of observer model. Therefore, the author’s opinion is that the publish-subscribe model is different from the observer model, but the publish-subscribe model includes the observer model

The publish-subscribe pattern has the advantage of decoupling both in time (asynchronous notifications) and between objects

However, if the publish-subscribe model is used extensively, the low coupling of publisher and subscriber to communicate can cause the problem of unclear data communication. If a Vue uses dispatch and broadcast for communication, data flows may be chaotic 😵💫

Push/pull model

The above sale, can sell all the information to push to buyers, also can just tell buyers, the sale, let it take the initiative to ask (pull) information. This is divided into push model and pull model

  • Push model: When an event occurs, the publisher pushes all the status and data to subscribers at once
  • Pull model: When an event occurs, the publisher only notifes the subscriber that an update has occurred, requiring the subscriber to take the initiative to pull data. The advantage is that it canAccording to the need to pullRelevant data.

Is Vue2 a push model or a pull model for publish and subscribe updates?

Adapter mode

An adapter, as its name implies, ADAPTS to problems that cannot be used due to inconsistent interfaces.

There are many examples of this in life. For example, some screens support VGA or HDMI, and if the cables around them don’t match, you can use an adapter to convert. For example, buy a Hong Kong version of the MacBook, but its charging plug is not consistent with the standard of the mainland, then you can add an adapter to solve.

Adapters are a “never too late” pattern, because if everything was perfect and well-matched, the pattern would not be used. For example, before the JSON format became popular, the interface returned XML data. If you want to continue to reuse the interface, you can add an XML-JSON adapter. In some cases, if it is only a change in the data structure, there is no need to go through the reconstruction of the data structure, just through the adapter.

Application of the adapter pattern in Axios

Axios supports sending HTTP requests in both the browser and Node environment. It is internally implemented using the adapter pattern.

  • Browser environment
function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var request = new XMLHttpRequest(); // Create XHR instance
    var fullPath = buildFullPath(config.baseURL, config.url); // Build the full request URL
    request.onloadend = onloadend; // Process the response
    request.open(config.method.toUpperCase(), fullPath, true) // Preopen the request
    request.send() // Send the request})}Copy the code
  • The Node environment
function httpAdapter(config) {
  return new Promise(function dispatchHttpRequest(resolve, reject) {
    var transport = isHttpsProxy ? https : http; // HTTPS/HTTP according to the network protocol
    var req = transport.request(options, handleResponse) // Create the request and listen for the response
    var fullPath = buildFullPath(config.baseURL, config.url); // Build the full request URL
    req.end(data); // Send the request})}Copy the code

The adapter is then called externally

function dispatchRequest(config) {
  var adapter = config.adapter || defaults.adapter;
  return adapter(config).then(function onAdapterResolution(response) {
    // Request processing succeeded
    return response;
  }, function onAdapterRejection(reason) {
    // Failed to process the request
    return Promise.reject(reason); })}Copy the code

Axios shields the environment by calling Adapter, exposing the same interface and using the same invocation.

The proxy pattern

The proxy pattern is also common in life. For example, as workers, we need to write a weekly report every week and send it to the superior leader, who acts as an intermediary agent to summarize the work content and send it to the senior leader. So why don’t we just send it to the big boss? If they do, they will inevitably be swamped with emails and unable to concentrate on their weekly papers, reducing their productivity.

In the front-end development, in order to give users a better experience, almost all use image preloading.

Let’s look at the original method

const myImage = (function() {
  const imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  var img = new Image()
  img.onload = function() {
    imgNode.src = img.src
  }
  return {
    setSrc: function (src) {
      imgNode.src = 'https://path-to-loading.gif'
      imgNode.src = src
    }
  }
})();

myImage.setSrc('https://path-to-realPicPath.png')
Copy the code

The above implementation not only needs to set the SRC of the image, but also is responsible for image preloading. If the subsequent network is fast enough that image preloading is not required, the specific logic of myImage needs to be modified to remove the relevant logic of preloading. In fact, the main responsibility above is to set the SRC for the IMG, and image preloading is an additional function, so you can use the idea of proxy mode to transfer the preloading function to the proxy object.

// The original method is only responsible for the primary responsibilities
const myImage = (function() {
  const imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  
  return {
    setSrc: function (src) {
      imgNode.src = src
    }
  }
})();

// The proxy object exposes the same interface as the original object
// Add preload responsibilities
const proxyImage = (function() {
  const img = new Image()
  img.onload = function() {
    myImage.setSrc(this.src)
  }
  
  return {
    setSrc: function(src) {
      myImage.setSrc('https://path-to-loading.gif')
      img.src = src
    } 
  }
})();

// Set the image through the proxy object
proxyImage.setSrc('https://path-to-realPicPath.png')
Copy the code

Through the proxy object proxyImage, the client’s access to the ontology myImage is controlled, and the image is preloaded before it is really loaded. If the subsequent network is fast enough to eliminate the need for preloading, simply replace all proxy objects with the original.

Looking back at the adapter pattern mentioned above, both seem to add a middleman between the real triggering ontology and the real receiving ontology. The intent of the adapter pattern tends to be to do something through the middleman to make the two things work properly, whereas the intent of the proxy pattern tends to control access to real objects.

conclusion

The realization of various design modes and the concrete embodiment of design ideas are introduced. Design patterns, when used in the right context, achieve high cohesion, low coupling, and easy to expand. But don’t overdesign, and avoid the trap of holding a hammer and seeing a nail in everything.

In the end, if there are mistakes or not precise place, welcome to correct criticism. Hope to inspire you, welcome to like, discuss 👏

reference

  1. javascript-state-machine
  2. xstate
  3. axios
  4. async-validator
  5. JavaScript design patterns and development practices