Midway is a TypeScript-based Node.js R&D framework developed by Alibaba. Within the group, because it integrates various internal basic services and stability monitoring, and supports FaaS function deployment, it is developed by internal Node.js applications. Preferred frame.
Although Midway combines the two programming paradigms of object-oriented (OOP + Class + IoC) and functional (FP + Function + Hooks), considering that most of the general projects adopt the object-oriented development method, this article also focuses on the object-oriented development method. A paradigm, code design that can be referenced in engineering development.
Design of Project Catalog Based on MVC
In Midway project development, it is generally recommended to use the following working directory to organize business codes. The content of the code in the config directory can be configured correctly and standardly according to your own needs and in combination with the official configuration documentation. I will not explain too much in this article.
- config 配置文件目录,存放不同环境的差异配置信息
- constant 常量存放目录,存放业务常量及国际化业务文案
- controller 控制器存放目录,存放核心业务逻辑代码
- dto 数据传输对象目录,存放外部数据的校验规则
- entity 数据实体目录,存放数据库集合对应的数据实体
- middleware 中间件目录,存放项目中间件代码
- service 服务逻辑存放目录,存放数据存储、局部通用逻辑代码
- util 工具代码存放目录,存放业务通用工具方法
When the request enters, the code corresponding to each directory plays the following functions:
- Middleware is used as the starting logic for general logic execution.
- Then the DTO layer verifies the parameters.
- If there is no abnormality in parameter verification, enter the Controller to execute the overall business logic.
- Database calls, or general business logic with strong integrity will be encapsulated into the Service layer for easy reuse.
- Tool methods, constants, configuration and database entities serve as the underlying support of the project, returning data to the Service or Controller.
- The Controller spit out the response result. If the response is abnormal, Middleware will carry out the logic.
- Finally, the response data is spit out and returned to the user.
To sort it out, you can classify it like this. In MVC, C layer corresponds to Middleware + DTO + Controller ; M layer corresponds to Service; V layer because the general backend only provides external interfaces, there will not be too many static The page is exposed, so can temporarily ignore . Of course, the Service layer has a certain boundary confusion. It does not only include the Model layer, otherwise we will directly call it the Model layer. In the Service, I will also put some abstract and reusable logic into it. , to alleviate the problem that Controller logic is too cumbersome in complex business.
After understanding the design thinking of the above Midway code catalog, we will share some experience in code design for each part.
Code suggestions for the Middleware layer
During development, business middleware can be designed and developed by itself, which depends on your business requirements. However, code execution exceptions can be handled gracefully through the following solutions.
Exceptional fault tolerance middleware
code execution exception refers to the execution error that may occur during the execution of the business code. Generally speaking, in order to solve this potential risk, we can add try catch statement to the outer layer of logic for processing. In many projects, a large number of try catch statements are added in order to handle exceptions; in many projects, the problem of exception handling is not considered, and there is no fault tolerance of try catch at all.
// 以下代码缺少异常兜底冗错
@Get('/findOne')@Validate()
async findOne(@Query(ALL) appParams: AppFindDTO) {
const { id } = appParams;
const app = await this.appService.findOneAppById(id);
return getReturnValue(true, app);
}
// 以下代码每个函数都要有一个 try catch 包裹
@Get('/findOne')@Validate()
async findOne(@Query(ALL) appParams: AppFindDTO) {
try {
const { id } = appParams;
const app = await this.appService.findOneAppById(id);
return getReturnValue(true, app);
} catch(e) {
return getReturnValue(false, e.message);
}
}
Using middleware, you can solve the above two problems , you can write the following middleware:
@Provide('errorResponse')
@Scope(ScopeEnum.Singleton)
export class ErrorResponseMiddleware {
resolve() {
return async (ctx: FaaSContext, next: () => Promise<any>) => {
try {
await next();
} catch (error) {
ctx.body = getReturnValue(
false,
null,
error.message || '系统发生错误,请联系管理员'
);
}
};
}
}
Add this middleware code to the execution logic of the program. When writing code, you no longer need to pay attention to the problem of code execution exceptions. The middleware will help you catch program execution exceptions and standardize the return. At the same time, you can also do abnormal log collection or real-time warning here, and expand more functions. Therefore, this middleware design is strongly recommended to be used uniformly in the project.
Code suggestions for DTO layers
DTO layer, that is, the data transmission object layer, in Midway, is mainly used to verify the request parameters of POST, GET and other requests. In the process of practice, there are two aspects that need to be paid attention to in the design: reasonable code reuse and clear code responsibility division.
Reasonable code reuse
First, let's look at the code design of the unreasonable DTO layer:
// 第一种问题:
// 分页的校验,看起来很难懂,未来很多地方都要用,这么写无法复用
export class AppsPageFindDTO {
@Rule(RuleType.string().required())
siteId: number;
@Rule(RuleType.number().integer().empty('').default(1).greater(0))
pageNum: number;
@Rule(RuleType.number().integer().empty('').default(20).greater(0))
pageSize: number;
}
// 第二种问题
// 对参数的校验,本身应该是 DTO 层面校验的,放到业务中不合理
// 同时,对逗号间隔的 id 进行校验,这是常见功能,放在这难以复用
@Get('/findMany')@Validate()
async findMany(@Query(ALL) appParams: AppsFindDTO) {
const { ids } = appParams;
const newIds = ids.split(',');
if(!Array.isArray(newIds)) {
return getReturnValue(false, null, 'ids 参数不符合要求');
}
const app = await this.appService.findOneAppByIds(newIds);
return getReturnValue(true, app);
}
It is recommended to use the following methods to write the code of the DTO layer. First, encapsulate common reusable rules:
// 必填字符串规则
export const requiredStringRule = RuleType.string().required();
// 页码校验规则
export const pageNoRule = RuleType.number().integer().default(1).greater(0);
// 单页显示内容数量校验规则
export const pageSizeRule = RuleType.number().integer().default(20).greater(0);
// 逗号间隔的 id 进行校验的规则扩展,起名为 stringArray
RuleType.extend(joi => ({
base: joi.array(),
type: 'stringArray',
coerce: value => ({
value: value.split ? value.split(',') : value,
}),
}));
Then in your DTO definition file, the code can be reduced to:
// 分页的校验的逻辑可以精简为这种写法
export class AppsPageFindDTO {
@Rule(requiredStringRule)
siteId: number;
@Rule(pageNoRule)
pageNum: number;
@Rule(pageSizeRule)
pageSize: number;
}
// 逗号间隔的 id 字符串校验,可以改为如下写法
export class AppsFindDTO {
@Rule(RuleType.stringArray())
ids: number;
}
@Get('/findMany')@Validate()
async findMany(@Query(ALL) appParams: AppsFindDTO) {
const { ids } = appParams;
const app = await this.appService.findOneAppByIds(ids);
return getReturnValue(true, app);
}
Compared with the initial code, it is much simpler, and all the verification rules can be reused in the future. This is the recommended DTO layer code design.
Clear division of responsibilities
The core responsibility of DTO is to verify the input parameters, and its responsibility is limited to this , but many times, we can see such code:
// Controller 层代码逻辑
@Post('/createRelatedService')
@Validate()
async createRelatedService(@Body(ALL) requestBody: AppRelationSaveDTO) {
// 判断当前站点和应用的关联是否存在
const appRelation = new AppRelation();
const saveResult = await this.appServiceService.saveAppRelation(
appRelation,
requestBody,
);
return getReturnValue(true, saveResult);
}
// Service 层的代码逻辑
async saveAppRelation(
relation: AppRelation,
params: AppRelationSaveDTO,
) {
const { appId, serviceId } = params;
appId && (relation.appId = appId);
serviceId && (relation.serviceId = serviceId);
const result = await this.appServiceRelation.save(relation);
return result;
}
In the method in the Service layer, the DTO of AppRelationSaveDTO is used as the type of Typescript to help the code type verification. The problem with this code is that the DTO layer assumes additional responsibilities beyond data verification. The Service layer itself is concerned with how data is stored, and now the Service layer also needs to pay attention to how external data is transmitted. Obviously, the code responsibilities are confusing.
The way to optimize is also very simple, we can improve the code:
// Controller 层代码逻辑
@Post('/createRelatedService')
@Validate()
async createRelatedService(@Body(ALL) requestBody: AppRelationSaveDTO) {
// 判断当前站点和应用的关联是否存在
const { appId, serviceId } = requestBody;
const appRelation = new AppRelation();
const saveResult = await this.appServiceService.saveAppRelation(
appRelation,
appId,
serviceId
);
return getReturnValue(true, saveResult);
}
// Service 层的代码逻辑
async saveAppRelation(
relation: AppRelation,
appId: string,
serviceId: stirng
) {
const { appId, serviceId } = params;
appId && (relation.appId = appId);
serviceId && (relation.serviceId = serviceId);
const result = await this.appServiceRelation.save(relation);
return result;
}
The parameter types of the Service layer are no longer described by DTO, and the code logic is very clear: the Controller layer is responsible for extracting the necessary data; the Service layer is responsible for obtaining the necessary data for addition, deletion, modification and checking; and the DTO layer is only responsible for the data Validation responsibilities.
Code suggestions for the control and service layers
The design recommendations of the Controller and Service layers may be quite controversial. Here I only express my personal opinion: Controller is a controller, so business logic should be written in the Controller, and the Service layer, as a service layer, should be abstracted The logic is placed in it (such as database operations, or reusable code). That is to say, the Controller layer should store the one-time logic of business customization, and the Service layer should store the reusable business logic .
The responsibilities of the control layer and the service layer are clear
Around this idea, give a design example of optimized code for reference:
// 控制器层代码
@Post('/create')
@Validate()
async update(@Body(ALL) appBody: AppSaveDTO) {
const { code, name, description } = appBody;
const saveResult = await this.appService.saveApp(
code, name, description
);
return getReturnValue(true, saveResult);
}
// 服务层代码
async saveApp(code: sting, name: string, description: string) {
const app = await this.findOneAppByCode(code);
app.code = code;
app.name = name;
app.description = description;
const result = await this.appModel.save(app);
return result;
}
This code is actually to update a piece of information, and the code, name and description must be updated all at once. In this way, the Service layer is actually coupled with the Controller. How it is stored is actually the business logic, which should be decided by the Controller, so It is recommended to modify the code to the following:
// 控制器层代码
@Post('/create')
@Validate()
async update(@Body(ALL) appBody: AppSaveDTO) {
const { code, name, description } = appBody;
const app = await this.appService.findOneAppByCode(code);
app.code = code;
app.name = name;
app.description = description;
const saveResult = await this.appService.saveApp(app);
return getReturnValue(true, saveResult);
}
// 服务层代码
async saveApp(app: App) {
const result = await this.appModel.save(app);
return result;
}
In this way, compared to the previous code, Controller focuses more on business; Service focuses more on service, and can be better reused. This is a design idea that you can refer to when writing code in the controller and service layers.
One-to-one matching between controller layer and service layer
When writing Midway code, there is such a flexibility: controllers can call multiple services, and services can call each other. That is, a piece of code in the service layer may be called in any controller, or in any service layer. This relatively strong flexibility will eventually lead to unclear code hierarchy and inconsistent coding methods, which will eventually lead to weakened system maintainability.
In order to avoid the problems that may be caused by excessive flexibility, we can make certain constraints from the norm. My current thinking is that the controller only calls its own service layer, and if it needs the capabilities of other service layers, it forwards in its own service layer. After doing this, the code of a service layer can only be called by its own controller, or by other service layers. The flexibility of calling is reduced from N2 to N, and the code is relatively more controllable.
Still through the code example:
// 控制器中的函数方法
@Post('/create')
@Validate()
async create(@Body(ALL) siteBody: SiteCreateDTO) {
// 用到一个 ACL 的服务层
const hasPermission = await this.aclService.checkManagePermission('site');
if (!hasPermission) {
return getReturnValue(false, null, '您无权限,无法创建');
}
const { name, code } = siteBody;
const site = new Site();
site.name = name;
site.code = code;
// 用到自身的服务层
const result = await this.siteService.createSite(site);
return getReturnValue(true, result);
}
If the code is designed in this way, the ACL service is used in the business code to verify permissions, then with the development of the business, the aclService layer may be coupled with more and more custom logic, because all permission verification is performed by a method Provided, if there are many calling scenarios, there will definitely be customization requirements.
So a more reasonable and extensible code could be changed to the following:
// 控制器中的函数方法
@Post('/create')
@Validate()
async create(@Body(ALL) siteBody: SiteCreateDTO) {
// 用到自身的服务层
const hasPermission = await this.siteService.checkManagePermission();
if (!hasPermission) {
return getReturnValue(false, null, '您无权限,无法创建');
}
const { name, code } = siteBody;
const site = new Site();
site.name = name;
site.code = code;
// 用到自身的服务层
const result = await this.siteService.createSite(site);
return getReturnValue(true, result);
}
// 自身服务层的代码
async checkManagePermission(): Promise<boolean> {
const hasPermission = await this.aclService.checkUserPermission('site');
return hasPermission;
}
In its own service layer, adding a layer of forwarding code can not only constrain the flexibility of the code, but also can be directly extended here when the customized logic is increased, so it is a more reasonable code design.
Code Design of Database Query
Use logical table associations
In Midway, the integrated TypeORM database framework provides database operation syntax such as OneToOne and OneToMany to help you automatically generate Join statements and manage associations between tables.
But in the business system, I do not recommend using this kind of direct table join statement, because it is easy to generate slow SQL and affect the performance of the system, so it is recommended to use the logical table association method to query the associated data in database operations. Here is the code example directly:
@Get('/findRelatedServices')
@Validate()
async findRelatedServices(@Query(ALL) appParams: AppServicesFindDTO) {
const { id } = appParams;
// 寻找关联关系内容
const relations = await this.appService.findAppRelatedServices(id);
// 从关联关系中找到另一张表关联的 id 合集
const serviceIds = relations.map(item => item.serviceId);
// 去另一张表取数据拼装
const services = await this.appService.findServicesByIds(serviceIds);
// 返回最终数据
return getReturnValue(true, {services});
}
Although this kind of query has more code than Join, the logic is all reflected in the code, and the performance is very good, so it is recommended to use this database operation design in development.
Usage of constants
Constants are very commonly used in server-side development. Some enumerations are expressed semantically through constants. This basic content will not be repeated. I will mainly talk about the idea of using constants to manage business prompts.
business reminder copy
Complicated projects may eventually go international. If there are too many hard-coded text prompts in the code, and finally doing internationalization, you still need to invest energy in modification, so it is better to prepare the project in advance at the beginning of development. , very simple, you just need to extract all the text prompts to the constant file for management.
// 不推荐这种写法,文字和业务耦合
@Post('/create')
@Validate()
async create(@Body(ALL) appBody: AppSaveDTO) {
const { code, name } = appBody;
const appWithSameCode = await this.appService.findOneAppByCode(code);
if (appWithSameCode) {
// 文字和业务耦合在一起
return getReturnValue(false, null, 'code 已存在,无法重复创建!');
}
const app = new App();
const saveResult = await this.appService.saveApp(app, name);
return getReturnValue(true, saveResult);
}
// 推荐这种写法,文字和业务解耦
@Post('/create')
@Validate()
async create(@Body(ALL) appBody: AppSaveDTO) {
const { code, name } = appBody;
const appWithSameCode = await this.appService.findOneAppByCode(code);
if (appWithSameCode) {
// 文字拆离到常量中管理,实现解耦
return getReturnValue(false, null, APP_MESSAGES.CODE_EXIST);
}
const app = new App();
const saveResult = await this.appService.saveApp(app, name);
return getReturnValue(true, saveResult);
}
A small change can make a big difference to your code, and this technique is highly recommended.
Summarize
In complex project development, choosing a good development framework is only the first step, and writing the code is the most difficult thing. This article summarizes my thoughts on how to write good services during the development process using the Midway framework in the past year. Some thoughts and coding skills of the terminal code, I hope it can inspire you. If you have any challenges or questions, please leave a message for discussion.
Author: ES2049 | Dell
The article can be reproduced at will, but please keep the original link.
You are very welcome to join ES2049 Studio with passion. Please send your resume to caijun.hcj@alibaba-inc.com .
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。