【Node.js从基础到高级运用】十六、代码组织与项目结构(服务层和仓储模式)

引言

在Node.js项目中,除了MVC之外,还有一些其他流行的设计模式,可以帮助我们更好地组织代码和项目结构。本次我们将探讨服务层(Service Layer)仓储模式(Repository Pattern)。这两种模式非常适用于构建结构清晰、可维护性强的大型应用。

服务层(Service Layer)

服务层是位于应用程序的业务逻辑层和表示层之间的一个逻辑层。它的主要职责是实现应用程序的业务逻辑,为表示层(如Web API)提供数据和操作。通过引入服务层,我们可以将业务逻辑从表示层中分离出来,提高代码的复用性和维护性。

示例代码

假设我们有一个简单的用户管理系统,需要实现用户的增删改查功能。下面是使用服务层设计模式的一个实例:

userModel.js - 定义用户模型

// 引入mongoose,用于定义模型和操作MongoDB
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  username: { type: String, required: true },
  password: { type: String, required: true },
  email: { type: String, required: true }
});

// 导出用户模型
module.exports = mongoose.model('User', userSchema);

userRepository.js - 实现仓储模式,封装对用户模型的直接操作

// 引入用户模型
const User = require('./userModel');

class UserRepository {
  async createUser(userData) {
    const user = new User(userData);
    return await user.save(); // 保存用户到数据库
  }

  async findUserById(userId) {
    return await User.findById(userId); // 根据ID查找用户
  }

  // 其他需要的用户操作方法...
}

// 导出仓储实例
module.exports = new UserRepository();

userService.js - 定义服务层,封装业务逻辑

// 引入用户仓储
const userRepository = require('./userRepository');

class UserService {
  async registerUser(userData) {
    // 可以在这里添加注册用户前的业务逻辑,如校验数据格式等
    return await userRepository.createUser(userData); // 创建用户
  }

  async getUserById(userId) {
    // 可以添加获取用户信息前的业务逻辑,如检查用户是否存在等
    return await userRepository.findUserById(userId); // 查找用户
  }

  // 其他业务逻辑方法...
}

// 导出服务实例
module.exports = new UserService();

userController.js - 控制器,处理来自前端的请求,通过服务层与业务逻辑交互

// 引入服务层
const userService = require('../models/userService');

const express = require('express');
const router = express.Router();

// 注册用户的路由处理
router.post('/users', async (req, res) => {
  try {
    const user = await userService.registerUser(req.body);
    res.status(201).send(user);
  } catch (error) {
    res.status(500).send(error);
  }
});

// 获取用户信息的路由处理
router.get('/users/:id', async (req, res) => {
  try {
    const user = await userService.getUserById(req.params.id);
    if (!user) {
      return res.status(404).send('User not found');
    }
    res.send(user);
  } catch (error) {
    res.status(500).send(error);
  }
});

// 导出路由
module.exports = router;

test16.js - 创建运行示例

const express = require('express');
const mongoose = require('mongoose');
const userRoutes = require('./controllers/userController');

const app = express();
app.use(express.json());
app.use(userRoutes);
//在Mongoose 4.x版本中,useNewUrlParser和useUnifiedTopology这两个选项并没有被引入
mongoose.connect('mongodb://localhost:27017/userDB');

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

module.exports = app;

开启mongo服务

mongo

注册
在这里插入图片描述
使用_id查找
在这里插入图片描述

仓储模式(Repository Pattern)

仓储模式用于将数据访问逻辑抽象化,并将其从业务逻辑中分离出来。这样,业务逻辑就不需要直接与数据存储交互,而是通过仓储接口进行。这使得代码更容易维护和测试,同时也更容易实现数据访问技术的更换,比如从一个数据库切换到另一个数据库。

在上面的例子中,userRepository.js文件就是实现了仓储模式的典型例子。通过这种方式,我们可以看到业务逻辑(在userService.js中实现)和数据访问逻辑(在userRepository.js中实现)是如何被清晰地分离开来的。

测试用例

// 引入mongoose,用于操作MongoDB数据库
const mongoose = require('mongoose');
// 引入mongodb-memory-server,用于创建MongoDB内存服务器,便于进行测试
const { MongoMemoryServer } = require('mongodb-memory-server'); //版本@6.9.6
// 引入supertest,用于模拟HTTP请求
const supertest = require('supertest');
// 引入你的app,用于测试
const app = require('../test16');
// 引入用户模型
const User = require('../models/userModel');

// 定义一个变量,用于存储MongoDB内存服务器的实例

let mongoServer;

// 在所有测试用例执行前,启动MongoDB内存服务器并连接
beforeAll(async () => {
  mongoServer = new MongoMemoryServer();
  const mongoUri = await mongoServer.getUri();
  mongoose.connect(mongoUri);
});

// 在所有测试用例执行完毕后,断开数据库连接并停止MongoDB内存服务器
afterAll(async () => {
  mongoose.disconnect();
  await mongoServer.stop();
});

// 定义一个测试集,描述用户管理功能
describe('User Management', () => {
  // 定义一个测试用例,测试创建新用户的功能
  it('should create a new user', async () => {
    // 使用supertest模拟POST请求,创建新用户
    const res = await supertest(app)
      .post('/users')
      .send({
        username: 'testUser',
        email: 'test@example.com',
        password: 'password123',
      });

    // 断言:响应的状态码应为201,表示创建成功
    expect(res.statusCode).toEqual(201);
    // 断言:响应的数据中应包含用户名和邮箱
    expect(res.body).toHaveProperty('username', 'testUser');
    expect(res.body).toHaveProperty('email', 'test@example.com');
  });

  // 定义一个测试用例,测试通过ID查询用户的功能
  it('should retrieve a user by id', async () => {
    // 先创建一个新用户,用于测试查询功能
    const user = new User({
      username: 'findByIdTest',
      email: 'findbyid@example.com',
      password: 'password123',
    });
    await user.save();

    // 使用supertest模拟GET请求,查询刚才创建的用户
    const res = await supertest(app)
      .get(`/users/${user._id}`);

    // 断言:响应的状态码应为200,表示查询成功
    expect(res.statusCode).toEqual(200);
    // 断言:响应的数据中应包含用户名和邮箱
    expect(res.body).toHaveProperty('username', 'findByIdTest');
    expect(res.body).toHaveProperty('email', 'findbyid@example.com');
  });
});

注意
在运行测试用例之前test16.js文件中包含了一个在应用启动时就会被执行的数据库连接操作。这可能是导致“Trying to open unclosed connection”错误的原因:

// mongoose.connect('mongodb://localhost:27017/userDB');

// app.listen(3000, () => {
//   console.log('Server is running on port 3000');
// });

// app.listen(3000, () => {
//   console.log('Server is running on port 3000');
// });

总结

通过引入服务层和仓储模式,我们不仅提高了代码的可维护性和可测试性,也使得业务逻辑和数据访问逻辑的分离更加清晰。这种分离是构建大型、可扩展应用的关键,能够帮助开发团队更有效地协作开发和维护项目。

  • 12
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值