Don't know how to improve code quality?Take a look at these design patterns!

Keywords: calculator axios github Programming

Purpose of improving code quality

Programming apes do their job of writing code. Writing high-quality code should be our pursuit and our own requirement because:

  1. High quality code often means fewer BUG s, better modularity, and is the basis for our scalability and reusability
  2. High quality code also means better writing, better naming, and maintenance

What code is good quality

There are many standards in the industry on how to define "good" code quality. This paper holds that good code should have the following characteristics:

  1. Code is clean, such as indentation, and there are now many tools that can automate this, such as eslint.
  2. The structure is tidy, there is no long structure, the function is split reasonably, there won't be a function of thousands of lines, and there won't be dozens of if...else.This requires code writers to have some optimization experience, and this article describes several modes to optimize these situations.
  3. Read it well and don't have a bunch of names like a, B and C. Instead, try to be as semantical as possible. Variable and function names are as meaningful as possible. It's best to have code as a comment so that people can see what your code is doing.

The main design modes described here are strategy/status mode, appearance mode, iterator mode, and memo mode.

Policy/Status Mode

Basic structure of policy patterns

If we need to be a calculator, we need to support addition, subtraction, multiplication and division. In order to determine what users need to do, we need four if...elses to make a judgment. If we need more, if...else will be longer, not good for reading, and not elegant for viewing.So we can use the strategy pattern to optimize the following:

function calculator(type, a, b) {
  const strategy = {
    add: function(a, b) {
      return a + b;
    },
    minus: function(a, b) {
      return a - b;
    },
    division: function(a, b) {
      return a / b;
    },
    times: function(a, b) {
      return a * b;
    }
  }
  
  return strategy[type](a, b);
}

// When in use
calculator('add', 1, 1);

The code above replaces multiple if...else with an object, and all the operations we need correspond to a property inside the object. This property name corresponds to the type we passed in. We can get the corresponding operation by using this property name directly.

State Mode Basic Structure

State and policy modes are similar, and there is an object that stores some policies, but there is also a variable that stores the current state, and we get specific actions based on the current state:

function stateFactor(state) {
  const stateObj = {
    status: '',
    state: {
      state1: function(){},
      state2: function(){},
    },
    run: function() {
      return this.state[this.status];
    }
  }
  
  stateObj.status = state;
  return stateObj;
}

// When in use
stateFactor('state1').run();

If...else actually changes the behavior of the code according to different conditions, while policy mode and state mode can change the behavior according to the incoming policy or state, so we can use these two modes instead of if...else.

Example: Access permissions

The requirement of this example is that our pages need to render different content according to different roles, if we write in if...else:

// There are three modules to display and different roles should see different modules
function showPart1() {}
function showPart2() {}
function showPart3() {}

// Get the current user's role and decide which parts to display
axios.get('xxx').then((role) => {
  if(role === 'boss'){
    showPart1();
    showPart2();
    showPart3();
  } else if(role === 'manager') {
    showPart1();
    showPart2();
  } else if(role === 'staff') {
    showPart3();
  }
});

In the above code, we get the current user's role through API requests, and then a bunch of if...elses to determine which modules should be displayed. If there are many roles, if...else may be very long here, we can try to optimize with state mode:

// Wrap roles into a ShowController class first
function ShowController() {
  this.role = '';
  this.roleMap = {
    boss: function() {
      showPart1();
      showPart2();
      showPart3();
    },
    manager: function() {
      showPart1();
    	showPart2();
    },
    staff: function() {
      showPart3();
    }
  }
}

// Add an instance method show on ShowController to show different content based on role
ShowController.prototype.show = function() {
  axios.get('xxx').then((role) => {
    this.role = role;
    this.roleMap[this.role]();
  });
}

// When in use
new ShowController().show();

The code above rewrites the access module through a state mode, removing if...else, and encapsulating the different roles in the roleMap makes it much easier to add or reduce them later.

Example: compound motion

The need for this example is that we now have a ball and we need to control its movement, either up and down or in a combination of upper left and lower right movements.If we also write in if...else, this will end up:

// First come basic motion in four directions
function moveUp() {}
function moveDown() {}
function moveLeft() {}
function moveRight() {}

// The specific method of moving can accept one or two parameters, one is the basic operation, the two parameters are upper left, lower right such operations
function move(...args) {
  if(args.length === 1) {
    if(args[0] === 'up') {
      moveUp();
    } else if(args[0] === 'down') {
      moveDown();        
    } else if(args[0] === 'left') {
      moveLeft();        
    } else if(args[0] === 'right') {
      moveRight();        
    }
  } else {
    if(args[0] === 'left' && args[1] === 'up') {
      moveLeft();
      moveUp();
    } else if(args[0] === 'right' && args[1] === 'down') {
      moveRight();
      moveDown();
    }
    // There are many if...
  }
}

You can see here if...else can see that we're all big heads, so let's optimize with a strategy pattern:

// Create a mobile control class
function MoveController() {
  this.status = [];
  this.moveHanders = {
    // Write the method for each instruction
    up: moveUp,
    dowm: moveDown,
    left: moveLeft,
    right: moveRight
  }
}

// MoveController adds an instance method to trigger a movement
MoveController.prototype.run = function(...args) {
  this.status = args;
  this.status.forEach((move) => {
    this.moveHanders[move]();
  });
}

// When in use
new MoveController().run('left', 'up')

The above code also encapsulates all the strategies in the moveHanders and executes the specific policies using the method run was passed in.

Appearance Mode

Basic structure

When we design a module, the internal methods may be designed in detail, but when exposed to external use, these small interfaces may not have to be exposed directly. External users may need to combine some of the interfaces to implement a function. We can actually organize this when exposed.It's like a menu in a restaurant, with many dishes. Users can order one dish at a time or directly from a set of meals. The appearance pattern provides a similar organized set of meals:

function model1() {}

function model2() {}

// It provides a higher-level interface that combines model1 and model2 for external use
function use() {
  model2(model1());
}

Example: Common interface encapsulation

Appearance patterns are very common, many modules are complex inside, but the external interface may be one or two. We don't need to know the complex internal details, just call a unified advanced interface, such as the following tab module:

// A tab class that may have multiple sub-modules inside
function Tab() {}

Tab.prototype.renderHTML = function() {}    // Submodule of Rendering Page
Tab.prototype.bindEvent = function() {}    // Submodule of Binding Events
Tab.prototype.loadCss = function() {}    // Load Style Submodules

// There is no need to expose the specific sub-modules above, just an advanced interface.
Tab.prototype.init = function(config) {
  this.loadCss();
  this.renderHTML();
  this.bindEvent();
}

This encapsulation mode of the above code is very common and actually uses the appearance mode. Of course, it can also expose specific renderHTML, bindEvent, loadCss sub-modules, but external users may not care about these details, just give a uniform advanced interface, which is equivalent to exposing a change in appearance, so it is called the appearance mode.

Example: Method Encapsulation

It is also common to encapsulate similar functions into a single method, rather than writing them everywhere.In the past, when IE was still dominant, we needed to do a lot of compatible work, just one binding event had addEventListener, attachEvent,onclick, etc. To avoid these checks every time, we could encapsulate them as a method:

function addEvent(dom, type, fn) {
  if(dom.addEventListener) {
    return dom.addEventListener(type, fn, false);
  } else if(dom.attachEvent) {
    return dom.attachEvent("on" + type, fn);
  } else {
    dom["on" + type] = fn;
  }
}

We then expose the addEvent for external use, which we often encapsulate when encoding, but we may not be aware that it is a form factor.

Iterator mode

Basic structure

Iterator mode mode is very common in JS. forEach, which comes with arrays, is an application of iterator mode, and we can achieve a similar function:

function Iterator(items) {
  this.items = items;
}

Iterator.prototype.dealEach = function(fn) {
  for(let i = 0; i < this.items.length; i++) {
    fn(this.items[i], i);
  }
}

In the code above, we create a new iterator class, the constructor receives an array, and the instance method dealEach receives a callback that executes on each item of the items on the instance.

Example: Data Iterator

In fact, many native methods of JS arrays use iterator mode, such as find, find, which receives a test function and returns the first data that matches the test function.The purpose of this example is to extend this functionality by returning all data items that match the test function, but also by receiving two parameters, the first parameter being the property name and the second parameter being the value, all items whose properties match the value are returned as well:

// The outer layer is encapsulated in a factory mode and calls are made without writing new
function iteratorFactory(data) {
  function Iterator(data) {
    this.data = data;
  }
  
  Iterator.prototype.findAll = function(handler, value) {
    const result = [];
    let handlerFn;
    // Processing parameters, if the first parameter is a function, use it directly
    // Give a default function to compare if it's not a function or an attribute name
    if(typeof handler === 'function') {
      handlerFn = handler;
    } else {
      handlerFn = function(item) {
        if(item[handler] === value) {
          return true;
        }
        
        return false;
      }
    }
    
    // Loop each item in the data, inserting the matching result into the result array
    for(let i = 0; i < this.data.length; i++) {
      const item = this.data[i];
      const res = handlerFn(item);
      if(res) {
        result.push(item)
      }
    }
    
    return result;
  }
  
  return new Iterator(data);
}

// Write a data test
const data = [{num: 1}, {num: 2}, {num: 3}];
iteratorFactory(data).findAll('num', 2);    // [{num: 2}]
iteratorFactory(data).findAll(item => item.num >= 2); // [{num: 2}, {num: 3}]

The above code encapsulates an iterator similar to the array find, extending its capabilities and is well suited to handle a large number of structurally similar data returned by the API.

Memento

Basic structure

The memo mode is similar to the cache function commonly used by JS, in that it records a state, which is the cache, and can take the cached data directly when we access it again:

function memo() {
  const cache = {};
  
  return function(arg) {
    if(cache[arg]) {
      return cache[arg];
    } else {
      // Execute method before caching, get result res
      // Then write res to the cache
      cache[arg] = res;
      return res;
    }
}

Example: Article Cache

This example is also common in real-world projects where users need to request data from the API every time they click on a new article. If they click on the same article next time, we may want to use the data directly from the last request instead of requesting it again. We can use our memo mode now, just use the above structure:

function pageCache(pageId) {
  const cache = {};
  
  return function(pageId) {
    // To keep the return types consistent, we all return a Promise
    if(cache[pageId]) {
      return Promise.solve(cache[pageId]);
    } else {
      return axios.get(pageId).then((data) => {
        cache[pageId] = data;
        return data;
      })
    }
  }
}

The above code uses a memo pattern to solve this problem, but the code is simpler and the requirements in the actual project may be more complex, but this idea can be referred to.

Example: Forward and backward functionality

The requirement of this example is that we need to make a movable DIV, and the user can move the DIV freely, but sometimes he may misoperate or repent and want to move the DIV back, that is, to put the state back to the previous one, to have the requirement of the fallback state, and of course the requirement of the paired forward state.Similar needs can be met using a memo model:

function moveDiv() {
  this.states = [];       // An array records all States
  this.currentState = 0;  // A variable records the current state location
}

// Move method, record status per move
moveDiv.prototype.move = function(type, num) {
  changeDiv(type, num);       // Pseudocode, move DIV specific operation, not implemented here
  
  // Record this operation into states
  this.states.push({type,num});
  this.currentState = this.states.length - 1;   // Change current state pointer
}

// Forward method, take out state execution
moveDiv.prototype.forward = function() {
  // If this is not the last state
  if(this.currentState < this.states.length - 1) {
    // Take out the state of progress
    this.currentState++;
    const state = this.states[this.currentState];
    
    // Execute this status location
    changeDiv(state.type, state.num);
  }
}

// The backward approach is similar
moveDiv.prototype.back = function() {
  // If this is not the first state
  if(this.currentState > 0) {
    // Remove Backward Status
    this.currentState--;
    const state = this.states[this.currentState];
    
    // Execute this status location
    changeDiv(state.type, state.num);
  }
}

The above code records all the states that the user has operated on through an array, allowing the user to move forward and backward between states at any time.

summary

These design mode strategies/state modes, appearance modes, iterator modes, and memo modes described in this article are all well understood and very common in practice. Being familiar with them can effectively reduce redundant code and improve the quality of our code.

  1. Policy mode reduces the number of if...else s by rewriting our if criteria to one-by-one strategies, making them look fresher and easier to expand.The state mode is similar to the policy mode, but there is one more state, which can be used to select a specific policy.
  2. Appearance patterns may have been used unintentionally by encapsulating some of the module's internal logic within a more advanced interface or by encapsulating similar operations within a method to make external calls more convenient.
  3. Iterator mode has many implementations on JS arrays, and we can imitate their data processing work, especially for handling large amounts of structurally similar data fetched from API s.
  4. Memo mode is the addition of a cached object to record the status of previously acquired data or operations, which can then be used to speed up access or roll back status.
  5. Again, the key point of design mode is to understand ideas, which can be achieved in a variety of ways.

This is the last article on design patterns. The first three articles are:

(500 + Zan!)Don't know how to encapsulate code?Look at these design patterns!

(100 + Zan!) Design patterns in framework source code to improve scalability

Don't know how to improve code reuse?Take a look at these design patterns

At the end of the article, thank you for taking the time to read it. If this article gives you some help or inspiration, don't stint on your approval of GitHub Star. Your support is the driving force behind the author's continued creation.

The material for this article comes from NetEase Advanced Front End Development Engineer Microspecialty Teacher Tang Lei's design mode course.

Author blog GitHub project address: https://github.com/dennis-jiang/Front-End-Knowledges

Summary of Author's Digging Articles: https://juejin.im/post/5e3ffc85518825494e2772fd

Posted by kcorless on Fri, 05 Jun 2020 17:20:40 -0700