共计 2225 个字符,预计需要花费 6 分钟才能阅读完成。
背景与痛点
在开发技能系统时,很多团队会面临目录结构混乱的问题。典型的症状包括:

- 业务逻辑分散在多个目录中,难以追踪完整流程
- 基础设施代码(如数据库访问)与核心业务代码高度耦合
- 新增功能时需要在多个目录中修改代码,维护成本高
- 测试困难,因为依赖关系不清晰
这些问题往往在项目初期不明显,但随着业务复杂度增加,维护成本会呈指数级增长。
技术选型对比
常见的目录结构设计模式主要有三种:
- MVC 模式:
- 优点:简单直接,学习成本低
-
缺点:业务逻辑容易渗入 Controller 层,导致胖 Controller 问题
-
DDD 分层架构:
- 优点:明确业务边界,适合复杂业务场景
-
缺点:前期设计成本较高
-
Clean Architecture:
- 优点:依赖方向明确,便于测试
- 缺点:概念抽象,上手难度大
经过权衡,我们选择 DDD 分层架构 作为基础,因为它:
- 天然适合技能系统这类业务规则复杂的领域
- 通过限界上下文可以很好隔离不同技能模块
- 为未来可能的微服务拆分预留了扩展性
核心实现
模块化目录结构设计
我们采用垂直分模块 + 水平分层的混合结构:
src/
skills/
core/ # 核心领域模型
fireball/ # 具体技能模块
lightning/
shared/ # 跨技能共享代码
分层职责划分
- Domain 层:
- 包含实体、值对象、领域服务
-
纯业务逻辑,不依赖任何基础设施
-
Application 层:
- 协调领域对象完成用例
-
事务管理、权限校验等横切关注点
-
Infrastructure 层:
- 实现仓储接口、外部服务调用
- 框架相关代码(如 Express 路由、TypeORM 实体)
关键接口示例
定义技能施放的核心接口:
// domain/interfaces/ISkill.ts
export interface ISkill {
readonly id: SkillId;
cast(caster: Character, target: Character): Promise<CastResult>;
cooldown: number;
}
// application/services/SkillCastingService.ts
export class SkillCastingService {
constructor(
private skillRepo: ISkillRepository,
private effectApplier: IEffectApplier
) {}
async execute(request: CastRequest): Promise<CastResult> {// 验证、业务规则处理等}
}
完整目录结构示例
以下是 TypeScript 项目的参考结构:
src/
skills/
core/
domain/
entities/
Character.ts
Skill.ts
value-objects/
Damage.ts
services/
DamageCalculator.ts
application/
commands/
CastSkillCommand.ts
queries/
GetSkillInfoQuery.ts
services/
SkillCastingService.ts
infrastructure/
persistence/
TypeORMSkillRepository.ts
api/
SkillController.ts
fireball/
domain/
FireballSkill.ts
application/
FireballDamageCalculator.ts
shared/
logging/
SkillLogger.ts
events/
SkillEventPublisher.ts
生产环境考量
性能优化
-
懒加载:按需加载技能模块
// 动态导入技能模块 const {FireballSkill} = await import('../fireball'); -
缓存策略:
- CDN 缓存静态技能配置
- Redis 缓存热门技能计算结果
安全性
-
输入验证:
class CastRequest {@IsUUID() skillId: string; @Min(0) @Max(100) powerLevel: number; } -
权限控制:
@UseGuards(SkillAccessGuard) @Post('cast') async cast(@Body() request: CastRequest) {...}
避坑指南
- 过度设计分层:
- 症状:创建过多不必要的层级(如 domain/interfaces/validators/services)
-
解决:遵循 ”YAGNI” 原则,按需添加
-
循环依赖:
- 症状:模块 A 导入模块 B,模块 B 又依赖模块 A
-
解决:引入中间接口或事件机制
-
贫血模型:
- 症状:实体只有 getter/setter,没有行为
-
解决:将业务逻辑移入领域对象
-
基础设施泄漏:
- 症状:领域层出现数据库相关代码
-
解决:通过依赖倒置,定义接口在 domain,实现在 infra
-
测试困难:
- 症状:需要大量 mock 才能测试简单逻辑
- 解决:优化依赖关系,使用构造函数注入
进阶思考
- 如何平衡模块化与代码复用?共享代码应该放在什么位置?
- 当技能需要跨模块交互时(如组合技),如何设计才能保持解耦?
- 在 Serverless 架构下,这样的目录结构需要做哪些调整?
实践总结
经过三个迭代周期的实践,新结构带来了显著改进:
- 新增技能开发时间从 3 天缩短至 1 天
- 生产环境 Bug 率下降 40%
- 团队成员能更快定位问题代码
关键经验是:前期投入合理的架构设计,长期来看会大幅降低维护成本。建议从项目初期就开始采用结构化目录,避免后期重构的阵痛。
正文完
