iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 19
0
Software Development

今晚我想來點 Express 佐 MVC 分層架構系列 第 19

[今晚我想來點 Express 佐 MVC 分層架構] DAY 19 - Service Layer Pattern

什麼是 Service?

Service 是很常見的設計模式,通常會把商業邏輯寫在 Service 層,才不會讓 Controller 過於笨重,這個設計模式在前端領域也有被使用,像是:Angular。下方的圖為 Express 套用 Service 後的運作模式:
https://ithelp.ithome.com.tw/upload/images/20200903/20119338ZTzwzP1QlJ.png

設計 TodoService

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

為了方便維護系統,我在這邊與大家分享我自己的規範:

只有一個

在同一個模組下只保有一個專用的 Service,什麼意思呢?以我們的 Todo 來說的話, Todo 這個模組下就只能有一個 TodoService,不會有其他處理 Todo 模組的 Service,除非是 Todo 的子模組。
https://ithelp.ithome.com.tw/upload/images/20200903/20119338LeBlXkwXnh.png

可使用上層 Service

若當前子模組沒有專用的 Service,則可以使用上層模組的 Service,這邊做個假設,有個 A 模組且有個叫 B 的 子模組 ,B 模組沒有設置 Service,所以它使用 A 模組的 Service 是可被接受的。
https://ithelp.ithome.com.tw/upload/images/20200903/201193388Wye6G4pkN.png

小結

將處理資料的邏輯切割出來交給 Service,有效減少 Controller 的負擔,不過仍然面臨一個問題,就是 Service 目前不僅包含了處理資料的邏輯,還包含了取資料的邏輯,應該要將取資料的邏輯再切割出去,詳細說明會在下篇告訴大家,敬請期待!


上一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 18 - 整合 Express MVC
下一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 20 - Repository Pattern
系列文
今晚我想來點 Express 佐 MVC 分層架構30

尚未有邦友留言

立即登入留言