您现在的位置是:网站首页 > 控制器与服务的分离文章详情

控制器与服务的分离

控制器与服务的分离

Express框架中,控制器负责处理HTTP请求和响应,而服务则专注于业务逻辑。将两者分离可以提高代码的可维护性和可测试性,避免控制器变得臃肿。这种分层架构让开发者能够更清晰地组织代码,特别是在大型项目中。

为什么需要分离控制器与服务

控制器直接与路由交互,处理请求参数、验证输入、调用服务并返回响应。如果业务逻辑直接写在控制器中,会导致控制器代码量过大,难以维护。例如,一个用户注册功能可能包含密码加密、验证邮箱、发送欢迎邮件等步骤,这些逻辑全部放在控制器中会让代码变得混乱。

服务层则专注于实现核心业务逻辑,不关心HTTP细节。它接收控制器传递的参数,执行操作并返回结果。这种分离让业务逻辑可以独立于Web框架进行测试和复用。

控制器的最佳实践

控制器应该保持精简,主要职责是:

  1. 解析请求参数
  2. 调用适当的服务
  3. 处理响应
// 不好的做法:业务逻辑直接放在控制器中
router.post('/users', async (req, res) => {
  const { email, password } = req.body;
  
  // 直接进行密码加密
  const hashedPassword = await bcrypt.hash(password, 10);
  
  // 直接操作数据库
  const user = await User.create({ email, password: hashedPassword });
  
  res.status(201).json(user);
});

// 好的做法:控制器只负责协调
router.post('/users', async (req, res) => {
  try {
    const userData = req.body;
    const newUser = await userService.createUser(userData);
    res.status(201).json(newUser);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

服务层的实现

服务层包含应用程序的核心逻辑,通常按业务领域组织。例如,可以有userServiceauthServiceproductService等。

// services/userService.js
const bcrypt = require('bcrypt');
const User = require('../models/User');

module.exports = {
  async createUser(userData) {
    const hashedPassword = await bcrypt.hash(userData.password, 10);
    const user = await User.create({
      email: userData.email,
      password: hashedPassword
    });
    return user;
  },
  
  async getUserById(id) {
    return await User.findById(id);
  }
};

分层架构的优势

  1. 可测试性:服务层可以独立测试,不需要模拟HTTP请求
  2. 可复用性:同样的服务可以被不同控制器调用
  3. 可维护性:业务逻辑变更只需修改服务层,不影响控制器
  4. 团队协作:前端和后端开发者可以并行工作,只需约定接口

实际项目中的应用

在大型项目中,可以进一步细化分层:

src/
├── controllers/    # 控制器
├── services/       # 服务层
├── repositories/   # 数据访问层
├── models/         # 数据模型
└── routes/         # 路由定义

控制器调用服务,服务调用仓储层,形成清晰的依赖关系:

// controllers/userController.js
const userService = require('../services/userService');

exports.getUserProfile = async (req, res) => {
  try {
    const user = await userService.getUserProfile(req.userId);
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

// services/userService.js
const userRepository = require('../repositories/userRepository');

exports.getUserProfile = async (userId) => {
  return await userRepository.findById(userId);
};

// repositories/userRepository.js
const User = require('../models/User');

exports.findById = async (id) => {
  return await User.findById(id).select('-password');
};

错误处理策略

分离架构下,错误处理也需要分层考虑。服务层抛出业务错误,控制器捕获并转换为适当的HTTP响应。

// services/authService.js
exports.login = async (email, password) => {
  const user = await userRepository.findByEmail(email);
  if (!user) {
    throw new Error('用户不存在');
  }
  
  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) {
    throw new Error('密码错误');
  }
  
  return generateToken(user);
};

// controllers/authController.js
exports.login = async (req, res) => {
  try {
    const token = await authService.login(req.body.email, req.body.password);
    res.json({ token });
  } catch (error) {
    res.status(401).json({ error: error.message });
  }
};

中间件的使用

Express中间件可以进一步简化控制器代码。例如,参数验证可以提取到中间件中:

// middlewares/validateRequest.js
const { body } = require('express-validator');

exports.validateCreateUser = [
  body('email').isEmail(),
  body('password').isLength({ min: 6 })
];

// routes/users.js
const { validateCreateUser } = require('../middlewares/validateRequest');
const userController = require('../controllers/userController');

router.post('/users', validateCreateUser, userController.createUser);

依赖注入模式

为了提高可测试性,可以采用依赖注入方式组织服务:

// services/userService.js
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }
  
  async createUser(userData) {
    // 实现逻辑
  }
}

module.exports = UserService;

// app.js
const UserService = require('./services/userService');
const UserRepository = require('./repositories/userRepository');

const userRepository = new UserRepository();
const userService = new UserService(userRepository);

app.use('/api', createRouter({ userService }));

性能考量

分层架构虽然带来了清晰的结构,但也可能引入性能开销。需要注意:

  1. 避免不必要的分层调用
  2. 服务层可以缓存常用数据
  3. 复杂的业务操作可以考虑使用领域事件
// services/orderService.js
const Order = require('../models/Order');

class OrderService {
  constructor(eventEmitter) {
    this.eventEmitter = eventEmitter;
  }
  
  async createOrder(orderData) {
    const order = await Order.create(orderData);
    this.eventEmitter.emit('orderCreated', order);
    return order;
  }
}

与前端架构的对比

这种分层思想与前端框架中的组件/服务分离类似。例如在React中:

// React组件(类似控制器)
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    userService.getUser(userId).then(setUser);
  }, [userId]);
  
  return <div>{user?.name}</div>;
}

// 服务层(与后端服务类似)
const userService = {
  getUser(id) {
    return fetch(`/api/users/${id}`).then(res => res.json());
  }
};

常见误区与解决方案

  1. 过度分层:简单的CRUD操作可能不需要完整的分层

    • 解决方案:根据项目复杂度灵活调整
  2. 循环依赖:服务之间相互调用导致复杂依赖

    • 解决方案:引入领域事件或命令模式
  3. 贫血模型:服务层变成纯粹的过程调用

    • 解决方案:考虑领域驱动设计(DDD)
// 不好的贫血模型示例
class ProductService {
  applyDiscount(product, discount) {
    product.price = product.price * (1 - discount);
    return product;
  }
}

// 更好的富模型示例
class Product {
  applyDiscount(discount) {
    this.price = this.price * (1 - discount);
  }
}

测试策略

分层架构大大简化了测试:

// 测试服务层(不需要启动Express)
describe('UserService', () => {
  let userService;
  
  beforeEach(() => {
    userService = new UserService(mockUserRepository);
  });
  
  it('should create user with hashed password', async () => {
    const user = await userService.createUser({
      email: 'test@example.com',
      password: '123456'
    });
    
    expect(user.password).not.toBe('123456');
  });
});

// 测试控制器(可以模拟请求)
describe('UserController', () => {
  it('should return 201 when user created', async () => {
    mockUserService.createUser.mockResolvedValue({ id: 1 });
    
    const req = { body: { email: 'test@example.com', password: '123456' } };
    const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };
    
    await userController.createUser(req, res);
    
    expect(res.status).toHaveBeenCalledWith(201);
  });
});

与微服务架构的关系

当应用发展为微服务时,服务层的概念可以自然扩展:

  1. 本地服务变为远程服务调用
  2. 控制器变为API网关
  3. 服务间通信使用RPC或消息队列
// 从单体架构的服务调用
const user = await userService.getUser(id);

// 变为微服务架构的调用
const user = await userServiceClient.getUser(id);

演进式架构

项目初期可以简单实现,随着复杂度增加逐步分层:

  1. 第一阶段:控制器直接包含业务逻辑
  2. 第二阶段:提取出服务层
  3. 第三阶段:引入领域层和仓储层
  4. 第四阶段:考虑CQRS或事件溯源
// 演进示例:从简单到复杂
// 1. 初期
router.post('/orders', async (req, res) => {
  const order = await Order.create(req.body);
  res.json(order);
});

// 2. 引入服务层
router.post('/orders', async (req, res) => {
  const order = await orderService.createOrder(req.body);
  res.json(order);
});

// 3. 完整分层
router.post('/orders', 
  validateOrder,
  authMiddleware,
  async (req, res) => {
    try {
      const order = await orderApplicationService.createOrder(
        req.body,
        req.user.id
      );
      res.json(order);
    } catch (error) {
      res.status(400).json({ error: error.message });
    }
  }
);

框架扩展与集成

Express的灵活性允许集成各种技术栈:

// 集成GraphQL
const { graphqlHTTP } = require('express-graphql');
const userSchema = require('./graphql/userSchema');

app.use('/graphql', graphqlHTTP({
  schema: userSchema,
  graphiql: true
}));

// GraphQL解析器可以看作是一种特殊的服务层
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      return await userService.getUser(id);
    }
  }
};

性能监控与日志

分层后可以更方便地添加横切关注点:

// 使用中间件记录控制器执行时间
app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    console.log(`${req.method} ${req.url} - ${Date.now() - start}ms`);
  });
  next();
});

// 服务层可以添加业务指标监控
class ProductService {
  constructor(metricsClient) {
    this.metrics = metricsClient;
  }
  
  async updateProduct(id, data) {
    const start = Date.now();
    try {
      const result = await productRepository.update(id, data);
      this.metrics.timing('product.update', Date.now() - start);
      return result;
    } catch (error) {
      this.metrics.increment('product.update.error');
      throw error;
    }
  }
}

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步