1. 为什么工程结构决定系统寿命
我见过太多项目在初期跑得飞快,却在三个月后陷入泥潭。这些系统往往不是被高并发压垮,而是被自己混乱的结构拖死。最典型的症状就是:每次需求变更都像在拆炸弹,新人入职三个月还不敢碰核心模块,简单的功能迭代需要修改十几处文件。
问题的根源在于:结构没有为复杂度设计缓冲空间。当业务从10个接口膨胀到100个接口时,当团队从3人扩展到20人时,当技术栈需要部分升级时——混乱的结构会让这些变化成为灾难。
1.1 结构混乱的五大症状
业务膨胀恐惧症
每次新增功能都像在玩俄罗斯方块,找不到合适的落点。最终要么硬塞进不相关的Service,要么新建一个孤立的模块,导致系统变成打满补丁的破衣服。模块连环爆炸
修改用户模块导致订单模块报错,调整支付逻辑影响库存计算。模块间像缠在一起的耳机线,牵一发而动全身。历史债务陷阱
三年前写的Controller现在没人敢改,因为里面混着业务逻辑、数据校验、甚至直接操作Redis的代码。每次修改都可能引发未知故障。新人上手成本高
新同事问"用户登录逻辑在哪",你可能要回答:"一部分在UserController,权限校验在AuthUtil,日志记录在LogAspect,最后还有个LoginEventConsumer"。技术栈升级困难
想从MyBatis换成JPA?发现Entity里写满了SQL注解。要加Redis缓存?发现缓存逻辑散落在二十多个Service中。
1.2 好结构的核心特征
优秀的工程结构应该像乐高积木:
- 明确边界:每个模块像积木块,有标准的凹凸接口
- 独立进化:可以单独替换某个积木块(如MySQL换MongoDB)
- 组合自由:能按需组装出不同形态(如APP端和Admin端)
- 说明书清晰:新人能快速理解组装规则
我在电商项目中最深刻的教训:早期图快把优惠券逻辑直接写在OrderService里,结果半年后优惠券业务膨胀到2万行代码,每次大促前改逻辑都要通宵。后来用分层结构重写,同样的需求现在2小时就能安全上线。
2. 五层架构设计详解
2.1 接口适配层(interfaces)
这是系统的"玄关",负责与外部世界打交道。以用户注册为例:
// 正确示范 @RestController public class UserController { private final UserApplicationService userAppService; @PostMapping("/users") public UserResponse register(@Valid @RequestBody UserCreateRequest request) { // 转换DTO为领域命令 CreateUserCommand command = UserAssembler.toCommand(request); // 调用应用服务 User user = userAppService.register(command); // 转换领域对象为DTO return UserAssembler.toResponse(user); } }常见错误:
- 在Controller里写业务规则(如密码强度校验)
- 直接返回Entity或Map
- 把Service注入到DTO里做校验
实操技巧:
- 使用
@Valid进行基础校验(非空、格式等) - DTO类名应体现场景:
UserLoginRequest比UserDTO明确 - 对于复杂校验,可以创建专门的
XXXValidator类
2.2 业务编排层(application)
这是业务的"指挥中心",负责协调多个领域对象完成用例。注意它不包含具体业务规则!
@Service @Transactional public class UserApplicationService { private final UserRepository userRepository; private final EmailSender emailSender; public User register(CreateUserCommand command) { // 校验用户名唯一性(业务规则在Domain中) User user = UserFactory.create(command); // 持久化 userRepository.save(user); // 发送欢迎邮件(领域事件会更优雅) emailSender.sendWelcomeEmail(user.getEmail()); return user; } }分层误区警示:
- ❌ 把Service当成"大杂烩":既处理HTTP参数,又写SQL,还包含业务规则
- ✅ 正确认知:Service是业务流程的脚本,Domain才是业务规则的归宿
2.3 业务核心层(domain)
这是系统的"大脑",所有业务规则和状态变迁都在这里发生。好的领域模型应该像一本业务说明书。
public class User { private UserId id; private String username; private Password password; private UserStatus status; // 业务行为 public void changePassword(String oldPass, String newPass) { if (!password.matches(oldPass)) { throw new BusinessException("旧密码错误"); } this.password = Password.create(newPass); } // 工厂方法 public static User create(CreateUserCommand command) { User user = new User(); user.id = UserId.next(); user.username = command.getUsername(); user.password = Password.create(command.getPassword()); user.status = UserStatus.INACTIVE; return user; } }领域模型设计要点:
- 贫血模型是万恶之源:把行为塞进Service的User不叫领域模型
- 警惕getter/setter暴露:应该暴露行为而非内部状态
- 值对象是秘密武器:
Password、UserId等小对象能极大增强类型安全
2.4 技术实现层(infrastructure)
这是系统的"工具箱",所有技术细节在这里被封装。关键是要让业务层感觉不到技术存在。
@Repository public class JpaUserRepository implements UserRepository { private final EntityManager em; @Override public User findById(UserId id) { UserEntity entity = em.find(UserEntity.class, id.getValue()); return entity != null ? UserMapper.toDomain(entity) : null; } @Override public void save(User user) { UserEntity entity = UserMapper.toEntity(user); em.merge(entity); } }技术隔离技巧:
- 为每个技术组件定义接口(如
UserRepository) - 实现类放在infrastructure包
- 业务层永远只依赖接口
- 使用Mapper隔离领域对象与持久化对象
2.5 通用规范层(common)
这是系统的"宪法",定义全系统遵守的约定。注意它不是垃圾回收站!
推荐内容:
- 统一异常处理(
BusinessExceptionvsSystemException) - 返回值包装(
Result<T>) - 日志规范(MDC跟踪、日志格式)
- 工具类(严格审核,避免重复)
反面教材:
// 糟糕的common设计 public class StringUtils { // 与Apache Commons重复 public static boolean isEmpty(String str) {...} // 业务相关方法不该在这里 public static String maskMobile(String phone) {...} }3. 模型拆分实战技巧
3.1 DTO vs Entity vs Domain
这是最容易混淆的概念,用餐厅比喻:
- DTO:顾客看到的菜单(展示什么、怎么展示)
- Entity:厨房的食材库存表(怎么存储、怎么取用)
- Domain:厨师的烹饪方法(业务规则)
典型错误案例:
// 灾难代码:三合一模型 @Entity public class User { @Id private Long id; @Column private String username; @JsonIgnore // 因为密码不能返回给前端... private String password; // 混入业务逻辑 public boolean checkPassword(String input) { return this.password.equals(hash(input)); } }正确拆分示范:
// Domain模型 public class User { private UserId id; private Username username; private Password password; public boolean verifyPassword(Password input) { return password.matches(input); } } // Entity(数据模型) @Entity @Table(name = "t_users") public class UserEntity { @Id private String id; private String username; private String encryptedPassword; } // DTO(接口模型) public class UserResponse { private String userId; private String displayName; }3.2 模型转换的最佳实践
避免在业务代码中到处写new UserResponse(user),推荐集中管理:
public class UserAssembler { public static UserResponse toResponse(User user) { UserResponse response = new UserResponse(); response.setUserId(user.getId().toString()); response.setDisplayName(user.getUsername().getValue()); return response; } public static UserEntity toEntity(User user) { UserEntity entity = new UserEntity(); entity.setId(user.getId().toString()); entity.setUsername(user.getUsername().getValue()); entity.setEncryptedPassword(user.getPassword().getHash()); return entity; } }转换器设计原则:
- 保持单向依赖:Assembler依赖所有模型,但模型不依赖Assembler
- 隔离变化:当接口字段调整时,只需修改Assembler
- 可以适当用MapStruct简化代码
4. 分层架构的演进路径
4.1 单体应用下的分层
com.example.mall ├─ interfaces │ ├─ web │ ├─ job │ └─ admin ├─ application ├─ domain ├─ infrastructure └─ common4.2 演进到微服务时
com.example ├─ user-service # 独立服务 │ └─ 同上结构 ├─ order-service └─ inventory-service平滑演进的关键:
- 领域层完全不受影响
- 只需重写interfaces层(如HTTP改RPC)
- 基础设施实现可能调整(如本地事务改分布式事务)
4.3 前端多端适配场景
当需要支持APP/小程序/管理后台时,分层优势显现:
interfaces ├─ web # 给APP的接口 │ └─ v1 │ └─ v2 ├─ mini # 小程序专用 └─ admin # 管理后台共用同一套application和domain,但提供不同的DTO和Assembler。
5. 避坑指南与经验总结
5.1 我踩过的三个大坑
坑1:过早抽象
在项目初期硬套DDD,创建大量无用的领域对象。实际上简单CRUD用贫血模型更高效。
教训:当业务逻辑超过300行Service代码时,再考虑引入领域层。
坑2:过度分层
曾经给一个中小型项目设计七层架构,导致简单查询要穿越5个类。维护成本远超收益。
教训:五层架构适合复杂业务系统,简单后台用三层足够。
坑3:忽视团队认知
在团队没有分层意识时强行推行,导致大量"分层漏洞"。
教训:需要通过代码评审和示例项目培养共识。
5.2 判断结构健康的三个指标
- 变更成本:新增一个字段是否需要修改超过3个文件?
- 理解成本:新人能否在一天内找到登录逻辑的所有相关代码?
- 测试成本:领域逻辑是否可以不启动Spring容器进行单元测试?
5.3 渐进式改进策略
如果已有混乱项目,不要试图重写,推荐:
- 冻结腐烂:新功能严格按分层实现
- 局部手术:每次修改旧代码时顺便重构受影响模块
- 建立防护网:补充关键测试用例
- 文档化约定:编写团队分层规范文档
6. 工具与框架选型建议
6.1 分层架构的黄金搭档
- MapStruct:优雅处理DTO/Entity/Domain转换
- JUnit 5:领域层应该不依赖Spring进行测试
- ArchUnit:用代码约束分层规范
- Lombok:减少模板代码(但慎用
@Data)
6.2 避免的框架反模式
- Entity直接当DTO:MyBatis/JPA实体加上
@JsonIgnore满天飞 - Service继承Controller:Spring的
@Controller继承@Service - AOP滥用:把业务规则写在切面中
7. 真实项目结构示例
这是我在电商项目中验证过的结构:
mall ├─ interfaces │ ├─ web │ │ ├─ controller │ │ ├─ dto │ │ └─ assembler │ ├─ job │ └─ admin ├─ application │ ├─ command │ └─ query ├─ domain │ ├─ model │ ├─ service │ └─ repository ├─ infrastructure │ ├─ persistence │ ├─ cache │ └─ mq └─ common ├─ exception ├─ util └─ config关键数字:
- 领域模型占比40%代码量
- 转换代码占比<5%
- 平均每个需求修改文件数:3.2个(之前是8.7个)