Foreword: What is SOLID design principle and why use it


  • In the field of programming, SOLID (S: single function, O: Open and close principle, L: Richlet substitution, I: interface isolation, D: dependency inversion) refers to the five basic principles of object-oriented design. When applied together, these principles can make software more robust and stable.

  • Why I used it in the first place was because of the fear of product manager requirements, which can be a problem with many front ends, where requirements change very frequently. What I am doing now is the background system. The background system of our group is the most complicated in the whole company. Not only are the problems left over from the system intractable, but the new unreasonable requirements of the product are also continuous. (Our company’s products are the big brother, development has no position)

  • Two words: annoyed

  • Note: This article references a large number of materials, such as the following (if you want to go further) :
    • Javacript Design Patterns and Development Practices
    • Javascript Design Patterns
    • Refactoring: Improving the Design of Existing Code
    • “Javascript Functional Programming”
    • A technical director’s advice: proficient in so many technologies, why still do a bad project? Mp.weixin.qq.com/s/yFxpb8b7B…
    • MDN: Optional operator developer.mozilla.org/zh-CN/docs/…
    • Nuggets article: Front-end defensive programming juejin.cn/post/684490…
    • Geek Time: The Beauty of Design Patterns
    • Ramda official documentation
    • Gold Digging Volume: Javacript design patterns in detail
  • All right, here we go

Single responsibility Principle

  • It states that a class should have only one reason for change

  • The above definition is too broad. How do we define whether a class has a single design responsibility?

  • Whether the responsibility is single depends on whether the business is so complex that the class or function must be split

  • Let me give you an example

// For example, you want to write a user class like this:
// Note that many of these attributes and methods are related to the person's occupation
class UserInfo {
    constructor(name, gender, profession, workingTime, salary){
        this.name = name; / / name
        this.gender = gender;  / / gender
        this.profession = profession; // What is your profession?
        this. WorkingTime = workingTime;// How long have you been working?
        this.salary = salary; // Salary (career related)
    }
    isWorkBusy(){ // Are you busy at work (career-related)
        if(xx){
            return false;
        }
        return true; }}Copy the code
  • Do you think this class meets the single responsibility principle?
  • Some might say that the UserInfo class is supposed to include all attributes and methods related to user information, so this class meets the single responsibility principle. Some people say that the UserInfo class contains a large proportion of a user’s profession, so it can be split into a single UserProfession class, which then refers to an instance of that class.

  • Why do we emphasize the need to look at single responsibilities depending on the business?
  • For example, the requirements of the product will not involve changing UserInfo’s professional information in the next few years, so there is no problem if you don’t split it, because what you are splitting is changing content, and if you don’t change it, there is no need to split it (even if the code is bad, because you are not likely to stay with a company for many years when you are young).
  • For example, if there are a lot of requirements later in the product that involve changing UserInfo’s career information, this information should be packaged separately


Conclusion:

  • The single responsibility principle can be briefly understood as the need to extract content that changes frequently and can be separated into separate classes
  • In a business, we can start by writing a coarse-grained class. When we find that it is far from meeting the business changes, we need to divide the content in the class with finer granularity, and then we need to extract and encapsulate the content
  • This involves two concepts, one is don’t over-design, we are not gods, can’t anticipate the product and market changes, and the other is to continue to refactor, when it needs to be refactor, just refactor

Note: some students may destroy the cohesion of the class for the sake of a single responsibility.
  • For example, have you used native REdux and DVA? Why do you prefer THE DVA design and are not satisfied with the use of REdux? Because redux files are too scattered, DVA cohesion is higher. Let’s take a look at the following simple redux process and see if there are too many files to work with. (Personally, a sign of lack of cohesion is that you need things scattered across files, not in a single class.)

  • Redux/actionType.js

export const ADDNAME = 'ADDNAME'
export const ADDAGE = 'ADDAGE'
Copy the code
  • Redux/actions.js
import { ADDNAME,ADDAGE } from "./action-type";

export const addNameCreater = (name) = >({type:ADDNAME,data:name})
export const addAgeCreater = (age) = > ({type:ADDAGE,data:age})
export const addNameAsync = (name) = >{
    return dispatch= >{
        setTimeout(() = >{
            dispatch(addNameCreater(name))
        },2000); }}Copy the code
  • Redux/reducer.js
import {ADDNAME, ADDAGE} from './action-type'
import {combineReducers} from 'redux'
function addName(state='initRedux',action){ // Parameter default value
    switch(action.type){
        case ADDNAME:
            return action.data
        default:
            return state
    }
}
function addAge(state=0,action){
    switch(action.type){
        case ADDAGE:
            return action.data
        default:
            return state
    }
}
Copy the code

Let’s take a look at dVA processing. Here is a very simple schematic code:

export default {
  namespace: 'todo'.state: {
    list: []},reducers: {
    save(state, { payload: { list } }) {
      return { ...state, list }
    }
  },
  effects: {*addTodo({ payload: value }, { call, put, select }){},},subscriptions: {
    setup({ dispatch, history }) {
      // Listen for route changes and request page data}}}Copy the code
  • Used a redux of the students probably would have the same feeling with me, write a simple reducer, redux/actionType js, redux/action, redux/reducer, and reference project code to switch back and forth, very complicated! We’ll look at the dva processing, the actionType, action, reducer to write together, whether in high cohesion within a lot, write up business code is easier?
  • Some of you said, high cohesion, low coupling, how do you get low coupling with high cohesion, and then we’ll talk about some methods, but I’m just going to talk about one method here, which is a method that depends on a class, a property or a method that depends on another class, and so on, and finally it all depends on abstraction, which is interface oriented programming, This is why I personally find typescript particularly important. Js doesn’t have a native syntax for interfaces.

The single responsibility principle in business: Inverted pyramid — Business logic components need to be presented as an inverted pyramid


  • The bricks that make up the pyramid are, in plain English, components

  • The business logic layer should be designed as a very single-functional widget, with small apis and few lines of code.

  • Due to the single responsibility, there must be a large number of components, each of which corresponds to a very specific business function point (or several similar ones);

  • The system architecture naturally takes on an inverted pyramid shape: the closer you get to the top, the more business scenario components there are, and the lower down, the more reusable components there are.

  • Design patterns related to single responsibility (personal opinion, subject to change with understanding of design patterns)

    • The proxy pattern
    • The bridge model
    • The appearance model
    • Chain of Responsibility model
    • The state pattern
  • For example, how to embody the agent mode mentioned above

  • Higher-order components, which I find somewhat similar to the proxy model

  • For example, if you want to access a component, you need to check whether the higher-order component (a proxy class) meets certain conditions to access the component

  • All right, let’s move on to point two

The Open Closed Principle

  • To put it simply, the open closed principle is that adding a new function should be done by extending the code (adding modules, classes, methods, attributes, etc.) on the basis of existing code, rather than modifying the existing code (modifying modules, classes, methods, attributes, etc.). (Not absolutely, more on that in a moment)
  • Start with a very simple example to get a feel for the benefits of this principle. Rewrite if-else in strategic mode (the second example will be more difficult).
// Notice that there is a method to get user information called getUserData

// There are a lot of decisions here, depending on the urlType parameter, request different interfaces
calss User{
  getUserData(urlType){
    if (urlType === 'a') {
		// to do something
    } else if(urlType = = ='b') {// to do something
    } else {
		// to do something}}}Copy the code
  • If WE’re going to add an if judgment, we tend to add it to the getUserData method, and the problem is that’s process-oriented thinking

  • Object – oriented modification is best done by adding classes and methods, which is object – oriented thinking

  • Why, as we understand it, is that our method is nothing more than a specific function written in code for a business logic

  • So if the business logic changes, let’s not tinker with the old function, because you don’t know how buggy the old logic is

  • It’s important to note that we’re not trying to kill procedural orientation, because plain if-else functions are great, and procedural orientation is great, but what if you can go one step further, encapsulate procedural code and be extensible, and most importantly meet the changing needs of the business?

  • Ok, let’s modify the above method so that when we add an if-else, instead of modifying this method, we extend it

// What we need to do is to add a new function to replace the previous function as follows:
const Strategy = {
  a() {
    // TODO
  },
  b() {
    // TODO
  },
  other() {
    // TODO}},// Call the Strategy function with the same method name if the urlType matches the parameter you passed in

// If the urlType does not match the argument you passed, call the other method.
calss User{
  getUserData(urlType){
    Strategy[urlType] ? Strategy[urlType] : Strategy['other']; }}// If we add a judgment we just need to change it this way

const Strategy = {
  a() {
    // TODO
  },
  b() {
    // TODO
  },
  c() {
    // Add method <-------------- key
  },
  other() {
    // TODO}},// The new c method extends the class or method, instead of modifying the getUserData method
Copy the code

But there is one very very thing to note about the open close principle!


The open close principle does not mean that changes are completely eliminated but that new features are developed with minimal changes to the code


  • Changing code sometimes makes it difficult to replace classes or methods within the scope of actual business scenarios and limited personal capabilities. We need to keep the scope of change as small as possible by leaving room for future scenarios.

  • At the beginning of progress, as long as the code is better than the previous maintenance, more readable, in fact, for the vast business development boy like me, it has improved a lot of efficiency, slowly accumulated, slowly improved, continuous progress!

  • Ok, the second example of the open closed principle is that many design patterns are tailored to the open closed principle, so it’s certainly a good example to use these patterns, especially behavioral design patterns, which are used to identify common communication patterns between objects and implement them. This allows for increased flexibility in these communications. Elasticity can simply be interpreted as being easier to modify.

  • These are really examples of behaviors that you can pick up at random. Let’s pick up one

Chain of Responsibility model

  • If you’re familiar with compose, the functional programming component, or tapable, the webPack lifecycle implementation library,
  • Either you’re familiar with compose from the KOA source code, or you’re familiar with the Redux Onion model property, these are examples of the chain of responsibility pattern
For example, 'compose' is a function composed of several if-else functions. Function a(b){if(b>3000) {// TODO}else if(b <= 3000 &&b >= 1000){}else if(b> 0 &&b < 1000){}}Copy the code

It is easy to think of the chain of responsibility by modifying this function. Why? See the picture below!

  • Isn’t that an orderly chain up there?
// How to change the chain of responsibility
function chainA(b){
	if(b > 3000) {return 'Chain of Responsibility 1'}}function chainB(b){
	if(b <= 3000 && b >= 1000) {return 'Responsibility chain 2'}}function chainC(b){
	if(b > 0 && b < 1000) {return 'Responsibility chain 3'}}// The call function is the key, and a function is flushed only if the return value is not undefined
 class syncBailHook {
     constructor(array) {
         this.tasks = array;
     }

     call(. args) {
         let ret;
         let index = 0;
         do {
             ret = this.tasks[index++](... args) }while(ret === undefined && index < this.tasks.length)
         
         returnret; }}const chain = new syncBailHook([chainA, chainB, chainC]);
 chain.call(2000);  // return to chain of responsibility 2
Copy the code
  • Of course this is just the tip of the iceberg in the chain of responsibility, for other cases you can search Google or Baidu – ‘Tapable implementation’

  • (This is a relative concept. In general, I will use if-else to solve the problem. There is nothing bad about it. Each change is large, so you must consider the chain of responsibility model)

  • Secondly, if you write some common components, methods, etc. for others to use, in some key parts, such as implementing plug-in mechanisms, implementing interceptors, etc., better to use a little more advanced skills

  • On the reusability of functions

    • The underscore, loadash, and Ramda classes extend many methods into JS

    • Often, the front end has to deal with all sorts of data in the background, especially arrays with nested objects

    • Similar data processing, you can use some function libraries

  • Let’s say we have an array of people

var persons = [{
  name: 'Summ'.age: 24.address: 'safrouscsco'.school: 'picking university'.contry: 'china'
}, {
  name: 'Lucy'.age: 11.address: 'safrouscsco'.school: 'picking university'.contry: 'china'
}, {
  name: 'Block'.age: 30.address: 'safrouscsco'.school: 'picking university'.contry: 'china'
}]
Copy the code
  • We want to screen out people older than 18, and we just need their name and age fields
  • This sounds like a complex operation, but using the Ramda library is surprisingly simple
var isAdult = R.pipe( R.prop('age'), R.lte('18'))var list = R.pipe(
  R.filter(isAdult),
  R.map(R.pick(['name'.'age']))
)(persons)
Copy the code
  • Related design principles
    • Decorator mode
    • The strategy pattern
    • Responsibility responsibility chain model
    • The state pattern
    • Programming based on interfaces rather than implementations
    • polymorphism
    • Command mode
    • Observer model
    • The mediator pattern
    • Visitor pattern
    • Portfolio model
    • The bridge model
    • Builder model

Richter’s substitution principle

  • In plain English, the Richter substitution principle says that a subclass can extend the functionality of its parent class, but cannot change the functionality of its parent class.

  • The Richter substitution principle tells us that if software replaces a base object with a subclass object, the program will not generate any errors or exceptions. The reverse is not true. If a software entity uses a subclass object, it may not be able to use a base object

  • For example, if the parent class has a setState method, the subclass should not rewrite it. After rewriting, the intent of setState will be changed so that the component cannot use setState to render the page

Interface isolation (understand)

  • A client should not rely on interfaces it does not need. The dependency of one class on another should be based on the smallest interface
  • This principle, in typescript, can be interpreted as something defined on an interface and deleted if none of the classes used use methods or attributes on the interface

Dependence Inversion Principle

  • Advocate: high-level modules should not depend on low-level modules. Both should rely on abstraction

  • Abstractions should not depend on details, details should depend on abstractions

  • Program to the interface, not to the implementation

  • Take an example that is business-related

  • Business 1 depends on business 2, so the change of business 2 is likely to affect the logic of business 1. For example, business 1 uses three methods of business 2, and business 2 reconstructs, and these methods also change. Is it dangerous?

  • This is like a class that makes itself more unstable by referring to a method or attribute of another class.

  • So a business should depend on the interface, the business of this class is to provide data abstraction into interface, let a reliance on business two abstract interface, so business. No matter how to change, to provide the same things, such as the methods to provide the same output, output string, the output array (array members generally are objects, The properties of these objects are also specified using interfaces.

β—‹ For example:

class A(a){
     // B is an instance of class
     constructor(B){
         this.B = B;
     }
     fn(){
         this.B.heihei()
     }
 }

class B(a){
    heihei(){}}new A(new B());
Copy the code
  • The problem is that if B’s heihei method is overwritten, A will report an error
  • That is to say, A depends on everything in B. Class B is written by other colleagues. When B changes greatly, A will report errors, isn’t it A little annoying
  • This is where typescript’s strengths come in: interface oriented programming
  • Next interface, the advantage is, if one day B business logic has a big change, we are not afraid!!
Interface BI{fn(string: a): String} class B implements BI{fn(string){console.log(string)}} Class A{// B is an instance of class constructor(public B:BI){ this.B = B; } fn(){this.b.fn ('heihei')}} new A(new B()).fn();Copy the code

  • Design patterns are highly recommended. Most design patterns already provide ready-made solutions for complex refactorings, especially those that require the open and close principle. Once you are familiar with them, you will be able to abstract your patterns from a higher dimension.
  • In addition to these design patterns, there are some useful patterns for the front end, and as the front end evolves, more specific patterns emerge
    • For example, MVVM mode, data binding in both directions
    • Chain mode, similar to jquery
$('input[type="button"]')
  .eq(0).click(() = > {
    alert('Click on me! ');
  }).end()
  .eq(1)
  .click(() = >{$('input[type="button"]:eq(0)').trigger('click');
  })
  .end()
  .eq(2)
  .toggle(() = >{$('.aa').hide('slow');
  }, () = >{$('.aa').show('slow');
  });
Copy the code
  • Middleware patterns (e.g., Onion model, Redux middleware implementation, compose function)
  • Function corrification (although not a pattern, but in JS is a particularly useful way to write functions, improve function reuse)

refactor

  • When you refactor your code at a point where it doesn’t change, the cost of fixing it is high, and there are all kinds of bugs on the line, the best time to refactor is already delayed. Here’s a reference to the broken window effect.

  • Broken window

    • The theory of the broken window effect holds that undesirable phenomena in the environment, if allowed to exist, will induce people to imitate or even worsen them

  • There is no one-size-fits-all code, and refactoring can be a great way for engineers to grow, discover better techniques for refactoring, or apply advanced techniques they have learned

Common refactoring strategies

  • Ideas from Refactoring: Improving the Design of Existing Code

Refining functions (corresponding to the single responsibility principle and the open and closed principle, as well as readability)

  • If you run into a function with a lot of code, chances are you’ll need to refactor the function
  • Functions should be short and have a single responsibility. We can use the function class library to expand our frequently used functions
var getUserInfo = function () {
  ajax('http:// xxx.com/userInfo', function (data) {
    console.log('userId: ' + data.userId);
    console.log('userName: ' + data.userName);
    console.log('nickName: ' + data.nickName);
  });
};
var getUserInfo = function () {
  ajax('http:// xxx.com/userInfo', function (data) {
    printDetails(data);
  });
};
var printDetails = function (data) {
  console.log('userId: ' + data.userId);
  console.log('userName: ' + data.userName);
  console.log('nickName: ' + data.nickName);
};
Copy the code
  • The above code uses the printDetails () function to wrap the console, because each console means the same thing: printing data, so it can be wrapped in a single function
  • Because in real business, your Ajax functions will have a lot of logic, and if you can abstract the process-oriented logic out of each function, you can reduce the scope for changing the function when the business changes, rather than the entire Ajax function finding the corresponding code. Second, when the business changes and you need to add logic, the scope you add is in some abstract function, not the entire Ajax function.
  • That’s what we’re talking about in the on/off principle, trying to keep it as small as possible

Refining conditional branch statements into functions (corresponding to the single responsibility principle and the open and closed principle, as well as code readability)

  • Preempting functions instead of nested conditional branches (corresponding to the single responsibility principle and the open and closed principle, as well as code readability)
var getPrice = function (price) {
  var date = new Date(a);if (date.getMonth() >= 6 && date.getMonth() <= 9) {
    return price * 0.8;
  }
  return price;
}
var isSummer = function () {
  var date = new Date(a);return date.getMonth() >= 6 && date.getMonth() <= 9;
}


var getPrice = function (price) {
  var date = new Date(a);if (isSummer()) {
    return price * 0.8;
  }
  return price;
}
Copy the code

Nested conditional branching statements are very nasty and difficult to read. If else statements are flat (see the code below for what is flat), we can tile if else statements

var fn = function(a){
	if(a>100) {return '1',}else if (a > 200) {
    	return '2'
    } else {
    	return '3'}}Copy the code

Tiled as follows

var fn = function(a){
	if(a>100) {return '1',}if (a > 200) {
    	return '2'
    } 
    if{
    	return '3'}}Copy the code
  • Use less ternary operators (hard to read)

Additional topics: Defensive programming tips that are useful in business

  • Button to prevent repeated clicking
Anti - shake, throttling or request to start ash, request end to restore the click stateCopy the code
  • The interface returns an exception, such as null, when it should return an array
    • Use optional operators to make this easier
let nestedProp = obj.first && obj.first.second;
Copy the code
  • To avoid errors, ensure that the value of obj.first is neither null nor undefined before accessing obj.first. If you simply access obj.first.second directly without checking obj.first, an error may be thrown.

  • With the optional chain operator (? .). , it is no longer necessary to explicitly verify the state of obj.first before accessing obj.first.second and then obtain the final result with short-circuit calculation:

letnestedProp = obj.first? .second;Copy the code
  • And the null-value merge operator?? , you can set a default value when using optional chains:
let customer = {
  name: "Carl".details: { age: 82}};letcustomerCity = customer? .city ??Dark City;
console.log(customerCity); // Dark City
Copy the code