Business background

There is a business scenario that can be summarized as: collect some information about a user, such as the user’s name, address, bankcard information, etc., through a form page. The usual way to do this is to create a form page, list the relevant fields, and do a for loop in the view layer to display the corresponding fields. Here is a code demonstration using Element UI.

The business requirements described above can be implemented with the following code:

// App.vue
<template>
  <div id="app">
    <el-form :model="userInfo" :rules="rules" ref="userInfo" label-width="100px" label-position="top">
      <template v-for="item in formFields">
        <el-form-item :key="item.id" :label="item.label" :prop="item.id">
          <el-input v-model="userInfo[item.id]" :placeholder="item.label"></el-input>
        </el-form-item>
      </template>
      <el-form-item>
        <el-button type="primary" @click="saveDate('userInfo')">save</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  name: 'APP',
  data () {
    return {
      userInfo: {
        name: ' '.address: ' '.cardNumber: ' '
      },
      formFields: [{id: 'name'.label: 'name'.type: 'input'
        },
        {
          id: 'address'.label: 'address'.type: 'input'
        },
        {
          id: 'cardNumber'.label: 'Bank Card Number'.type: 'input'}].rules: {
        name: [{required: true.message: 'Please enter your name'.trigger: 'blur'}].address: [{required: true.message: 'Please enter your address'.trigger: 'blur'}].cardNumber: [{required: true.message: 'Please enter your bank card number'.trigger: 'blur'}]}}},methods: {
    saveDate (formName) {
      this.$refs[formName].validate((valid) = > {
        if (valid) {
          console.log('submit! ')}else {
          console.log('error submit!! ')
          return false}})}}}</script>
Copy the code

The above code has realized the basic functions of the requirements. If we need to collect other user information in the future, we can directly modify the values of formFields and rules. So far, no matter what information you want to collect, we only need to change two fields to meet your requirements.

Increased demand for products

Sure enough, a few days after the launch of this function, the product needs to add a bank code field this time, this field allows users to choose the bank code corresponding to the bank card, the bank data is provided by the product, written into the code by the front end, for users to choose.

The input type is not enough to satisfy the requirements of the product, but it is not difficult to change. Just add a select type.

As we said above, only two fields, formFields and Rules, need to be modified to meet the requirements of the product, so we will focus on these two fields. FormFields is the core field of our feature, and all changes are made around it.

HTML section modification:

<template v-if="item.type === 'input'">
    <el-input v-model="userInfo[item.id]" :placeholder="item.label">
    </el-input>
</template>
<template v-if="item.type === 'select'">
    <el-select v-model="userInfo[item.id]" :placeholder="item.label">
        <el-option
            v-for="son in item.list"
            :key="son.value"
            :label="son.label"
            :value="son.value">
        </el-option>
    </el-select>
</template>
Copy the code

JS part modification:

// Add the bankList array to script
const bankList = [
    { label: Agricultural Bank of China.value: '001' },
    { label: Industrial and Commercial Bank of China.value: '002'}]// Add bankList in formFields and change type to select
{
    id: 'bankCode'.label: 'Bank code'.type: 'select'.list: bankList
}

// Add in the rules field
bankCode: [{required: true.message: 'Please select bank code'.trigger: 'change'}]Copy the code

So far, we’ve only added a few lines of code and quickly completed the requirements for the product. If it is input, display the input component. If it is SELECT, display the SELECT component. If we need to add a date component in the future, multi-select components. The list property in formFields holds the data needed for the dropdown box. If you add other dropdown fields, you can simply add the data to the list.

Is that where the demand ends? No!

Demand for products is increasing again

Sure enough, a few days after the launch of this function, the product needs to add a field of mobile phone number, mobile phone number to default display area code prefix, such as +86, and users can not modify, save the mobile phone area code to put in, modify the area code will be hidden.

This is not a matter of a few lines of code, but we found that in addition to changing the two fields mentioned above, we also had to make adjustments to the HTML section. HTML section modification:

// Add prefixed logic to the input component
<el-input v-model="userInfo[item.id]" :placeholder="item.label">
+    <template v-if="item.prefix" slot="prepend">{{item.prefix}}</template>
</el-input>
Copy the code

Changes to the JS part:

// formFields is added
{
    id: 'phoneNumber'.label: 'Mobile phone Number'.type: 'input'.prefix: '+ 86'
}

// Add the rules field
phoneNumber: [{required: true.message: 'Please enter your mobile phone number'.trigger: 'blur'}]Copy the code

So far, we’ve only added a few lines of code and quickly completed the requirements for the product. If the requirements of the product were to end there, the code would be enough, not to mention the design patterns.

Junk code resulting from requirements changes

If the requirements for this form hadn’t changed so much, we would have followed the above approach and added a few lines of code to complete the product requirements. Such as product may be added after the phone number field, and puts forward the function of group users, the user is divided into 2 AB group, asked A group of users mobile number prefix, the bank code field and B group of the user’s mobile phone number prefix, bank code, then we may want to only 2 users group, do A the if the else judgment not to go, So we added a judgment on the basis of the original code to complete the requirements of the product.

A few days later, the product came to us again and said that the bank code selected by some users did not match the bank card number entered by the users, so it required us to do a verification. The bank card number was checked according to different bank codes, so we added a custom verification method to the Rules field.

From the above requirements changes, we can see that even a simple form page can become more and more bloated as the product requirements change and the page becomes more and more if and else.

The following code might be the way we see it most often:

// App.vue
<script>

const bankListA = [
  { label: People's Bank of China.value: '001' },
  { label: Industrial and Commercial Bank of China.value: '002'}]const bankListB = [
  { label: '(003) China Construction Bank '.value: '003' },
  { label: Commercial Bank of China.value: '004'}]export default {
  name: 'APP',
  data () {
    const checkCardNumber = (rule, value, callback) = > {
      const code = this.userInfo.bankCode
      if(! code)return
      // This is a simple regex check. The actual check rule may be more complex
      const regs = {
        '001': /^\d{12}$/.// 12 bits long
        '002': /^\d{14}$/.// 14 bits long
        '003': / ^ \ d {8, 12} $/.// 8-12 bit length
        '004': /^\d{10}$/ // 10 bits long
      }
      const reg = regs[code]
      if(! reg.test(value)) { callback(new Error('Please enter the correct bank card number'))}else {
        callback()
      }
    }
    return {
      userInfo: {
        name: ' '.address: ' '.cardNumber: ' '
      },
      userGroup: ' '.formFields: [{id: 'name'.label: 'name'.type: 'input'
        }, {
          id: 'phoneNumber'.label: 'Mobile phone Number'.type: 'input'.prefix: '+ 86'
        },
        {
          id: 'address'.label: 'address'.type: 'input'
        },
        {
          id: 'bankCode'.label: 'Bank code'.type: 'select'.list: []}, {id: 'cardNumber'.label: 'Bank Card Number'.type: 'input'}].rules: {
        name: [{required: true.message: 'Please enter your name'.trigger: 'blur'}].phoneNumber: [{required: true.message: 'Please enter your mobile phone number'.trigger: 'blur'}].address: [{required: true.message: 'Please enter your address'.trigger: 'blur'}].bankCode: [{required: true.message: 'Please select bank code'.trigger: 'change'}].cardNumber: [{required: true.message: 'Please enter your bank card number'.trigger: 'blur' },
          { validator: checkCardNumber, trigger: 'blur'}]}}},methods: {
    initFields () {
      this.userGroup = sessionStorage.getItem('userGroup') | |'B' // Emulation gets user group information from the interface
      const indexBankCode = this.formFields.findIndex(item= > item.id === 'bankCode')
      const indexPhone = this.formFields.findIndex(item= > item.id === 'phoneNumber')
      // Handle the banking code list and the mobile phone thing prefix
      if (this.userGroup === 'A') {
        this.formFields[indexBankCode].list = bankListA
        this.formFields[indexPhone].prefix = '+ 86'
      } else {
        this.formFields[indexBankCode].list = bankListB
        this.formFields[indexPhone].prefix = '+ 57'
      }
    },
    saveDate (formName) {
      this.$refs[formName].validate((valid) = > {
        if (valid) {
          console.log('submit! ')}else {
          console.log('error submit!! ')
          return false
        }
      })
    }
  },
  mounted () {
    this.initFields()
  }
}
</script>
Copy the code

In the above code, the initFields method handles the field data required by different groups of users. If the product requirement is changed to group A users only display the name and cardNumber fields, and user B displays the same fields, It is likely that we will continue to do formFields in the initFields method, which will become heavier and heavier, with a lot of if and else code as the business needs change.

In addition, we found that if A group A user requests this page, the group B user’s data is not required, and vice versa. As a result, if the bankList has a lot of data, the size of the code will increase and the initial loading speed of the page will be affected.

Product demand is increasing again…

Sure enough, a few days after the launch of this feature, demand for the product came back, this time directly put a big move. The requirements look like this:

  1. Add multiple user groups. The mobile phone prefixes of users in different groups should display their corresponding group area codes (for example, the user countries may be different).
  2. The bank code list of different groups is different, and the front end controls and displays the corresponding bank list;
  3. Different groups display different fields, such as group A only display name, mobile phone number, bank card number, group B display name, bank code list, bank card number, group C display name, address, phone number, group D display name, city, address, phone number;
  4. Different groups of mobile phone number, bank card, name, address and other authentication rules are not the same;
  5. Multilanguage support;

At this point, we can no longer modify the original code base, if we still develop on the basis of the original code, then the code is very complicated, and it is estimated that if else will fly all over the place.

Find problems with the current code implementation

We now have a new understanding of the business requirements, and while the requirements are much more complex than when they were first proposed, they are essentially the same form submission with a little more logic.

Since the essence of the business has not changed, it is essential to refactor the code by combing through the business requirements and abstracting the current code to find the parts that have changed and the parts that have not changed.

Through combing, we find that the dependence point of change is the user’s group: different groups -> lead to different formFields -> different formFields lead to different verification rules; The similarities are business logic: get the fields required by the form -> verify the values of the fields -> verify by submitting the form. This is a simple abstract process.

Design patterns emerge

Now that you know what the problem is, start fixing it. Separating invariant parts from changing parts is the theme of every design pattern. Let’s introduce two design patterns: template method pattern and policy pattern:

The template method pattern consists of two parts, the first part is the abstract parent class, the second part is the concrete implementation child class.

The definition of a strategy pattern is to define a series of algorithms, encapsulate them one by one, and make them interchangeable.

Different users display different fields, and the changes are in that field, so we can abstract out a template that we can use only to get the values we need to get the form, validate the values, and submit the form. For each different group, I put it in a separate file and conducted specific business processing through the strategy mode. This is also the purpose of the policy pattern: to separate the use of algorithms from their implementation.

Here is the refactored directory structure

// Directory structure
src
  |--group // store the processing methods of different groups
    |--a-list.js
    |--a.js
    |--b.js
  App.vue
  main.js
Copy the code

We separate the data processing methods of each group and store them in the group folder, while app.vue serves as a template to handle the common business logic.

First look at the code for app.vue

// App.vue
<template>
  <div id="app">
    <el-form :model="userInfo" :rules="rules" ref="userInfo" label-width="100px" label-position="top">
      <template v-for="item in formFields">
        <el-form-item :key="item.id" :label="item.label" :prop="item.id">
          <template v-if="item.type === 'input'">
              <el-input v-model="userInfo[item.id]" :placeholder="item.label">
                <template v-if="item.prefix" slot="prepend">{{item.prefix}}</template>
              </el-input>
          </template>
          <template v-if="item.type === 'select'">
              <el-select v-model="userInfo[item.id]" :placeholder="item.label">
                <el-option
                  v-for="son in item.list"
                  :key="son.value"
                  :label="son.label"
                  :value="son.value">
                </el-option>
              </el-select>
          </template>
        </el-form-item>
      </template>
      <el-form-item>
        <el-button type="primary" @click="saveDate('userInfo')">save</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
import { getField } from './group/index'

export default {
  name: 'APP',
  data () {
    return {
      userInfo: {},
      userGroup: ' '.formFields: [].rules: {}}}.methods: {
    // Get the form fields required by group users
    async initFields () {
      this.userGroup = sessionStorage.getItem('userGroup') | |'B' // Emulation gets user group information from the interface
      let fields = null
      try {
        // Use the policy mode to get the corresponding form field
        fields = await getField(this.userGroup)
      } catch {
        console.log('Load failed')}this.initData(fields)
      this.formFields = fields
    },

    // Set userInfo and rules
    initData (fields) {
      const info = {}
      fields.forEach(item= > {
        info[item.id] = ' '
        if (item.rule) {
          /* Element UI's custom validation function can't take arguments, so we need to modify the validation function */ to be able to access the current form data within the validation function
          if (Array.isArray(item.rule)) {
            item.rule.forEach(son= > {
              if (typeof son.validator === 'function') {
                son.validator = son.validator.bind(this.userInfo)
              }
            })
          }
          this.rules[item.id] = item.rule
        }
      })
      // Assign to userInfo to achieve bidirectional binding
      this.userInfo = info
    },

    // Submit data
    saveDate (formName) {
      this.$refs[formName].validate((valid) = > {
        if (valid) {
          console.log('submit! ')}else {
          console.log('error submit!! ')
          return false
        }
      })
    }
  },
  mounted () {
    this.initFields()
  }
}
</script>
Copy the code

In the above code we only define userInfo, formFields, and rules, but do not actually assign values. Using initFields, we get the fields required by the form, and use initData to assign values to other data.

We’ve also introduced an external method called getField. What does the group/index.js file do

// group/index.js
/** * Get the corresponding form field * from the user group@param {String} UserGroup userGroup *@returns Array* /
export const getField = async function (userGroup) {
  const group = userGroup.toLocaleLowerCase()
  const file = await require(`. /${group}`)
  const { formFields } = file
  return formFields
}

Copy the code

The above code achieved through group users access to the corresponding form the data required for the function, so that we can implement the algorithm and the algorithm implementation of separation, after another new user groups, we only need to add new js in the group directory files, old business won’t be affected, the realization of the new requirements also can better and more convenient.

Let’s take a look at the code in A.js

// a.js
import { bankList } from './a-list'

export const formFields = [
  {
    id: 'name'.label: 'name'.type: 'input'
  }, {
    id: 'phoneNumber'.label: 'Mobile phone Number'.type: 'input'.prefix: '+ 86'
  }, {
    id: 'bankCode'.label: 'Bank code'.type: 'select'.list: bankList
  }, {
    id: 'cardNumber'.label: 'Bank Card Number'.type: 'input'.rule: [{required: true.message: 'Please enter your bank card number'.trigger: 'blur' },
      { validator: checkCardNumber, trigger: 'blur'}}]]function checkCardNumber (rule, value, callback) {
  const code = this.bankCode
  if(! code)return
  // This is a simple regex check. The actual check rule may be more complex
  const regs = {
    '001': /^\d{12}$/.// 12 bits long
    '002': /^\d{14}$/.// 14 bits long
    '003': / ^ \ d {8, 12} $/.// 8-12 bit length
    '004': /^\d{10}$/ // 10 bits long
  }
  const reg = regs[code]
  if(! reg.test(value)) { callback(new Error('Please enter the correct bank card number'))}else {
    callback()
  }
}
Copy the code

Here we define each field required by the form, and put the validation rules under each field. In the future, we only need to maintain a file like this to realize the new product requirements, isn’t the original version of the code much more convenient?

Rewritten code problems

The refactored code is much simpler than before, but it also shows that there is more redundant code. For example, there are now 20 groups containing the name, phoneNumber fields, We found that we had to maintain 20 identical rule validation rules, and the number of fields multiplied exponentially. Another problem is how to handle the change event of the form field. For example, if a city field is added to the field, the bankCode is not displayed according to the value of city. In addition to the two problems mentioned above, there may be other problems, which are not listed here. For these two problems, the refactored code is still much easier to change than the original code.

For problem 1, redundant code, we can add a list of public fields, which stores the default attributes of each field. We just need to Merge Option, and we don’t need to write the actual code here.

For the second problem, the change event handling problem, it is also very simple, we can define a change function in the group/index.js file and throw this function, and the specific implementation of the change function can be redefined in the grouped JS file. In the app. vue file we just need to execute in the form’s change event.

The main reason for these two questions is that they show the benefits of design patterns in that the refactored code is more flexible and the code for the main business logic is easier to maintain.

Product demand is increasing again…

Sure enough, a few days after this function was launched, the demand of the product came again. Group D was added, and the bankCode data of group D users should be obtained through the interface back end. Group D used another interface at commit time for special reasons. At this time, if it is you, how will you fulfill the product requirements?

conclusion

This article takes a form page as an example to briefly illustrate the benefits of design patterns for optimizing our code. We all use design patterns at some point in our everyday coding, but some of us may not know which one. In addition, we also need to pay attention to how to better understand the product requirements. The product requirements here are not just simple writing business, but are included in various aspects, such as vue.js as a product, as small as a landing page and so on.

We should have the ability to summarize problems, learn to summarize problems, find the current business, technology, products and other pain points, and think a step further. From a technical point of view, is there a problem with our current code? Can it meet the current business implementation? Is there a better solution? If there is a better solution, think about how many implementations there are, can I implement them, what are the costs of each implementation, and what are the benefits of each implementation.

The source code

To run the above code locally, visit github.com/hawuji/desi…