阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 控制器与服务的分离

控制器与服务的分离

作者:陈川 阅读数:58748人阅读 分类: Node.js

控制器与服务的分离

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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌