Write high-quality front-end code to reduce coupling and improve orthogonality

Coupling and Orthogonality

what is coupling

In Baidu Encyclopedia, the explanation of coupling

Coupling refers to the phenomenon that two or more systems or two forms of motion influence each other through interaction and even combine.

I once bought a remote control airplane toy. When I push the forward lever, the airplane not only moves forward, but also moves to the left or right. When I push the left and right levers, in addition to moving left and right, it also moves forward and rises. ; And when I press the up button, in addition to rising, it will also be accompanied by irregular forward movement or left and right turns;

The operating attitude of the aircraft is completely out of my control. Under ideal conditions, forward is forward, and there should be no left and right swings and lifts. This should also be the case for left and right operations and lift operations. Such a system is easy to operate, otherwise, every operation will It affects other operations and makes it impossible to successfully complete a simple task.

The same is true for coupling in code. I obviously only want to modify function A, but it inadvertently affects function B. When I repair function B, it also affects function C. Modifying one piece of code like this will affect another piece of code, which is called These two pieces of code are coupled.

However, the concept of coupling is a bit ambiguous. For example, module A and module B are coupled. We don’t know whether module A calls module B or module B calls module A. Therefore, the relationship between modules can be more accurately described by "dependence", and A depends on B means A needs to call B's exposed API.

Coupling is not completely bad. If a module wants to be used by others, there must be coupling. Therefore, the general discussion is about low coupling. We should try to eliminate some unreasonable coupling, that is, eliminate unreasonable dependencies. , thus making the system easier to maintain.

Common coupling types

1. System level coupling

Coupling of technology selection

I believe everyone has heard of the monolithic mode and the microservice mode. In the monolithic mode, multiple projects must use the same development language, the same various third-party module versions, etc. For example, the original project was developed with Vue2+ElementUI2 , now there are new Vue3 and Element Plus, in contrast, unless you upgrade the entire project, you cannot use these new technologies, because at the architectural level, the single mode requires that new projects must depend on The technology used in the original project is a kind of architectural coupling. With the microservice architecture, this problem does not exist. Each project is independent, and there are no dependencies between sub-projects. You can use Vue2, Vue3, or even React.

The same is true for back-end services. By using the microservice architecture, each module is called through http instead of directly calling methods based on a certain language, which achieves decoupling in technology selection between services. .

Deployment environment coupling

Nowadays, more and more enterprises choose Docker-based deployment methods. They use Docker technology to create separate containers for each service. Each container contains its own required deployment environment. When Docker is not used, if you want to deploy on a server It is very difficult to deploy multiple language applications such as PHP, Java, Python, etc. at the same time, or even deploy applications in the same language but different versions at the same time. Conflict problems are often encountered. Using Docker eliminates this coupling relationship. This is one of the reasons why Docker is so popular.

2. Coupling of system and third-party modules

In front-end projects, third-party libraries for network requests are basically used, such as axios, fetch, etc. If we directly call the axios method on the page to make network requests, then if one day we want to replace the network request library, or upgrade the network Regarding the version of the request library, if the new request library method and parameter order are inconsistent with the previous ones, it will bring a lot of modification work.

Thinking about it further, what we actually need is network requests, not axios. How we make network requests should not depend on what interface axios provides. Instead, we define the interface for network requests, and then use axios to implement it.

For example, we can define a network request module, request, which provides get, post and other requests. The methods provided by request are called on the page. If one day we want to change axios, we only need to modify the method in request.

Network requests in the page before the transformation

//页面中直接调用axios
import axios from 'axios'
function getData(url, params){axios.get(url, params)
} 

Renovated request.js

import axios from 'axios'
function request(url, options){return axios(url, options).then(res =>{}).catch(e =>{})
}

function get(url, params){return request(url, {method: 'GET',params})
}

function post(url, body){return request(url, {method: 'POST',body})
}
export default {get,post
} 

Network requests in the transformed page

import request from '@/utils/request'
function getData(url, params){request.get(url, params)
} 

Through this transformation, even if the interface parameter transfer method of axios changes in the future, or we directly replace axios with fetch, it will be very easy to do it, because we have completed the solution of the project and third-party components through this dependency inversion method. coupling.

Similar problems also exist on the backend. For example, when the backend saves logs, if it directly relies on a log framework such as Log4j, subsequent replacement of the log framework will bring a lot of modifications. Similarly, we can encapsulate a log. class, and then use third-party Log4j to implement it to complete the decoupling of the project and the specific log framework.

3. Circular dependencies

Systems that are not well designed often have circular dependency problems. For example, among the three modules A, B, and C in utils, A calls a method in B, and then B calls a method in A, or A calls B. , B calls C, and C calls A, which also forms a circular dependency.

A system with circular dependencies causes sufficient coupling between modules and is very inconvenient to modify. The common parts of multiple business module utils can be extracted to the base layer in a layered manner. The business layer utils can rely on the base layer utils. , but the base layer cannot rely on the business layer, thereby eliminating circular dependencies.

4. Global data coupling

Nowadays, in front-end projects, global variables are used relatively rarely, but there are more and more global data. Due to the use of vuex and redux, more and more modules link multiple functional modules through global state. Admittedly, sometimes we You need to take advantage of this coupling, but try to control the number of modules involved in this coupling.

Coupling global data will inevitably destroy the encapsulation of modules or components, making it difficult for components to be reused. At the same time, coupling global data will also destroy testability, causing us to simulate global data code when writing single tests. The code for the test itself is even longer.

For example, we have a diff function that compares the difference between two objects, as shown below. The parameter passed is target, and the source data to be compared is taken from the global state.

//utils中的diff函数
function diff(target){let source = store.state.appInfo//对比source和target
} 

If this is implemented, there will be the following problems:

  • When reading the code in the business, I don’t know who the diff is compared with. For example, I have to go deep into the diff function to understand the truth, which undoubtedly increases the cognitive load of reading.
//业务中的代码
import {diff} from 'utils'

function getDiffResult(info){return diff(info);//当阅读这块代码时,你知道是和谁对比吗?
} 
  • The diff method cannot be migrated to other projects, or cannot be used by other modules, because when used elsewhere, it does not necessarily need to be diffed with this global variable, which reduces reusability.
  • When testing the diff method, global variables must be mocked, which reduces testability.

We only need to make a simple transformation to decouple this coupling, that is, passing global variables as parameters, which at least ensures that the diff function is reusable and testable.

//utils中的diff函数
function diff(source, target){//对比source和target//不在依赖任何全局变量,可以随处使用
} 
//业务中的代码
import {diff} from 'utils'

function getDiffResult(info){return diff(store.state.appInfo, info);//清晰的看出谁和谁进行比较
} 

In addition to coupling and destroying encapsulation, reusability and testability, global data also reduces readability. We don’t know where a certain global data comes from or when it changes unless You have a detailed understanding of it, and this violates the principle of minimum knowledge. To change a line of code, you need to add a lot of knowledge first, which is undoubtedly scary.

Do not add global variables unless necessary.

5. Coupling of data structures

When the parameter of a function is an object, it means that the function must understand the structure of the object, which is also a kind of coupling.

Once the data structure of the object changes, the implementation inside the function must also change. Although sometimes this change is unavoidable, you should avoid in-depth understanding of the internal structure of the object, that is, avoid obtaining grand-level data of the object.

For example, to perform a deletion operation in the table we encapsulate, you need to pass the object listProps of a certain row of data. In the processing function, if you want to get the real row data, you need to read it through listProps.rowData.row, which is the processing function. We must be clear about the data structure transmitted from the table. If one day we want to modify the data structure of the table, it will cause a large number of modifications to the internal implementation of the function.

<template><tn-table><templateslot="op-buttons"slot-scope="listProps"><el-buttontype="text" size="mini"@click="deleteApp(listProps)">删除</el-button></template></tn-table>
</template>

<script>
export default {methods: {deleteApp(listProps) {//必须清楚表格传递过来的数据结构let rowData = listProps.rowData.row; let id = rowData.id;//调用接口删除数据}}
}
</script> 

It can be seen that the deletion function deleteApp couples the implementation of the table component. It is necessary to know what the data structure passed by the table component is. This is unreasonable. For the delete function, as long as the specific data of the row to be deleted is entered, or only Just pass the ID of the data to be deleted.

The modified implementation is as follows: when calling the delete function, the actual content of the row of data is passed, and the delete function no longer needs to know more implementation details of the table.

<template><tn-table><templateslot="op-buttons"slot-scope="listProps"><el-buttontype="text" size="mini"@click="deleteApp(listProps.rowData.row)">删除</el-button></template></tn-table>
</template>

<script>
export default {methods: {deleteApp(row) {//不在需要知道表格的特殊行数据结构,传递过来的就直接是改行数据let id = row.id;//调用接口删除数据}}
}
</script> 

Or sometimes we don't need to pass a complete object deconstruction, just the specific parameters used.

Transformation of the former:

function deleteNamespace(row){let namespace = row.content.metadata.namespace;//删除namesoace
} 

After transformation:

//删除函数不再需要知道具体数据结构
function deleteNamespace(namespace){//删除namesoace
}

//调用方把需要的值直接取出来传递过去
deleteNamespace(row.content.metadata.namespace) 

This method is only suitable for situations where there are relatively few parameters. Too many parameters will cause additional cognitive and memory burdens, so balance them according to the situation.

6. Coupling functions and API interfaces

Generally, our functions and API interfaces will have some coupling. After all, data needs to be obtained from the interface. Changes in the interface will cause changes in the front end. For example, if the back end changes the creation time field from createTime to created, the front end will follow the changes. Currently, There is no good solution. If field mapping is used, the cost is too high, and only some fields can be decoupled.

Example 1: For example, the backend defines the state:

  • 0: Synchronized
  • 1: Not synchronized
  • 2: Conflict

If we use it directly in the code, first of all, the readability is not good. Secondly, if the backend subsequently modifies the corresponding relationship between status and value, the frontend will also be modified accordingly, causing coupling.

function deleteHandle(rowData) {if (rowData.status === 0 || rowData.status === 2) {}
} 

By defining state constants, the corresponding relationship between the backend interface state and specific values ​​is decoupled.

export const K8S_RESOURCE_STATUS = {synchronized: 0,unSynchronized: 1,inconsistent: 2
};

function deleteHandle(rowData) {if (rowData.status === K8S_RESOURCE_STATUS.synchronized|| rowData.status === K8S_RESOURCE_STATUS.inconsistent) {}
} 

Scenarios coupled with APIs also include changes brought about by changes in the back-end interface address. Therefore, as we introduced earlier, for network requests, the API address needs to be made into a configuration file, and the network requests are encapsulated into the service layer to reduce the front-end's need for API interfaces. coupling.

7. Coupling between components/modules

Front-end students basically know that communication between sub-components cannot directly call each other, which will cause coupling between sub-components. If something happens to a certain sub-component, you only need to throw the event. How to do it specifically? The subsequent processing is for the leadership (parent component) to consider and design. Any changes in the future will also be decided by the parent component. Child components have no right to interfere, let alone dictate and control other components.

Business process control should be given to the parent component. The sub-component is only responsible for completing things within its own scope of responsibility and should not exceed its authority.

Similar to this, there is communication between descendant components, such as through the event bus EventBus. When an event occurs, it is thrown. Whoever cares about this event will listen. The party that throws the event is decoupled from the party that receives the event.

There are also similar ideas such as message queue, observer mode, subscription mode, etc. The producer of data or events does not care about how to process it later. Through this mode of registration and monitoring, the producer and consumer are decoupled.

Orthogonal and decoupled

orthogonal system

In geometry, when two straight lines intersect to form a right angle, they are said to be orthogonal. For vectors, the two lines are independent of each other and develop in completely different directions.

In computers, orthogonality symbolizes independence or decoupling. For two or more things, if a change in one does not affect any other, these things are said to be orthogonal. For example, database-related code should be orthogonal to the user interface. Changing the interface does not affect the database, and switching databases does not affect the interface.

We want the components of the design to be self-contained: autonomous and with a single clearly defined intention. When components are isolated from each other, you know you can change one of them without worrying about affecting the others. As long as the external interface of the component is not changed, you can rest assured that it will not affect other parts of the system.

Advantages of Orthogonal Systems

Orthogonal systems can bring two main benefits: increased productivity and reduced risk, while also improving the testability of the system.

1. Increase productivity

  • When changes are localized, both development time and testing time are reduced. It's easier to write relatively small, self-contained components than to write a complete block of code.
  • The orthogonal approach also promotes reuse. If a component's responsibilities are clearly defined and single, it can be combined with other components in various ways.
  • If one component can do M unique things and the other can do N things, if they are orthogonal, they can do M*N things when combined. If the two components are not orthogonal, it will be difficult to combine.

2. Reduce risk

  • Diseased parts of the code are isolated. If a problem occurs in one module, it is unlikely to spread the symptoms to other parts of the system. It is also easy to cut out the diseased part and replace it with a new one.
  • The system will not be so fragile that when small changes and modifications are made to a specific area, the resulting problems will be localized to that area.
  • Orthogonal systems are more conducive to testing and more testable
  • It will not be tied to specific third-party components and can be replaced at any time according to the situation.

3. Improve testability

  • If you find a piece of code that is difficult to unit test, it means that it is probably too coupled.
  • In order to test coupled code, a lot of preparatory work is required, and preparing the code may take longer than the code itself to be tested.
  • Orthogonal systems are very convenient for constructing test cases, and testability is positively related to orthogonality.

Several methods of decoupling

black box development

When multiple modules are called, they are called through the API provided by the module without knowing its internal implementation details. Each module is like a black box to each other. The so-called black box means that you do not need to know the internal implementation details, such as the internal data structure of the module, the calling sequence, etc.

For example, there is an event class Event inside a certain component class Component. Event provides the delegate method of the event proxy.

let component = new Component();
component.event.delegate('click', callback); 

In this example, if you want to perform event delegation, you need to call it through the event instance inside the component, that is, you need to know the internal structure of the component class. If the component class no longer provides event proxy methods based on the Event class, or the component class is modified Internal implementation, then all business codes that call the event proxy must be modified, that is, the implementation of business modules and component classes is deeply coupled.

A better implementation is to directly call the API provided by the component class to the outside world. For example, the component class directly provides the event proxy method to the outside world, and then the internal delegate method calls the delegate method of the Event class.

class Component {delegate(eventName, callback){this.event.delegate(eventName, callback)}
}

let component = new Component()
component.delegate('click', callback) 

Subsequent component classes adjust the delegate method. As long as the API structure is not changed, the business will not be affected, and it will be like a black box to the business.

layered

The essence of decoupling is separation of concerns, and layering is a common and important method of separation of concerns.

The development history of the front-end is a history of layered development. Initially, the front-end and back-end were developed mixedly, and the UI page code and database-related code were mixed together. Later, the MVC layered model was adopted for development, and the front-end and front-end were separated. In this way, After decoupling, front-end and back-end development can finally be developed smoothly in parallel, which also makes the front-end become more and more important. In recent years, with the emergence of MVVM pattern frameworks such as Vue and React, the front-end has also entered the best era in history.

Taking MVC as an example, after layering, the View layer and the Model layer are decoupled. The Model layer query database is changed without affecting the page, and changes in the View layer will not affect the Model layer code. This layering method brings Here come the following 2 benefits:

  • Parallel development: Front-end and back-end engineers can develop their own codes in parallel, as long as the interactive data structure is agreed upon
  • Improve reuse: A page that contains front-end and back-end logic is difficult to use in another project. There are almost no pages with exactly the same front-end and back-end. However, after layering, even if the View layer is not exactly the same, the Model layer and Controller layer will most likely still be the same. reusable

In all aspects of front-end development, we are actually developing using the idea of ​​layering. For example, CSS development can include theme variables, global css, page css, and component css; the utils library can also be divided into basic general utils and business Utils, basic utils are utils that have nothing to do with business, such as the function that generates a unique string uuid that is often seen; components can also be divided into basic components and business components. Basic components are decoupled from specific businesses, such as general table components, Pop-up components, etc.

Since the CSS, JS and components of the base layer are decoupled from the business, their reusability is greatly improved. Basically, after opening a new project, the original base layer code can be used. If you open a new project every time Starting from scratch means that it has not been decoupled from the business.

Our goal is to make the base layer bigger and thicker through decoupling. The base layer is a stable code, and the business layer is a sensitive code. After the base layer is completed, there is almost no need for many changes.

dependency inversion

The Dependence Inversion Principle means that programs should rely on abstract interfaces and not on specific implementations. Simply put, it requires programming the abstraction and not the implementation, thus reducing the coupling between the client and the implementation module.

For example, the company has developed an autonomous driving system for Ford and Honda, which can realize autonomous driving functions as long as it is installed on both cars. We have defined an autonomous driving system class AutoSystem, a Ford car class FordCar, and a Honda car class HondaCar. Suppose the starting methods of Ford and Honda are different. One is run and the other is driver. To start the car in the automatic driving system, you need to distinguish between the two.

class HondaCar{run(){console.log("本田开始启动了");}
}
class FordCar{driver(){console.log("福特开始启动了");}
}

class AutoSystem {run(car){if(car instanceof HondaCar){car.run()}else if(car instanceof FordCar){car.driver()}}
} 

Now that the company's business has grown, it is about to install an automatic driving system for BMW cars. The method of starting a BMW car is startCar, so the automatic driving system needs to be modified to support BMW cars.

class HondaCar{run(){console.log("本田开始启动了");}
}
class FordCar{driver(){console.log("福特开始启动了");}
}
class BmwCar {startCar(){console.log("宝马开始启动了");}
}

class AutoSystem {run(car){if(car instanceof HondaCar){car.run()}else if(car instanceof FordCar){car.driver()}else if(car instanceof BmwCar){car.startCar()}}
} 

As the subsequent business grows, the autonomous driving system will be filled with various if-else. This is still an example of only one way to start the car. The actual situation is definitely more complicated. Every time we negotiate a partner, the autonomous driving system must do it. A large number of adaptations are obviously very unreasonable. There is serious coupling between the automatic driving system and specific models. This is precisely because high-level applications rely on the underlying implementation. Suppose we require that all cars should have fixed methods, which are often called interfaces in the backend. Then the autonomous driving system no longer needs to be changed frequently, and new ones are added every time. Car model, just add the corresponding car interface.

class HondaCar{run(){console.log("本田开始启动了");}
}
class FordCar{run(){console.log("福特开始启动了");}
}
class BmwCar {run(){console.log("宝马开始启动了");}
}

class AutoSystem {run(car){car.run()}
} 

It can be seen that the autonomous driving AutoSystem has been greatly simplified, and it will no longer be coupled to specific models in the future.

The next step is always:

1. Upper-layer applications should not depend on lower-layer modules, they should all depend on abstractions.

2. Abstraction should not depend on concreteness, concreteness should depend on abstraction.

Middle layer mapping, adaptation layer

A depends on B. If we want to decouple A and B, we can add an intermediate layer C between A and B to change the dependency relationship to A depending on B and B depending on C. This avoids the direct connection between A and B. coupling.

If B changes later, you only need to modify the adaptation layer C, and the A layer code does not need to be changed.

Some people may say that if a layer is added, then the change in B still needs to be modified, but the modification at A is moved to C.

If A calls a method of B 5 times, if B modifies the parameter passing order of the method, then A will have to modify it 5 times; if middle layer mapping is used, after B is modified, only middle layer C needs to be modified, and A does not need to modify it. If any modification is needed, it can be seen that by adding an intermediate layer or an adaptation layer, the number of modifications to the entire system after the dependent part changes is reduced.

Adding a middle layer makes us less dependent on a third-party module, makes it easier to replace, and does not tie the system to a fixed supplier.

Production and consumption, publish and subscribe, events, message queues

Taking the well-known events as an example, the producer throws an event to the event bus, and the consumer listens to an event and takes action. The event producer does not care who handles the event, and the event consumer does not care whether the event is Who generates it, in this way the decoupling of the producer and the consumer is achieved.

For example, for a producer product, if the event method is not used, the general operation is as follows: call the consumer in the producer code

function product(){custom1()custom2()
}

function custom1(){}
function custom2(){

} 

If it is necessary to add a consumer custom3 and remove the consumer custom1 as the demand changes, then the product must be modified accordingly, that is, the producer code and the consumer code are coupled.

function product(){//去除custom1()custom2()//新增custom3()
} 

With the event bus, there is no such problem. The producer can just throw events and related data. No matter how it is processed, the producer code remains unchanged.

function product(){event.emit('someEvent', data)
} 

Similar to them are message queue, observer mode, production and consumption mode, etc. Although there are differences, the general idea is to achieve decoupling of generation and consumption.

pure function

Pure function is a concept used in all functional programming languages. A pure function is like the function y = f(x) in our mathematics. The value of y only depends on the parameter x. That is, as long as the parameter x is determined, the output value y will be certain. constant. Pure functions in code generally have the following characteristics:

  • The function return value only depends on the parameters. If the parameters do not change, the return value will not change.
  • There are no side effects during function execution

Example 1: The result of each call to a non-pure function may be different

//非纯函数,每次调用add返回值不同
let count = 0
function add(){return count++
}

//纯函数
function add(a, b){return a + b
} 

Example 2: Impure functions may depend on external variables, such as global variables

//非纯函数, 会依赖全局变量
function add(a){return window.count + a
} 

The so-called side effects refer to the impact of function execution on the outside. This impact includes but is not limited to:

  • Modify parameter value
  • Make a network request
  • Manipulate dom
  • store
//非纯函数, 副作用:改变参数结构
function format(data){data.value = data.value + ''return data
}

//非纯函数, 副作用:网络请求
function getData(data){return axios.get(url)
}

//非纯函数, 副作用:存储
function setData(data){ localStorage.setItem('a', data)
} 

Due to these characteristics of pure functions, pure functions are naturally less coupled than ordinary functions. For example, they do not rely on external global data, they do not produce side effects, and of course they are not tightly coupled with the outside world.

Therefore, we should use pure functions as much as possible during development. Impure functions are of course unavoidable, but what we have to do is to extract pure functions, a large piece of code with side effects, from some business codes with side effects. , many times it can be split into a small function with side effects and some small pure functions. We limit the coupling part to a very small code area.

For example, a utils function diff, whose parameter is target, will compare target with a global variable. This is obviously not a pure function, and there is also coupling with global variables. We only need to make simple modifications to become a pure function.

//非纯函数
function diff(target){let source = window.source//比对
} 

After transformation

//utils中的纯函数
function diff(source, target){//比对
}

//业务中调用
diff(window.source, target) 

After a simple transformation, the coupling part of the code is limited to the business code, and the basic utils function eliminates this coupling problem.

It is recommended to write as many pure functions as possible in the code.

strategy pattern

We often encounter situations where a certain attribute value in some interfaces is converted to Chinese and displayed on the page. For example, for user types, the interface is usually in English, and it must be converted to English when displayed on the page.

function getLabel(type){if(type === 'normal'){return '普通类型'}else if(type === 'vip'){return '会员'}else {return '未知'}
} 

If the business changes and a new membership type is added, then the conversion code needs to be modified everywhere. That is, there is a strong coupling between the business rules and our UI display.

The solution to this problem is also very simple. Extract the bare metal business into the configuration file and use it as a strategy. Subsequent business changes only need to update the strategy.

//配置文件config.js
export const userType = {normal: '普通类型',vip: '会员',svip: '超级会员',default: '未知'
}

//业务中使用
function getLabel(type){return userType[type] || userType['default']
} 

It can be seen that the business code no longer has the annoying if-else, and after the business changes, there is no need to modify the code, just change the policy configuration, which complies with the opening and closing principle.

Of course, this is just the simplest way to use the strategy model, but the principles are the same, and some complex logic processing is also possible. For example, there is now a page like this, a tab page, which displays the epidemic data of various countries, such as China's , the world, and the current city. When switching tab pages, you need to request different data respectively.

<template><div><tabs v-model="activeTab" @change="tabChange"><tab-pannel name="china"><ChinaData /></tab-pannel><tab-pannel name="world"><WorldData /></tab-pannel><tab-pannel name="beijing"><BeijingData /></tab-pannel></tabs></div>
</template>
<script>
export default {data() {return {activeTab: 'china'}},methods: {tabChange(tab) {if (tab === 'china') {//请求中国数据} else if(tab === 'world') {//世界数据}else if(tab === 'beijing'){//北京数据}}}
}
</script> 

If the product manager now wants to add US data for comparison, a lot of logic must be modified. We can use the strategy model to decouple this coupling.

Add a config with two fields:

  • getData: Get data functions corresponding to different cities
  • component: UI display components corresponding to different cities
<template><div><tabs v-model="activeTab" @change="tabChange"><tab-pannel v-for="item, type in config" :key="type"><component :is="item.component" /></tab-pannel></tabs></div>
</template>
<script>
export default {data() {return {activeTab: 'china',//增加策略配置configconfig:{china: {getData: this.getChinaData,component: ChinaData},world:{getData: this.getWorldData,component: WorldData},beijing:{getData: this.getBeijingData,component: BeijingData},}}},methods: {tabChange(tab) {//切换页签的逻辑变的异常简单 this.config[tab].getData()},getChinaData(){},getWorldData(){},getBeijingData(){}}
}
</script> 

If you want to add US data after the transformation, you only need to configure it in config, and other logic does not need to be modified.

chain of responsibility

Another coupling relationship is the coupling of the calling order between modules. For example, function A calls function B, and function B calls C. If one day the calling order changes, the relevant processing code must be adjusted accordingly.

Taking form verification as an example, assume that we have three verification methods for name verification, namely checking whether it is empty, whether the length is legal, and whether it contains special characters.

function checkEmpty(){//校验逻辑checkLength()
}
function checkLength(){//校验逻辑checkSpecialCharacter()
}
function checkSpecialCharacter(){//校验逻辑
} 

If a new verification logic is added now, for example, it cannot start with a letter and is placed in the second verification position, then the code of other verification logic must be modified.

In response to this situation, we can use the chain of responsibility model to rewrite it. The so-called chain of responsibility simply means what to do after each step is executed. It is no longer hard-coded, but dynamically configured, and each processing function Registered on the chain, the responsibility chain is responsible for controlling the entire execution sequence.

There are many ways to implement it. For example, the backend may use integration-based implementation. The processing function of each responsibility chain has a next attribute that points to the next one.

class CheckChainHandler{constructor() {this.nextHandler = null;}setNextHandler(nextHandler) {this.nextHandler = nextHandler;}doValidate(params){throw new Error('必须由子类重写的验证方法');}executeValidateChain(params){let validateResult = this.doValidate(params);if(!validateResult) return;if (this.nextHandler){this.nextHandler.executeValidateChain.call(this.nextHandler,params);}}
}
class EmptyCheckHandler extends CheckChainHandler {
}
class LengthCheckHandler extends CheckChainHandler {
}
class SpecialCharacterCheckHandler extends CheckChainHandler {
}

function checkName(name){const emptyCheckHandler = new EmptyCheckHandler()const lengthCheckHandler = new LengthCheckHandler()const specialCharacterCheckHandler = new SpecialCharacterCheckHandler()emptyCheckHandler.setNextHandler(lengthCheckHandler)lengthCheckHandler.setNextHandler(specialCharacterCheckHandler)emptyCheckHandler.executeValidateChain(name)
} 

In the front-end, we generally use the use method to create a chain of responsibility, which is relatively simpler to use.

const checkMiddleware = new CheckMiddleware()
checkMiddleware.use(checkEmpty).use(checkLength).use(checkSpecialCharacter) 

Pipes in gulp:

gulp.src('client/templates/*.jade').pipe(jade()).pipe(minify()).pipe(gulp.dest('build/minified_templates')); 

Middleware in koa:

const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {await next();const rt = ctx.response.get('X-Response-Time');console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// response
app.use(async ctx => {ctx.body = 'Hello World';
});
app.listen(3000); 

Use global data with caution

Global data naturally couples multiple different modules together. The relevant content is also mentioned above, so I won’t go into details. For global data, use it as little as possible and be cautious. Sometimes you can avoid coupling a large number of modules with global data by passing parameters. , reduce the number of coupled modules.

chapter summary

  • Coupling is the mutual dependence between multiple modules. Coupling cannot be eliminated, but can only be reduced.
  • There are many situations that cause coupling, including system-level coupling, coupling between the system and third-party modules, circular dependencies, coupling caused by global data, coupling of data structures, coupling between front-end and back-end APIs, coupling between modules coupling, etc.
  • Building an orthogonal system can improve productivity and reduce risks. The testability of a module is positively related to orthogonality. Whether it is testable can be used to judge the degree of coupling.
  • There are many ways to decouple, including black box development, layering, dependency inversion, adding an adaptation layer, using production and consumption, publish and subscribe, observer, event, message queue, strategy, responsibility chain and other modes. Use pure functions and be careful. Use global variables

at last

I compiled a set of "Interview Guide for Front-End Major Companies", which includes HTML, CSS, JavaScript, HTTP, TCP protocol, browser, VUE, React, data structure and algorithm. There are 201 interview questions in total, and I have made an answer for each question. Answer and analysis.

Friends in need can click on the card at the end of the article to receive this document and share it free of charge

Part of the document display:



The length of the article is limited, so the following content will not be shown one by one.

Friends in need can click on the card below to get it for free

Guess you like

Origin blog.csdn.net/web2022050901/article/details/129204763
Recommended