Service 是很常見的設計模式,通常會把商業邏輯寫在 Service 層,才不會讓 Controller 過於笨重,這個設計模式在前端領域也有被使用,像是:Angular。下方的圖為 Express 套用 Service 後的運作模式:
在 todo
資料夾下建立 todo.service.ts
,要將 TodoController
的部分邏輯抽離到 Service 中,下方為 todo.service.ts
的程式碼:
import { QueryFindOneAndUpdateOptions } from 'mongoose';
import { TodoModel } from '../../../models/todo.model';
import { TodoDTO } from '../../../dtos/todo.dto';
import { DefaultQuery } from '../../../types/request.type';
export class TodoService {
public async getTodos(limit: number = DefaultQuery.LIMIT, skip: number = DefaultQuery.SKIP): Promise<TodoDTO[]> {
const todos = await TodoModel.find().skip(skip).limit(Math.min(limit, DefaultQuery.MAX_LIMIT));
const dtos = todos.map(todo => new TodoDTO(todo));
return dtos;
}
public async getTodo(id: string): Promise<TodoDTO | null> {
const todo = await TodoModel.findById(id);
const dto = todo ? new TodoDTO(todo) : null;
return dto;
}
public async addTodo(content: string): Promise<TodoDTO> {
const todo = new TodoModel({ content, completed: false });
const document = await todo.save();
const dto = new TodoDTO(document);
return dto;
}
public async completedTodo(id: string, completed: boolean): Promise<TodoDTO | null> {
const options: QueryFindOneAndUpdateOptions = {
new: true,
runValidators: true
};
const todo = await TodoModel.findByIdAndUpdate(id, { completed }, options);
const dto = todo ? new TodoDTO(todo) : null;
return dto;
}
public async removeTodo(id: string): Promise<TodoDTO | null> {
const todo = await TodoModel.findByIdAndRemove(id);
const dto = todo ? new TodoDTO(todo) : null;
return dto;
}
}
可以看到我們將所有從 Model 取資料的邏輯都抽離至 Service 層,並將資料包成 DTO 回傳給 Controller,如此一來 Controller 就可以減輕負擔,職責更為鮮明。下方為修改後的 TodoController
:
import { Request } from 'express';
import { ControllerBase } from '../../../bases/controller.base';
import { TodoService } from './todo.service';
import { ResponseObject } from '../../../common/response/response.object';
import { HttpStatus } from '../../../types/response.type';
export class TodoController extends ControllerBase {
private readonly todoSvc = new TodoService();
public async getTodos(req: Request): Promise<ResponseObject> {
const { limit, skip } = req.query;
const dtos = await this.todoSvc.getTodos(Number(limit), Number(skip));
return this.formatResponse(dtos, HttpStatus.OK);
}
public async getTodo(req: Request): Promise<ResponseObject> {
const { id } = req.params;
const dto = await this.todoSvc.getTodo(id);
if ( !dto ) {
return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
}
return this.formatResponse(dto, HttpStatus.OK);
}
public async addTodo(req: Request): Promise<ResponseObject> {
const { content } = req.body;
const dto = await this.todoSvc.addTodo(content);
return this.formatResponse(dto, HttpStatus.CREATED);
}
public async completedTodo(req: Request): Promise<ResponseObject> {
const { id } = req.params;
const { completed } = req.body;
const dto = await this.todoSvc.completedTodo(id, completed);
if ( !dto ) {
return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
}
return this.formatResponse(dto, HttpStatus.OK);
}
public async removeTodo(req: Request): Promise<ResponseObject> {
const { id } = req.params;
const dto = await this.todoSvc.removeTodo(id);
if ( !dto ) {
return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
}
return this.formatResponse(null, HttpStatus.NO_CONTENT);
}
}
當前資料夾結構如下:
├── src
| ├── index.ts
| ├── app.ts
| ├── app.routing.ts
| ├── + bases
| ├── + common/resonse
| ├── + exceptions
| ├── main
| | └── api
| | ├── todo.controller.ts //本篇修改
| | ├── todo.service.ts //本篇新增
| | └── todo.routing.ts
| ├── + models
| ├── + dtos
| ├── + types
| ├── + environments
| ├── + database
| └── + validators
├── package.json
└── tsconfig.json
為了方便維護系統,我在這邊與大家分享我自己的規範:
在同一個模組下只保有一個專用的 Service,什麼意思呢?以我們的 Todo 來說的話, Todo 這個模組下就只能有一個 TodoService
,不會有其他處理 Todo 模組的 Service,除非是 Todo 的子模組。
若當前子模組沒有專用的 Service,則可以使用上層模組的 Service,這邊做個假設,有個 A 模組且有個叫 B 的 子模組 ,B 模組沒有設置 Service,所以它使用 A 模組的 Service 是可被接受的。
將處理資料的邏輯切割出來交給 Service,有效減少 Controller 的負擔,不過仍然面臨一個問題,就是 Service 目前不僅包含了處理資料的邏輯,還包含了取資料的邏輯,應該要將取資料的邏輯再切割出去,詳細說明會在下篇告訴大家,敬請期待!