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