Recently, I used DDD/Clean Architecture to develop CRM for internal use in the company. I felt that this layered Architecture could solve the current problems, so I decided to reconstruct the current open source mobile best practices project. The following is the description of the layered Architecture. To learn more, please check the source code: github.com/mcuking/mob…
Application is introduced
Firstly, I will introduce the application of this project. It is an interactive and concise Todo application. The application is named Memo, short for Memory, and refers To Microsoft’s To Do, Listify, Trello and other applications. The biggest difference, however, is that the project does not rely on the back end. Instead, it uses the browser-provided indexDB for data storage, which ensures absolute security. In addition, updating the application will not clear the original data unless the application is uninstalled. The renderings are as follows:
Experience platform | Qr code | link |
---|---|---|
Web | Click the experience | |
Android | Click the experience |
Architectural layering
At present, front-end development is mainly based on single-page applications. When the business logic of applications is complex enough, problems like the following are always encountered:
-
The service logic is too concentrated in the view layer, so that multiple platforms cannot share platform-independent service logic. For example, a product needs to be maintained on Mobile and PC, or the same product has Web and React Native.
-
When a product requires multiple people to collaborate, each person has a different code style and understanding of the business, resulting in a chaotic distribution of business logic.
-
The understanding of the product stays at the page-driven level, resulting in a large discrepancy between the technical model implemented and the actual business model. When the business requirements change, the technical model is easily destroyed.
-
Too much reliance on the front-end framework results in the need to rewrite all business logic and perform regression tests if the refactoring does a framework switch.
In view of the problems encountered above, the author learned some knowledge about DDD (Domain-driven design), Clean Architecture, etc., and collected practical data of similar ideas in front end, forming the following front-end layered Architecture:
View layer must be very well understood, not here, focus on the following three layers of meaning:
Services layer
The Services layer is used to operate the underlying technologies, such as encapsulating AJAX requests, operating browser cookies, locaStorage, indexDB, operating the capabilities provided by Native (such as calling cameras, etc.), and establishing websockets to interact with the backend.
The Services layer can be subdivided into the Request layer and translator layer. The Request layer mainly implements most functions of Services. The translator layer is mainly used to clean the data returned from the server or client interface, such as deleting some data, modifying attribute names, and converting some data. Generally, the translator layer can be defined as a pure function. The following is an example of the actual code of this project.
Get quote data from the back end:
export class CommonService implements ICommonService {
@m({ maxAge: 60 * 1000 })
public async getQuoteList(): Promise<IQuote[]> {
const {
data: { list }
} = await http({
method: 'post'.url: '/quote/getList'.data: {}});returnlist; }}Copy the code
Synchronize Note data to client calendar:
export class NativeService implements INativeService {
// Synchronize to calendar
@p(a)public syncCalendar(params: SyncCalendarParams, onSuccess: () = > void) :void {
const cb = async (errCode: number) = > {const msg = NATIVE_ERROR_CODE_MAP[errCode];
Vue.prototype.$toast(msg);
if(errCode ! = =6000) {
this.errorReport(msg, 'syncCalendar', params);
} else {
awaitonSuccess(); }}; dsbridge.call('syncCalendar', params, cb); }... }Copy the code
Read a Note detail data from indexDB:
import { noteTranslator } from './translators';
export class NoteService implements INoteService {
public async get(id: number) :Promise<INotebook | undefined> {
const db = await createDB();
const notebook = await db.getFromIndex('notebooks'.'id', id);
return noteTranslator(notebook!);
}
}
Copy the code
Note Translator belongs to the translator layer and is used to correct the note data returned by the interface. The definition is as follows:
export function noteTranslator(item: INotebook) {
// item.themeColor = item.color;
return item;
}
Copy the code
When the back-end API is still under development, we can use local storage technologies such as indexDB to simulate the note-IndexDB service, and provide it to the upper Interactors layer to call. When the back-end API is developed, we can use local storage technologies such as indexDB to simulate the note-IndexDB service. You can create a note-server service to replace the previous service. As long as the two services have the same exposed interface and are not over-coupled to the upper Interactors layer, fast switching can be achieved.
Entities layer
Entity is the core concept of domain-driven design. It is the carrier of domain services, which defines the attributes and methods of an individual in a business. For example, in this project, Note and Notebook are entities. The key to distinguishing an object from an entity is whether it has a unique identifier (such as id). The following is the entity Note of this project:
export default class Note {
public id: number;
public name: string;
public deadline: Date | undefined; .constructor(note: INote) {
this.id = note.id;
this.name = note.name;
this.deadline = note.deadline; . }public get isExpire() {
if (this.deadline) {
return this.deadline.getTime() < new Date().getTime(); }}public get deadlineStr() {
if (this.deadline) {
return formatTime(this.deadline); }}}Copy the code
Through the code above you can see, here basically is based on the properties of the entity itself and the derived attribute is given priority to, of course, the entity itself can also have method, used to implement belong to the entity’s own business logic (the author thinks that the business logic can be divided into two parts, part of the business logic belongs to the associated with entities is strong, should be achieved by method in the entity class. The other part of the business logic is more business between entities and can be implemented in the Interactors layer. It is not covered in this project and will not be explained here, but those interested can refer to the article I translated below: Extensible Front End #2: Common Patterns (translation).
In addition, I believe that not all entities should be encapsulated into a class as described above. If the business logic of an entity is simple, there is no need to encapsulate it. For example, the Notebook entity in this project is not encapsulated. Instead, you call the apis provided by the Services layer directly from the Interactors layer. After all, the ultimate goal of these layers is to streamline business logic and improve development efficiency, so there is no need to be too rigid.
Interactors layer
The Interactors layer is the layer responsible for handling business logic and consists primarily of business use cases. In general, Interactor is a singleton that allows us to store some state and avoid unnecessary HTTP calls, provides a way to reset the state properties of the application (for example, restore data if a modification record is lost), and determines when new data should be loaded.
Here is the business for public calls provided by Common’s Interactors layer in this project:
class CommonInteractor {
public static getInstance() {
return this._instance;
}
private static _instance = new CommonInteractor(new CommonService());
private _quotes: any;
constructor(private _service: ICommonService) {}
public async getQuoteList() {
// In singleton mode, some basic fixed interface data is saved in memory to avoid repeated invocation
// Be careful to avoid memory leaks
if (this._quotes ! = =undefined) {
return this._quotes;
}
let response;
try {
response = await this._service.getQuoteList();
} catch (error) {
throw error;
}
this._quotes = response;
return this._quotes; }}Copy the code
As you can see from the above code, the Sevices layer provides a class instance through the Interactors class constructor, which can achieve the decoupling between the two layers, and quickly switch services. Of course, there are some differences between DI and this. But that’s what we need.
In addition, the Interactors layer can obtain the entity class provided by the Entities layer, and provide the View layer with the business logic provided by the Interactors layer. For example, Note’s Interactors layer looks like this:
class NoteInteractor {
public static getInstance() {
return this._instance;
}
private static _instance = new NoteInteractor(
new NoteService(),
new NativeService()
);
constructor(
private _service: INoteService,
private _service2: INativeService
) {}
public async getNote(notebookId: number, id: number) {
try {
const note = await this._service.get(notebookId, id);
if (note) {
return newNote(note); }}catch (error) {
throwerror; }}}Copy the code
Of course, this layered architecture is not a silver bullet. It is mainly suitable for scenarios where entity relationships are complex and interactions are relatively stereotypical, such as enterprise software. On the contrary, simple entity relationships and complex interactions are not suitable for this hierarchical architecture.
In specific business development practices, such domain models and entities are generally determined by the backend students. What we need to do is to be consistent with the backend domain model, but not the same. For example, the same function that is a simple button on the front end can be quite complex on the back end.
It’s also important to be clear that architecture is not the same as the project file structure, which is how you visually separate parts of your application, while architecture is how you conceptually separate your application. You can choose different file structures while maintaining the same architecture. There is no perfect file structure, so choose the one that works for you depending on your project.
At the end, I quote the summary in the article “Front-end Development – Domain Driven Design” of Ant Financial Data Experience technology:
Understand that the purpose of driving domain layer separation is not page reuse, and this must be converted to thinking. The domain layer is not pulled away because it is reused in multiple places. It was pulled away because:
- The domain layer is stable (pages and modules bound to pages are unstable)
- The domain layer is decoupled (pages are coupled, data on pages comes from multiple interfaces, multiple domains)
- The domain layer is extremely complex and deserves to be managed separately (the View layer handles page rendering and page logic control, and is complex enough that the decoupling of the domain layer can lighten the View layer. The view layer is as light as possible is our architect CNFI’s main idea)
- The domain layer can be reused on a layer by layer basis (your code may discard a technical architecture, convert from VUE to React, or launch a mobile version, in which case the domain layer can be reused directly)
- For the continuous evolution of the domain model (the purpose of the model is to make people focus, and the benefit of focus is to strengthen the front-end team’s understanding of the business. Only by thinking about the business process can the business move forward)
Several related libraries are recommended:
react-clean-architecture
business-rules-package
ddd-fe-demo
Recommend a few related articles:
Front-end Architecture – Making refactoring less painful
Extensible Front End #1– Architectural Foundation
Extensible Front End #2– Common Patterns
Practice of domain driven design in Internet business development
Front-end development – Domain driven design
Application of domain driven design in the front end
PS:
Next steps for the Mobile Web Best Practices Project:
Practice APP offline package technology, that is, front-end static resources are integrated into the client in advance, which can reduce the network loading time of web pages to 0 and greatly improve the user experience of the application.
The project is planned to be completed before the end of this year. Because this project involves front-end, client and back-end, especially the client has a lot of work, so it will take a long time. At that time, all end codes of the whole project will be open source, please look forward to it.