preface

In daily development, I have to encounter conditional branching judgment scenarios every day. However, I have written a lot of if/else judgments in my business just a few years after graduation, and sometimes I feel uncomfortable when I see them. Worry so I started thinking, how to optimize conditions of branch code, also recently read some articles, which articles are mentioned the strategy pattern, I has a curious on this thing, so I spent a lot of time to learn the thought of it, and think of a better way to share, rather than a concept about the outset.

The scene is introduced into

You’re frowning and fixing bugs when the product manager comes up to you and says, “Boy, there’s a very simple registration function that needs to be implemented. This form needs to include the following user information:

  • The user name
  • password
  • Mobile phone no.

Verify the format when submitting the form:

  • The user name cannot be empty.
  • The password must contain at least six characters.
  • The phone number format is correct.

You have roughly analyzed it and feel it is still simple. Your idea is as follows:

  • Get the form instance before submitting.
  • Validates each form entry

The question is how do you write this check, so you start implementing your code.

implementation

If/else implementation

Let’s first set up the DOM structure, regardless of the style is very simple:

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
    <title>design-pattern-strategy</title>
  </head>
  <body>
    <form action="/register" method="post" id="registerForm">
      <div class="form-item">Please enter user name:<input type="text" name="userName"/ >
      </div>

      <div class="form-item">Please enter your password:<input type="password" name="password"/ >
      </div>

      <div class="form-item">Please enter your mobile phone number:<input type="text" name="phoneNumber"/ >
      </div>
      <button>submit</button>
    </form>
  </body>
</html>

Copy the code

Then we need to implement our JS logic by getting the form instance and checking each field:

  const registerForm = document.getElementById('registerForm')

  const validatorFn = (form) = > {
    if (form.userName.value === ' ') {
      alert('User name cannot be empty')
      return false
    }

    if (form.password.value.length < 6) {
      alert('Password length must not be less than 6 characters')
      return false
    }

    if (!/ (a ^ 1 [3 | | 5 8] [0-9] {9} $) /.test(form.phoneNumber.value)) {
      alert('Incorrect format of mobile number')
      return false
    }

    return true
  }

  registerForm.onsubmit = function (e) {
    const isValid = validatorFn(this)

    if(! isValid) {return false
    }

    alert('success submit')

    e.preventDefault()
  }
Copy the code

The implementation effect is as follows:

You happily submit the code and start fishing

Policy pattern implementation

Now another one of your colleagues gets a request:

  • The user name must contain only letters and numbers.
  • Change your password to no less than 8 and up to 100 characters.
  • Added mailbox authentication.

It finds the validatorFn method, looks at your if/else and adds the rule, and the validation function gets bigger and bigger…

And then you find that this rule is not only used for registration module, when you want to reuse this code, you find why you wrote so many judgments, but you have no choice but to CV the same code in the past, and finally this code becomes shit in the eyes of others…

Over the next month or so, you start practicing design patterns. You find that strategic patterns optimize this part of logic very well. Although the previous code was really bad, you still have a desire for code, so you spend a lot of time optimizing this code.

  • Make rules configurable so that users don’t have to worry about their internal implementation, and they need to conform to the “open-closed” principle.
  • Rules can be reused for other modules.

First, we need a rule object that encapsulates all rule method processing, and these methods need to follow some parameter rules:

  • The first parameter is the value to verify
  • The last parameter receives the text displayed when verification fails
  • Verification parameters can be passed in between

Now let’s put this file in a separate place for maintenance:

rules.js

export const rules = {
  // Non-null judgment
  isNonEmpty(value, errorMsg) {
    if (value === ' ') {
      return errorMsg
    }
  },
  // Minimum length limit
  minLength(value, errorMsg, length) {
    if (value.length < length) {
      return errorMsg
    }
  },
  // Whether it is a mobile number
  isMobile(value, errorMsg) {
    if (!/ (a ^ 1 [3 | | 5 8] [0-9] {9} $) /.test(value)) {
      return errorMsg
    }
  },
}
Copy the code

With this object, we can uniformly add various rules and easily change them if they change, regardless of the specific business logic.

Next we need to abstract a Validator, which you can think of as a controller, to add and fire rules. It has the following properties:

  • cacheArray, which holds all rule configurations.
  • addMethod to add rules to a form item.
  • validatorMethod that triggers validation of all form items.

Let’s implement this class:

validator.js

import { rules } from './rules.js'

class Validator {
  constructor() {
    this.cache = []
  }

  /** * Add rule *@param {HTMLElement} FormItem indicates the formItem to be verified@param {Array<{ rule: string; errorMsg: string }>} ruleConfig Verify rule */
  add(formItem, ruleConfig) {
    // Iterate all rule configurations and store them in the cache
    ruleConfig.forEach(({ rule, errorMsg }) = > {
      MinLength :6 -> ['minLength', '6']
      const params = rule.split(':')

      // Store the validation function in the cache
      this.cache.push(() = > {
        // Check rules
        const _rule = params.shift()
        // Check the value
        params.unshift(formItem.value)
        // message
        params.push(errorMsg)

        return rules[_rule].apply(formItem, params)
      })
    })
  }

  validate() {
    // Call all validation functions
    for (const fn of this.cache) {
      const errorMsg = fn()

      if (errorMsg) {
        return errorMsg
      }
    }
  }
}

export default Validator
Copy the code

Next we need to write the business logic:

<script type="module">
  import Validator from './validator.js'

  const validatorFn = (registerForm) = > {
    const validator = new Validator()

    validator.add(registerForm.userName, [
      {
        rule: 'isNonEmpty'.errorMsg: 'User name cannot be empty'}, {rule: 'minLength:6'.errorMsg: 'Username length cannot be less than 10 characters',
      },
    ])

    validator.add(registerForm.password, [
      {
        rule: 'minLength:6'.errorMsg: 'Password length must not be less than 6 characters',
      },
    ])

    validator.add(registerForm.phoneNumber, [
      {
        rule: 'isMobile'.errorMsg: 'Incorrect format of mobile number',}])return validator.validate()
  }

  const registerForm = document.getElementById('registerForm')

  registerForm.onsubmit = function (e) {
    e.preventDefault()

    const errorMsg = validatorFn(this)

    if (errorMsg) {
      alert(errorMsg)
      return false
    }

    alert('success')
  }
</script>
Copy the code

Now the code is more maintainable, and it doesn’t matter if we add new form items, we just add them in, and we’re done, but if we do it the same way we did before, we might need to write a lot of if/else, and obviously the code is not maintainable, This way of adding rules in the form of configuration without having to worry about specific validation methods in the business logic is an embodiment of the policy pattern, which seems vague right…

So, what exactly is a strategic pattern?

Policy pattern concepts and usage scenarios

concept

With my personal understanding of a word summary is: to achieve the goal, by hook or by crook, no matter what way!

Let’s start with the first word, “strategy.” In this example, all the rules in the Rules object are policies. Think about why we have policies. Look at the Validator class above. And for this purpose, there are various strategies. To quote from JavaScript Design Patterns and Practices:

The strategy pattern refers to defining a set of algorithms and encapsulating them one by one. Separating the immutable from the changing is the theme of every design pattern, and policy patterns are no exception. The purpose of policy patterns is to separate the use of algorithms from the implementation of algorithms.

The strategy pattern has two parts:

  • A group of policy classes: YesrulesObject, each method is a policy.
  • A Context class that accepts consumer requests and delegates them to a policy, corresponding to the one aboveValidator

Now that you’ve at least figured out how it works, what about scenarios?

Usage scenarios

Policy patterns are often used when you find that your business logic requires a lot of judgment in order to achieve a certain goal. In addition to the form validation example above, there are many other application scenarios.

Mobile payment

At present, almost all our consumption is paid by scanning code, but there are many ways of payment, some people like alipay, some people use wechat, and some use UnionPay…

When you think of multiple ways to do one thing, you naturally think of the strategy model. “Scan payment” is a goal, and “payment method” is a set of strategies.

Way to travel

There may be many ways for us to travel on a daily basis. To achieve this goal, we will choose the following ones:

  • walk
  • Ride a bike
  • Ride electric
  • Driving a car
  • Take a plane

These different modes of travel are where strategic patterns come in.

Policy pattern in Vue source code application

The above case may be actual combat with less, we usually are sure to use the framework for development, I take Vue 2.x as an example, to speak about it in the source of the use of strategy mode to deepen our understanding.

As you probably know, new Vue is passed an option object, or mixins are used to add attributes to a component:

new Vue({
    mixins: [].data: {
        // ...
    },
    methods: {
       // ...}})Copy the code

These different rules are actually the policies in the strategy mode, also called “algorithm”. Users do not need to worry about the internal implementation, we just need to install the industry to write options, let’s look at the specific implementation.

First, the _init method is called internally when new Vue does some initialization:

src/core/instance/index.js

function Vue (options) {
  // The Vue constructor can only be called with new
  if(process.env.NODE_ENV ! = ='production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')}// Call init
  this._init(options)
}
Copy the code

The _init method has one logic to handle the incoming configuration:

src/core/instance/init.js

export function initMixin(Vue: Class<Component>) {
  Vue.prototype._init = function (options? :Object) {
    // Record the vUE instance
    const vm: Component = this;

    // Merge options in the Vue constructor with options passed by the user
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor), // Get the internal configuration of the vue instance, which was declared in vue. Options before new
        options || {}, // Incoming configurationvm ); }}Copy the code

Note that mergeOptions is a key method that merges the incoming configuration with the existing one, using its own internal “algorithm.” This method in the SRC/core/util/options, js, below is the core of its logic:

export function mergeOptions(
  parent: Object,
  child: Object, vm? : Component) :Object {
  // From this we can see that functions can be accepted in both component and root instances
  if (typeof child === "function") {
    child = child.options;
  }
  
  // Unify the external props, inject, and directives into an internal format
  normalizeProps(child, vm);
  normalizeInject(child, vm);
  normalizeDirectives(child);


  // Recursively merge mixins and extends options onto parent
  if(! child._base) {if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm);
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm); }}}// Create a normal object, this is the final configuration returned
  const options = {};
  let key;
  
  // Iterate over all the properties of the parent and merge the configuration into the options object using the algorithm in the 'mergeField' method
  for (key in parent) {
    mergeField(key);
  }
  // Iterate over all attributes of the child that do not exist in the parent and merge the configuration into the options object using the algorithm in the 'mergeField' method
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key);
    }
  }
  // Policy mode merge: Select a different way to merge for each configuration
  function mergeField(key) {
    const strat = strats[key] || defaultStrat;
    options[key] = strat(parent[key], child[key], vm, key);
  }
  return options;
}
Copy the code

The first is a standardization of the incoming configuration, mainly the props, Inject, and directives properties are unified and converted internally into one format:

  normalizeProps(child, vm);
  normalizeInject(child, vm);
  normalizeDirectives(child);
Copy the code

We then recursively merge the extends and mixins from the incoming configuration into parent:

    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm);
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm); }}Copy the code

Then enter the most important step, using the policy mode to configure the parent and child merge:

  let key
  // The next two iterations merge the configuration into the options object using the algorithm in the 'mergeField' method
  for (key in parent) {
    mergeField(key);
  }

  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key);
    }
  }
  
  // Policy mode merge
  function mergeField(key) {
    const strat = strats[key] || defaultStrat; // If there is only one attribute to merge, use 'defaultStrat' which is the default policy
    options[key] = strat(parent[key], child[key], vm, key);
  }
Copy the code

Let’s review that strategic patterns generally fall into two broad categories:

  • A set of policy classes: here’s the correspondingstratsdefaultStrat(Default policy).
  • A Context class: corresponding to the one abovemergeField.

Strats defines how each configuration should be merged, i.e. the specific algorithms defined in the current file (SRC /core/util/options.js). For lack of space, I won’t list all the merged strategies, so here are a few rules.

Merge strategy for lifecycle functions

The strategy first iterates through all the health hooks and then merges them using mergeHook’s algorithm:

/** * lifecycle merge strategy * Hooks and props are merged as arrays. */
function mergeHook(
  parentVal: ?Array<Function>,
  childVal: ?Function|?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
      ? childVal
      : [childVal]
    : parentVal;
  return res ? dedupeHooks(res) : res;
}

LIFECYCLE_HOOKS.forEach((hook) = > {
  strats[hook] = mergeHook;
});
Copy the code

LIFECYCLE_HOOKS are defined in SRC /shared/constants.js, which enumerates all the LIFECYCLE_HOOKS in Vue:

export const LIFECYCLE_HOOKS = [
  'beforeCreate'.'created'.'beforeMount'.'mounted'.'beforeUpdate'.'updated'.'beforeDestroy'.'destroyed'.'activated'.'deactivated'.'errorCaptured'.'serverPrefetch'
]
Copy the code

Let’s take a look at the mergeHook implementation. Its core logic is a very complex ternary operator, but we can translate it into the following:

  let res
  If the hook value does not exist in child, return the value of parent
  if(! childVal) { res = parentVal// parentVal is still a function at first, but after 'dedupeHooks' it returns an array
  } else {
    if (parentVal) {
      // When the parent and child hooks have values, merge the values into an array
      res = parentVal.concat(childVal);
    } else {
      // Convert the child to an array
      res = Array.isArray(childVal) ? childVal : [childVal]
    }
  }
  
  // Queue
  return res ? dedupeHooks(res) : res;
Copy the code

Which means that the hooks are executed when the lifecycle is triggered when using mixins, and that we can write hooks without mixins:

export default {
  created: [
    function () {
      console.log(1)},function () {
      console.log(2)},]}Copy the code

Of course the actual development or don’t mess with ah…

Data /provide merge strategy

When you use extend or mixin, it’s common to merge configurations. The most common is data. What’s the merge strategy for data?

strats.data = function (parentVal: any, childVal: any, vm? : Component): ?Function {
  // pass to mergeDataOrFn
  return mergeDataOrFn(parentVal, childVal, vm);
};

export function mergeDataOrFn(parentVal: any, childVal: any, vm? : Component): ?Function {
    return function mergedInstanceDataFn() {
      // The next two steps are to get the data object. If data is a function, call the function and return the object
      const instanceData =
        typeof childVal === "function" ? childVal.call(vm, vm) : childVal;
      const defaultData =
        typeof parentVal === "function" ? parentVal.call(vm, vm) : parentVal;
    
      // The value returned by child does not exist, use the value of parent, otherwise merge the two objects
      if (instanceData) {
        return mergeData(instanceData, defaultData);
      } else {
        returndefaultData; }}; }Copy the code

As you can see, data does some processing before merging. Except for the root instance, data is usually a function, so we need to call it first, get the returned object, and then call mergeData to merge:

function mergeData(to: Object.from:?Object) :Object {
  // There is no direct return
  if (!from) return to;
  // key - the current attribute to merge, toVal - the old value, fromVal - the value to merge
  let key, toVal, fromVal;
 
  // Reflect. OwnKeys iterates over all attributes of the object to be merged
  const keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from);

  for (let i = 0; i < keys.length; i++) {
    key = keys[i];
    // in case the object is already observed...
    // Skip the comparison in case the property is already set to reactive (this shows how serious the author is)
    if (key === "__ob__") continue;
    // Get the value of the current loop
    toVal = to[key];
    fromVal = from[key];
    
    // The target object does not have the key of the object to be merged
    if(! hasOwn(to, key)) {$set = Vue. Set/this.$set
      set(to, key, fromVal);
    } else if (
      // Both keys exist and the corresponding value is an object, then recursive comparison
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      mergeData(toVal, fromVal);
    }
  }
  
  return to;
}
Copy the code

As can be seen from the above, data merge rules are as follows:

  • In order totoAs the merge is targeted, willfromThe properties on the object are merged totoOn the object
  • fromThere is no return without processingto
  • traversefromAll of the properties
    • toWithout this attribute, usesetTo set it to a reactive property
    • tofromThe corresponding values are all objects, then the recursive processing
    • For other casestoIs not processed (e.gtofromIs not an object and is not equal.)

The visible code is rigorous and worth studying.

In addition to the two strategies mentioned above, the combination strategy of other attributes is also worth learning, if you are interested in it can continue to explore, I believe that here you have a further understanding of the strategy pattern.

conclusion

This paper starts with the introduction of the form validation scenario, using two different ways to solve the problem of this scenario, one is the more common if/else, the other is the strategic mode, through comparison to highlight the advantages of the strategic mode, and then further introduces the concept of the strategic mode and its application scenarios. Finally, through an in-depth understanding of Vue source code to understand the application of the strategy pattern. Of course, switch/case can also optimize the branching process, but it does not solve the essential problem, and the maintenance is still poor when a large number of branches are involved.

Of course, the strategy mode also has disadvantages, such as a lot of policy, the strategy class is very large (JS is better, with a common object to describe the class can be), so need to use according to the actual scene, I hope this article is helpful to you.

reference

[1] Zeng Tan.JavaScript Design Patterns and Practice: Posts and Telecommunications Press,2015