05 — 分层架构设计思想

26 阅读4分钟

5.2 四层架构

我们把后端分成四层:

┌─────────────────────────┐
│      Router 路由层      │  URL → Controller 方法
├─────────────────────────┤
│   Controller 控制层    │  解析请求 → 调 Service → 返回响应
├─────────────────────────┤
│    Service 服务层      │  业务逻辑、参数校验、多模型协调
├─────────────────────────┤
│     Model 模型层       │  数据 CRUD(操作数据库/内存)
└─────────────────────────┘

每层的职责

层级职责关心什么不关心什么
RouterURL 与 HTTP 方法映射路由怎么组织、怎么挂载业务逻辑
Controller请求与响应怎么拿参数、怎么返回格式数据存在哪、业务规则是什么
Service业务逻辑业务规则、参数校验、事务HTTP 协议、URL 长什么样
Model数据操作怎么存、怎么查、怎么改业务是什么、给谁用

调用方向

Controller → Service → Model

只能上层调用下层,不能反过来。

Service 不能 import Controller,Model 不能 import Service。否则就乱了。


5.3 为什么 Model 层不能直接暴露给 Controller

很多人会问:Service 层好像只是转调一下 Model,有必要吗?

简单的 CRUD 看起来确实像多余的。但业务一复杂,Service 层的价值就出来了:

场景1:创建项目时还要记录操作日志

// 没有 Service 层,Controller 里要写:
async function createProject(req, res) {
  await projectModel.create(req.body);
  await auditLogModel.create({ action: 'create_project', ... });
  // 还可能发通知、加权限...
}

有了 Service 层:

// Controller 只关心调用和返回
async function create(req, res) {
  const id = await projectService.create(req.body);
  sendOk(res, { id });
}

// Service 管业务逻辑
async function create(data) {
  const project = await projectModel.create(data);
  await auditLogModel.record('create_project', project.id);
  return project;
}

场景2:不同接口共用同一段业务逻辑

比如"创建项目"的校验逻辑,在 API 创建和批量导入时都要用。有 Service 层就直接复用。

场景3:换数据库不影响业务逻辑

如果哪天从 MySQL 换成 MongoDB,只需要改 Model 层,Service 和 Controller 不用动。


5.4 按模块组织 vs 按类型组织

按类型组织(不推荐)

src/
├── controllers/
│   ├── project-controller.ts
│   └── user-controller.ts
├── services/
│   ├── project-service.ts
│   └── user-service.ts
└── models/
    ├── project-model.ts
    └── user-model.ts

看起来整齐,但改一个项目相关的功能要跳 3 个文件夹。模块多了之后找代码很痛苦。

按模块组织(推荐)

src/
├── project/
│   ├── project-router.ts
│   ├── project-controller.ts
│   ├── project-service.ts
│   └── project-model.ts
└── user/
    ├── user-router.ts
    ├── user-controller.ts
    ├── user-service.ts
    └── user-model.ts

一个模块的所有代码在一个文件夹里,新增和删除都很方便。

我们教程里采用的是按业务模块 + 按层的混合方式:

src/
├── routers/project/
├── controllers/project/
├── services/project/
└── models/project/

每个层下面按模块分子文件夹。这样既保持了分层清晰,又能按模块找到对应代码。


5.5 依赖注入

什么是依赖注入

简单说:一个类需要什么依赖,从外面传进来,不要自己 new。

反面例子:

class ProjectService {
  private projectModel = new ProjectModel();  // ❌ 自己创建依赖

  list() {
    return this.projectModel.list();
  }
}

正面例子:

class ProjectService {
  constructor(private readonly projects: ProjectModel) {}  // ✅ 从外面传进来

  list() {
    return this.projects.list();
  }
}

为什么要依赖注入

  1. 好测试:测试时可以传 mock 对象
  2. 解耦:Service 不关心 Model 怎么创建的
  3. 灵活:同一个 Model 实例可以被多个 Service 共享

在哪里组装依赖

bootstrap.ts 里统一创建所有实例:

// src/bootstrap.ts
import { ProjectModel } from './models/project/project-model.js';
import { ProjectService } from './services/project/project-service.js';
import { ProjectController } from './controllers/project/project-controller.js';

export function bootstrap() {
  // 1. 创建 Model
  const projectModel = new ProjectModel();

  // 2. 创建 Service,注入 Model
  const projectService = new ProjectService(projectModel);

  // 3. 创建 Controller,注入 Service
  const projectController = new ProjectController(projectService);

  return {
    projectModel,
    projectService,
    projectController,
  };
}

然后路由里用传进来的 controller:

export function createProjectRouter(controller: ProjectController) {
  const router = Router();
  router.get('/list', controller.list);
  router.post('/', controller.create);
  // ...
  return router;
}

不需要用 NestJS 那种复杂的 DI 容器,手动组装就够用了。


5.6 怎么判断代码放哪层

一个简单的判断方法:

如果把这个项目的 Web 框架从 Express 换成 Koa,这部分代码需不需要改?

  • 要改 → Controller 层 / Router 层
  • 不用改 → Service 层 / Model 层

再问一个问题:

如果把数据库从 MySQL 换成 MongoDB,这部分代码需不需要改?

  • 要改 → Model 层
  • 不用改 → Service 层 / Controller 层

这样想一下,代码放哪层就清楚了。


5.7 小结

  • 分层的目的是解耦,让每层只关心自己的事
  • 四层:Router → Controller → Service → Model,只能上层调下层
  • Service 层在简单 CRUD 里像多余的,但业务复杂了就体现价值
  • 按模块组织目录,比按类型组织更好维护
  • 依赖注入:依赖从外面传进来,不要自己 new,方便测试和解耦