控制器与服务的分离
控制器与服务的分离
Express框架中,控制器负责处理HTTP请求和响应,而服务则专注于业务逻辑。将两者分离可以提高代码的可维护性和可测试性,避免控制器变得臃肿。这种架构模式让开发者能够更清晰地组织代码,特别是在大型项目中。
为什么需要分离
当所有逻辑都写在控制器中时,代码会变得难以维护。例如,一个用户注册的控制器可能包含验证、数据库操作、发送邮件等多个步骤。随着业务逻辑的复杂化,控制器会变得越来越庞大,测试也会变得困难。分离后,控制器只负责接收请求、调用服务和返回响应,而服务则处理具体的业务逻辑。
// 不推荐的写法:所有逻辑都在控制器中
app.post('/register', async (req, res) => {
const { email, password } = req.body;
// 验证输入
if (!email || !password) {
return res.status(400).json({ error: 'Email and password are required' });
}
// 检查用户是否存在
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'User already exists' });
}
// 创建用户
const user = new User({ email, password });
await user.save();
// 发送欢迎邮件
await sendWelcomeEmail(email);
return res.status(201).json({ message: 'User created' });
});
如何实现分离
将业务逻辑移到服务层,控制器只需调用服务。服务通常是独立的类或模块,可以被多个控制器复用。这种分离使得业务逻辑更容易测试,因为可以单独测试服务而不需要模拟HTTP请求。
// services/userService.js
class UserService {
async register(email, password) {
if (!email || !password) {
throw new Error('Email and password are required');
}
const existingUser = await User.findOne({ email });
if (existingUser) {
throw new Error('User already exists');
}
const user = new User({ email, password });
await user.save();
await sendWelcomeEmail(email);
return user;
}
}
// controllers/userController.js
const userService = new UserService();
app.post('/register', async (req, res) => {
try {
const { email, password } = req.body;
const user = await userService.register(email, password);
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
服务层的设计原则
服务应该是无状态的,不依赖于请求或响应对象。它们接收普通参数,返回普通值或Promise。这使得服务可以在任何上下文中使用,而不仅限于HTTP请求。服务之间也可以相互调用,形成更复杂的业务逻辑。
// services/orderService.js
class OrderService {
constructor(userService) {
this.userService = userService;
}
async createOrder(userId, products) {
const user = await this.userService.getById(userId);
if (!user) {
throw new Error('User not found');
}
// 创建订单逻辑...
}
}
错误处理的最佳实践
控制器负责将服务层的错误转换为适当的HTTP响应。服务层抛出带有语义的错误,控制器捕获这些错误并决定如何呈现给客户端。可以使用自定义错误类来区分不同类型的错误。
// errors/AppError.js
class AppError extends Error {
constructor(message, statusCode = 400) {
super(message);
this.statusCode = statusCode;
}
}
// services/userService.js
throw new AppError('User not found', 404);
// controllers/userController.js
app.get('/users/:id', async (req, res) => {
try {
const user = await userService.getById(req.params.id);
res.json(user);
} catch (error) {
res.status(error.statusCode || 500).json({ error: error.message });
}
});
测试的便利性
分离后,可以单独测试服务层而不需要启动Express应用。这使得测试运行更快,更专注于业务逻辑。控制器测试则可以专注于路由和HTTP层面的行为。
// tests/userService.test.js
describe('UserService', () => {
it('should register a new user', async () => {
const userService = new UserService();
const user = await userService.register('test@example.com', 'password');
expect(user.email).toBe('test@example.com');
});
});
// tests/userController.test.js
describe('UserController', () => {
it('should return 400 for invalid input', async () => {
const res = await request(app)
.post('/register')
.send({});
expect(res.statusCode).toBe(400);
});
});
依赖注入的优势
通过构造函数注入依赖,而不是在服务内部直接引入其他模块,可以提高代码的可测试性和灵活性。这使得在测试时可以轻松地替换真实服务为模拟对象。
// 使用依赖注入
class OrderService {
constructor(userService, emailService) {
this.userService = userService;
this.emailService = emailService;
}
}
// 测试时可以注入模拟对象
const mockUserService = { getById: jest.fn() };
const orderService = new OrderService(mockUserService);
中间件的合理使用
虽然控制器和服务已经分离,但Express中间件仍然有其价值。中间件适合处理跨切面关注点,如身份验证、日志记录等。业务逻辑仍应放在服务层。
// 中间件处理身份验证
app.use((req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
// 控制器和服务不需要关心认证逻辑
app.get('/profile', async (req, res) => {
const user = await userService.getProfile(req.userId);
res.json(user);
});
项目结构建议
典型的分离结构可能如下所示:
src/
controllers/
userController.js
orderController.js
services/
userService.js
orderService.js
models/
User.js
Order.js
routes/
userRoutes.js
orderRoutes.js
路由文件负责将路径映射到控制器方法,保持路由定义简洁:
// routes/userRoutes.js
const express = require('express');
const userController = require('../controllers/userController');
const router = express.Router();
router.post('/register', userController.register);
module.exports = router;
性能考量
虽然额外的抽象层会带来轻微的性能开销,但在大多数应用中这种开销可以忽略不计。分离带来的可维护性和可扩展性优势远大于微小的性能损失。对于性能关键路径,仍然可以将部分逻辑直接放在控制器中。
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:路由分层与模块化设计
下一篇:<br>-换行符