1. Background

The team belongs to the rear business support department, and the projects in the group are mainly PC middle and background applications. Compared with mobile applications, the code base is relatively large and the business logic is relatively complex. In the continuous iterative process, we found that the current code repository still has many points that can be optimized:

Can reduce the dependence on ui framework

In 2021, the front-end platform decided to migrate the technology stack to the React ecosystem in a unified manner, and the infrastructure construction of the subsequent platforms also revolved around React. This made the system developed by the merchants using the Vue ecosystem face the problem of technology stack migration, and the business logic and UI framework were linked together. become extremely important.

Code style can be more uniform

As the amount of code and the number of team members increases, so does the variety of code in the application. In order to be able to iterate continuously and rapidly, the team urgently needs a unified top-level code architecture design scheme.

Can integrate automated test cases

As the business becomes more and more complex, the team needs to frequently return functions during the rapid iteration process, so our demand for automated single test cases has become stronger and stronger.

In order to complete the above optimization, the four groups have refactored the existing application architecture, and the core of the refactoring is the clean architecture.

2. The Clean Architecture

The clean architecture is a set of code organization concepts proposed by Robert C. Martin (Uncle Bob) in 2012. Its core is to split the code into different levels according to the different functions of each part of the code. A clear dependency principle has been formulated between each level to achieve the following purposes:

  1. Framework-independent: Whether it is front-end code or server-side code, the logic itself should be independent and should not depend on a third-party framework or tool library. A separate set of code can use third-party frameworks, etc. as tools.
  2. Testable: The business logic in the code can be tested without relying on UI, database, server.
  3. It has nothing to do with ui: the business logic in the code should not be strongly bound to the ui. For example, when a web application is switched to a desktop application, the business logic should not be affected.
  4. It has nothing to do with the database: no matter whether the database uses mysql or mongodb, no matter how it changes, it should not affect the business logic.
  5. It has nothing to do with external services: no matter how the external service changes, it does not affect the business logic of using the service.

In order to achieve the above goals, the neat architecture divides the application into at least four layers, such as entities, use cases, interface adapters (MVC, MVP, etc.), and Web/DB. In addition to layering, this architecture also has a very clear dependency between layers, and the logic of the outer layer depends on the logic of the inner layer.

Entity
entities encapsulate enterprise-level business logic and rules. There is no fixed form for entities, whether it is an object or a collection of functions, the only criterion is that it can be reused by various applications of the enterprise.

Use Case
Entities encapsulate a part of the most common logic in the enterprise, and the business logic of each application is encapsulated in the use case. The most common crud operation for a model in daily development belongs to the usecase layer.

Interface Adapter
This layer is similar to the glue layer, which needs to be responsible for the data conversion between the entities and use cases of the inner circle and the external interfaces of the outer circle. It is necessary to convert the data of the outer service into the data that the inner entity and usecase can consume, and vice versa. As shown in the diagram above, this layer can sometimes be simple (a transformation function), and sometimes it can be complex enough to include an entire MVC/MVP architecture.

External Interfaces
External services we need to rely on, third-party frameworks, and page UIs that need to be pasted all belong to this layer. This layer is completely unaware of any logic in the inner circle, so no matter how this layer changes (UI changes), it should not affect the application layer logic (usecase) and enterprise-level logic (entity) of the inner circle.

Dependency principle <br>In the original design of the clean architecture, it is not mandatory to write only such four layers, and it can be divided into finer details according to the needs of the business. However, no matter how it is dismantled, it is necessary to abide by the aforementioned principle of dependence from outside to inside. That is, entity, as an enterprise-level general logic, cannot depend on any module. The outer ui can use usecase and entity.

3. Refactoring

Some of the current specific problems of the current code base have been introduced earlier, and the idea of clean architecture can help us optimize code maintainability.

As a front end, our business logic should not depend on the view layer (ui framework and its ecology), and at the same time should ensure the independence and reusability of business logic (usecase & entity). Finally, as a data-driven end application, it is necessary to ensure that application view rendering and business logic are not affected by data changes (adapter & entity).

Based on the above thinking, we have implemented the "clean architecture" as follows.

Entities
For front-end applications, in the entity layer, we only need to make a simple abstraction of the raw data on the server side, and generate an anemic object for subsequent rendering and interaction logic.


The above is the entity factory function of the merchant's background order model. The factory is mainly responsible for processing the raw data returned by the server, so that it can meet the requirements of the rendering layer and the logic layer. In addition to the abstract data, it can be seen that the data is also verified in the entity factory, and the dirty data and the data that does not meet the expectations are all processed or the bottom line (the specific operation depends on the business scenario).

One thing to note is that reusability needs to be considered when designing entities (especially base entities). For example, on the basis of the above orderEntity, we can generate a virtual commodity order entity through a simple combination:

In this way, we achieve 2 purposes through the entity layer:

  1. Isolate the front-end logic from the server-side interface data, no matter how the server-side changes, the subsequent rendering and business code of the front-end do not need to be changed, we only need to change the entity factory function; and after processing by the entity layer, all flow into subsequent rendering & interaction Logical data is reliable; for some abnormal data, the front-end application can detect and alarm at the first time.
  2. By abstracting the business model, the combination and reuse of modules are realized. In addition, the abstracted entity is also very helpful to the maintainability of the code, and developers can intuitively know all the fields contained in the entity used.

Usecase
The usecase layer is a series of crud operations around the entity, as well as some linkages for page rendering (implemented through the ui store). Due to the current architecture (no bff layer), usecase may also undertake some of the work of microservice tandem.

For example, the backend order page of the merchant has a bunch of preparation logic before rendering:

  1. Which tab is selected by default is determined according to the query parameter of the route and some merchant type parameters
  2. According to whether it is a domestic merchant or an overseas merchant, calling the corresponding supplier interface to update the supplier drop-down box is now roughly implemented as follows:

We can see that lines 7-15 and 24-125 assign values to this.subType. However, since we can't determine whether the function in line 20 also assigns this.subType, we can't completely determine what the value of subType is based on the code of the mounted function, and we need to jump to the getAllLogisticsCarrier function to confirm. This code has been simplified here. The actual code has several calls like getAllLogisticsCarrier. If you want to understand the logic, you have to read all the functions. The readability of the code is average. At the same time, since functions are encapsulated in ui components, some modifications are required to cover unit tests for functions.
In order to solve the problem, we split this part of the logic into the usecase layer:


First of all, you can see that all usecase must be a pure function, and there will be no problem of side effects.

Secondly, the prepareOrderPage usecase is specially customized for the order page. After splitting, it can be seen at a glance that the preparation of the order page needs to decide the selected tab and pull the supplier list. The other split queryLogisticsCarriers encapsulates the cross-border and domestic logic of the merchant's backend. No matter how the cross-border or domestic logic changes, its scope of influence is limited to the queryLogisticsCarriers function, and we need to perform functional regression on it. ; For prepareOrderPage, queryLogisticsCarriers is just an implementation of () => Promise<{ carriers: ICarrires }>, its internal logic of calling queryLogisticsCarriers is completely unaffected, and no regression is required.

Finally, since we have done dependency inversion, we can easily override the single test for usecase:

In addition to the functional regression of the unit test, its description (the format of Given-When-Then is used in the demo, due to space reasons, the details of the unit test will be introduced in the subsequent articles) for understanding the logic of the code. Very helpful. Due to the forced binding of the unit test and code logic, we can even treat the unit test description as a real-time updated business document.

In addition to the convenience of writing single tests, after the usecase split is completed, the ui component has truly become a component that is only responsible for "ui" and monitoring user interaction behavior, which lays the foundation for our subsequent React technology stack migration; through usecase we It also achieves a very good modularity. For some entities that are used more, its crud operations can be reused between multiple pages and even applications through independent usecases.

Adapter
The fetchAllLogisticsCarrier in the above usecase example is an adapter. The function of this layer is to convert the data returned by the external system into entities and return them in a unified data format.

The core of this layer is that it can rely on the factory function of the entity to convert the data returned by the interface into the model data designed by the front-end itself, ensuring that the data flowing into the usecase and ui layers are processed "clean data". In addition, usually at this layer we return data in a fixed data format, such as {success: boolean, data?: any} in the example. This is mainly to smooth out the differences brought about by connecting multiple systems, and at the same time reduce the communication cost when multiple people collaborate.

Through the combination of Adapter + entity, we basically form an anti-corrosion layer between the front-end application and the back-end service, so that the front-end can complete the development of ui rendering, usecase and other logic without knowing the interface definition at all. After the server produces the definition, the front end only needs to return and adapt the actual interface to the model defined by itself (through entity). This is very, very important to improve the efficiency of the front-end test week. Because of the existence of the anti-corrosion layer, we can design a business model according to the content of the prd after completing the requirement review in the test week, and then complete the requirement development. After that, you only need to connect with the server to complete the adaptation of the adapter layer.

In the process of practice, we found that when docking the same system (stark service for merchants), each adapter handles exceptions in almost the same way (lines 11-15 above), and we can extract it through Proxy. reuse. Of course, in the future, we also have the opportunity to automatically generate adapters based on the interface definition.

UI
After the previous split, whether our UI layer is written in React or Vue, the work to be done is very simple:

  1. Listen for interaction events and call the corresponding usecase to respond
  2. Get entity data for rendering through usecase

Since entity has already done filtering and adaptation processing, we can use it with confidence at the ui layer without writing a bunch of inexplicable judgment logic. In addition, since the entity is a model defined by the front end itself, no matter how the server interface changes during the development process, only the entity factory function will be affected, and the ui layer will not be affected.

Finally, at the UI layer we are left with the headache of stack migration. The entire team currently uses Vue for 10 projects. According to the iteration frequency and project scale, migration schemes can be divided into two categories:

  • Large applications with frequent iterations: mainly include several medium and large applications with many lines of code and complex logic. It is extremely expensive for these applications to directly complete the migration with a shuttle, but at the same time, each iteration has considerable requirements. Based on this situation, for these three applications, we adopted a micro-frontend method for migration. Each application has a corresponding React application. For new pages and some businesses whose logic has been completely decoupled from the UI, the migration cost is not high, and they are all undertaken by the React application. Finally, the integration is achieved through module federation.
  • Small applications with infrequent iterations: The rest of the applications are small applications with low complexity. These applications do not require much iteration and are mainly maintained. Therefore, our solution is to restructure the existing logic neatly, and directly replace the UI layer after the UI and logic are layered to complete the migration.

4. Follow-up

Through a clean architecture, we have formed a unified coding standard, taking a solid step on the road of front-end application standardization. It is foreseeable that the entire standardization process will be very long. We will gradually add new specifications to the standard to make it more complete. In the short term, we plan to:

  • A single test is a document: As mentioned above, usecase cooperates with the implementation of a single test through dependency inversion. The follow-up team expects to precipitate some business logic implementation details through the description of a single test to solve the real-time problem of business documents.
  • Improve the monitoring system: The three types of anomalies that are often encountered in the front end include code logic anomalies, performance bottlenecks (rendering freezes, insufficient memory, etc.), and data-induced anomalies. For data anomalies, we can add the reporting of abnormal data in the process of entity layer mapping to fill the gaps in current monitoring. (Code logic exceptions have been monitored through sentry, performance monitoring is not required for middle and background applications)

After the standard is gradually stabilized, we also expect to carry out some engineering practices based on the stable specification (such as automatically generating the adapter layer according to the mooncake document, implementing the function switch based on usecase, etc.), so stay tuned.

Reference link:
The Clean Architecture: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
Module Federation: https://webpack.js.org/concepts/module-federation/
Anti-corruption Layer pattern: https://docs.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer

*Text/Chen Ziyu
@德物科技public account


得物技术
854 声望1.5k 粉丝