阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 代码组织与架构设计原则

代码组织与架构设计原则

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

代码组织与架构设计原则

Express作为Node.js最流行的Web框架之一,其灵活性和轻量级特性使得开发者可以快速构建应用。但随着项目规模扩大,合理的代码组织和架构设计成为维护性、扩展性的关键因素。良好的架构能显著降低后期维护成本,提升团队协作效率。

分层架构原则

Express应用通常采用分层架构模式,将不同职责的代码分离到独立层次。典型的三层结构包括:

  1. 路由层:处理HTTP请求和响应
  2. 服务层:包含业务逻辑
  3. 数据访问层:负责与数据库交互
// 路由层示例
router.get('/users/:id', async (req, res) => {
  try {
    const user = await userService.getUserById(req.params.id);
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// 服务层示例
class UserService {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }

  async getUserById(id) {
    return this.userRepository.findById(id);
  }
}

// 数据访问层示例
class UserRepository {
  async findById(id) {
    return db.query('SELECT * FROM users WHERE id = ?', [id]);
  }
}

这种分层结构使得各层职责单一,便于单元测试和代码复用。修改数据访问实现时不会影响业务逻辑,调整业务规则也不会波及路由处理。

单一职责原则

每个模块、类或函数应该只有一个引起变化的原因。在Express中,这意味着:

  • 路由处理函数只负责接收请求和返回响应
  • 业务逻辑集中在服务类中
  • 数据操作封装在专门的仓库类

违反单一职责的典型表现是"胖控制器":

// 不良实践:路由处理函数包含过多职责
router.post('/products', async (req, res) => {
  // 验证输入
  if (!req.body.name || !req.body.price) {
    return res.status(400).json({ error: 'Missing fields' });
  }
  
  // 业务逻辑
  const discount = req.body.price > 100 ? 0.9 : 1;
  const finalPrice = req.body.price * discount;
  
  // 数据操作
  const product = await db.query(
    'INSERT INTO products (name, price) VALUES (?, ?)',
    [req.body.name, finalPrice]
  );
  
  // 发送通知
  await emailService.sendNewProductNotification(product);
  
  res.status(201).json(product);
});

重构后符合单一职责原则的代码:

// 路由层
router.post('/products', productController.createProduct);

// 控制器
class ProductController {
  constructor(productService) {
    this.productService = productService;
  }

  async createProduct(req, res) {
    try {
      const product = await this.productService.createProduct(req.body);
      res.status(201).json(product);
    } catch (error) {
      res.status(400).json({ error: error.message });
    }
  }
}

// 服务层
class ProductService {
  constructor(productRepository, notificationService) {
    this.productRepository = productRepository;
    this.notificationService = notificationService;
  }

  async createProduct(productData) {
    this.validateProductData(productData);
    const finalPrice = this.applyDiscount(productData.price);
    const product = await this.productRepository.create({
      ...productData,
      price: finalPrice
    });
    await this.notificationService.notifyNewProduct(product);
    return product;
  }
}

依赖注入原则

依赖注入(DI)是实现松耦合架构的重要手段。Express中可以通过构造函数注入依赖:

// 依赖注入示例
const express = require('express');
const { UserService } = require('./services/user.service');
const { UserRepository } = require('./repositories/user.repository');
const { UserController } = require('./controllers/user.controller');

const app = express();
const userRepository = new UserRepository(db);
const userService = new UserService(userRepository);
const userController = new UserController(userService);

app.get('/users/:id', (req, res) => userController.getUser(req, res));

这种方式便于:

  • 单元测试时可以轻松注入mock对象
  • 替换实现时不影响其他组件
  • 明确组件间的依赖关系

模块化组织

Express应用的文件结构应该反映其架构设计。常见的组织方式:

/src
  /config         # 配置文件
  /controllers    # 路由控制器
  /services       # 业务服务
  /repositories   # 数据访问
  /models         # 数据模型
  /middlewares    # 中间件
  /routes         # 路由定义
  /utils          # 工具函数
  app.js          # 应用入口

每个模块应该:

  • 有明确的单一职责
  • 通过index.js暴露公共接口
  • 保持内部实现私有
// 模块示例:/services/user.service.js
class UserService {
  // 实现细节
}

module.exports = { UserService };

// /services/index.js
const { UserService } = require('./user.service');
const { ProductService } = require('./product.service');

module.exports = {
  UserService,
  ProductService
};

中间件的合理使用

Express中间件是强大的功能,但需要谨慎组织:

  1. 全局中间件:应用于所有路由的中间件(如日志、body解析)
  2. 路由中间件:特定路由组的中间件(如认证)
  3. 错误处理中间件:专门处理错误的中间件
// 全局中间件
app.use(express.json());
app.use(requestLogger);

// 路由组中间件
const authRouter = express.Router();
authRouter.use(authenticate);
authRouter.get('/profile', profileController.getProfile);

// 错误处理中间件
app.use((err, req, res, next) => {
  logger.error(err.stack);
  res.status(500).send('Something broke!');
});

避免在中间件中嵌入业务逻辑,中间件应该只处理横切关注点(cross-cutting concerns)如:

  • 认证授权
  • 请求日志
  • 数据验证
  • 响应格式化

配置管理

将配置与代码分离是重要的架构原则。Express应用通常需要管理:

  • 数据库连接配置
  • 第三方API密钥
  • 环境特定变量(开发/测试/生产)

推荐使用dotenv管理环境变量:

// .env 文件
DB_HOST=localhost
DB_PORT=5432
DB_USER=myuser
DB_PASS=mypassword

// config/database.js
require('dotenv').config();

module.exports = {
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  user: process.env.DB_USER,
  password: process.env.DB_PASS
};

对于更复杂的配置,可以使用配置对象:

// config/index.js
const dev = {
  app: {
    port: 3000
  },
  db: {
    host: 'localhost',
    port: 27017,
    name: 'dev_db'
  }
};

const prod = {
  app: {
    port: 80
  },
  db: {
    host: 'cluster.mongodb.net',
    port: 27017,
    name: 'prod_db'
  }
};

const config = {
  dev,
  prod
};

module.exports = config[process.env.NODE_ENV || 'dev'];

错误处理策略

统一的错误处理机制对维护性至关重要。Express中推荐的做法:

  1. 使用自定义错误类区分错误类型
  2. 中间件集中处理错误
  3. 标准化错误响应格式
// errors/app-error.js
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} not found`, 404);
  }
}

module.exports = {
  AppError,
  NotFoundError
};

// 控制器中使用
const { NotFoundError } = require('../errors/app-error');

async function getUser(req, res, next) {
  try {
    const user = await userService.getUser(req.params.id);
    if (!user) {
      throw new NotFoundError('User');
    }
    res.json(user);
  } catch (error) {
    next(error);
  }
}

// 错误处理中间件
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    error: {
      message: err.message,
      type: err.name,
      details: err.details
    }
  });
});

测试友好设计

良好的架构应该便于测试。在Express中这意味着:

  1. 避免直接require模块,使用依赖注入
  2. 分离副作用代码(如数据库访问)
  3. 保持函数纯净(同样输入总是同样输出)
// 可测试的服务示例
class OrderService {
  constructor(inventoryService, paymentGateway) {
    this.inventoryService = inventoryService;
    this.paymentGateway = paymentGateway;
  }

  async placeOrder(order) {
    await this.inventoryService.reserveItems(order.items);
    const receipt = await this.paymentGateway.charge(order.total);
    return { ...order, receipt };
  }
}

// 测试示例
describe('OrderService', () => {
  it('should place order successfully', async () => {
    const mockInventory = { reserveItems: jest.fn() };
    const mockGateway = { charge: jest.fn().mockResolvedValue('receipt123') };
    const service = new OrderService(mockInventory, mockGateway);
    
    const order = { items: ['item1'], total: 100 };
    const result = await service.placeOrder(order);
    
    expect(mockInventory.reserveItems).toHaveBeenCalledWith(['item1']);
    expect(mockGateway.charge).toHaveBeenCalledWith(100);
    expect(result.receipt).toBe('receipt123');
  });
});

性能考量

架构设计需要考虑性能因素:

  1. 中间件顺序:将常用中间件放在前面,过滤无效请求
  2. 路由组织:将高频路由放在前面
  3. 缓存策略:实现适当的缓存层
// 优化中间件顺序
app.use(helmet()); // 安全相关优先
app.use(compression()); // 压缩响应
app.use(rateLimiter); // 限流
app.use(authMiddleware); // 认证
app.use('/api', apiRouter); // 业务路由

对于数据密集型操作,考虑添加缓存层:

// 缓存装饰器示例
function cache(ttl) {
  return function(target, name, descriptor) {
    const original = descriptor.value;
    descriptor.value = async function(...args) {
      const cacheKey = `${name}-${JSON.stringify(args)}`;
      const cached = await cacheClient.get(cacheKey);
      if (cached) return JSON.parse(cached);
      
      const result = await original.apply(this, args);
      await cacheClient.set(cacheKey, JSON.stringify(result), 'EX', ttl);
      return result;
    };
    return descriptor;
  };
}

// 使用缓存装饰器
class ProductService {
  @cache(60) // 缓存60秒
  async getPopularProducts() {
    return this.productRepository.findPopular();
  }
}

扩展性设计

随着业务增长,架构需要支持水平扩展:

  1. 无状态设计:避免在内存中存储会话状态
  2. 队列集成:耗时操作异步处理
  3. 微服务准备:通过模块边界为未来拆分做准备
// 使用Redis存储会话
const session = require('express-session');
const RedisStore = require('connect-redis')(session);

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: 'your-secret',
  resave: false,
  saveUninitialized: false
}));

// 异步任务处理示例
router.post('/reports', async (req, res) => {
  const reportId = uuidv4();
  await queue.add('generate-report', {
    reportId,
    userId: req.user.id,
    params: req.body
  });
  res.json({ reportId, status: 'queued' });
});

// Worker处理任务
queue.process('generate-report', async (job) => {
  const { reportId, userId, params } = job.data;
  const report = await reportService.generateReport(userId, params);
  await storage.save(reportId, report);
  await emailService.sendReportReady(userId, reportId);
});

文档与一致性

良好的代码组织需要配套的文档和规范:

  1. API文档:使用Swagger或OpenAPI
  2. 架构决策记录:记录重大设计决策
  3. 代码风格指南:保持代码一致性
// Swagger文档示例
/**
 * @swagger
 * /users/{id}:
 *   get:
 *     summary: Get user by ID
 *     tags: [Users]
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: string
 *     responses:
 *       200:
 *         description: The user
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/User'
 */
router.get('/users/:id', userController.getUser);

架构决策记录示例(在docs/adr目录下):

001-use-express-framework.md
002-layered-architecture.md
003-postgresql-as-primary-db.md

现代化改进方向

随着技术演进,可以考虑:

  1. TypeScript集成:增强类型安全
  2. GraphQL混合:部分API采用GraphQL
  3. Serverless适配:部署到函数计算
// TypeScript控制器示例
import { Request, Response } from 'express';
import { UserService } from '../services/user.service';

export class UserController {
  constructor(private userService: UserService) {}

  async getUser(req: Request, res: Response): Promise<void> {
    try {
      const user = await this.userService.getUser(req.params.id);
      if (!user) {
        res.status(404).json({ error: 'User not found' });
        return;
      }
      res.json(user);
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }
}

持续演进与重构

架构不是一成不变的,需要定期评估和调整:

  1. 监控痛点:识别经常修改的模块
  2. 技术债务追踪:记录需要改进的地方
  3. 渐进式重构:小步迭代而非大规模重写

重构示例:将内联中间件提取为独立模块

// 重构前
app.use((req, res, next) => {
  if (!req.headers['x-api-key']) {
    return res.status(401).send('API key required');
  }
  if (req.headers['x-api-key'] !== process.env.API_KEY) {
    return res.status(403).send('Invalid API key');
  }
  next();
});

// 重构后:/middlewares/api-auth.js
function apiAuth(req, res, next) {
  if (!req.headers['x-api-key']) {
    return res.status(401).send('API key required');
  }
  if (req.headers['x-api-key'] !== process.env.API_KEY) {
    return res.status(403).send('Invalid API key');
  }
  next();
}

module.exports = apiAuth;

// 使用重构后的中间件
const apiAuth = require('./middlewares/api-auth');
app.use(apiAuth);

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

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