大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
设计模式与原则
这一课你将能做到
- 面对一段「又臭又长」的代码,用 SOLID 原则准确指出它违反了哪几条,并给出重构方向
- 画出本站课程域的 DDD 限界上下文图,标出聚合根和领域事件
- 解释 CQRS 和最终一致性的适用场景,能画出读写分离的架构图
为什么要学
设计模式和原则不是炫技,是应对变更的武器。需求变了 100 次,你的代码改了 100 次还是改到崩溃——说明设计出了问题。
人话:好的设计像乐高,换一块积木不会散架;坏的设计像俄罗斯方块,塞进去就拔不出来,越堆越高。
面试场景:「你用过哪些设计模式?」「你们系统的分层是怎么设计的?」「订单状态机怎么保证一致性?」——这些问题没有标准答案,但 SOLID 和 DDD 是你的分析框架。
先建立图像
本站课程模块的分层演进:
【贫血模型】(初学者常见)
Controller → Service(全是 if/else 业务逻辑) → DAO → MySQL
【充血模型/DDD】(演进后)
Controller → ApplicationService(编排)
→ DomainService(跨聚合逻辑)
→ CourseAggregate(聚合根,内含业务规则)
→ Repository(接口,由 DAO 实现)
→ MySQL / Redis
跟着做
步骤 1:用 SOLID 审查一段坏代码
// 违反 SRP:一个类干三件事
public class CourseManager {
public void createCourse() { ... } // 业务逻辑
public void sendEmail() { ... } // 通知逻辑
public void generateReport() { ... } // 报表逻辑
}
重构后:
public class CourseService {
private final NotificationService notifier;
private final ReportService reporter;
public void createCourse(CreateCourseCmd cmd) {
// ... 只负责课程创建
notifier.notifyCourseCreated(course); // 委托通知
reporter.trackCreation(course); // 委托报表
}
}
步骤 2:识别 DDD 限界上下文
本站可拆出三个上下文:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 课程上下文 │────→│ 订单上下文 │←────│ 用户上下文 │
│ │ │ │ │ │
│ 聚合:Course │ │ 聚合:Order │ │ 聚合:User │
│ 事件:Course │ │ 事件:Order │ │ 事件:User │
│ Published │ │ Created │ │ Registered │
└──────────────┘ └──────────────┘ └──────────────┘
上下文映射:
- 课程上下文 → 订单上下文:上游(UL 以课程为准)
- 用户上下文 → 订单上下文:上游
- 订单上下文 → 推荐上下文:领域事件(OrderCreated → 更新推荐模型)
步骤 3:CQRS 推演
本站场景:课程详情页读多写少,且读模型和写模型差异大。
// 写模型(命令端)
public class CourseWriteService {
@Transactional
public void updateCourse(CourseId id, UpdateCmd cmd) {
Course course = courseRepo.findById(id);
course.update(cmd);
courseRepo.save(course);
eventBus.publish(new CourseUpdated(id));
}
}
// 读模型(查询端)
public class CourseReadService {
public CourseDetailDTO getDetail(CourseId id) {
// 直接从 Redis/ES 读,不走领域模型
return cache.getOrLoad(id, () -> elasticSearch.get(id));
}
}
关键:命令端保证强一致,查询端允许最终一致(缓存 5 分钟过期)。
讲清楚要点
- SOLID 是设计原则,不是设计模式:原则是「好设计的标准」,模式是「常见问题的套路」。先学原则,再学模式,否则模式用错地方。
- 单一职责是最难也是最重要的:判断标准不是「这个类做了几件事」,而是「这个类变更的理由有几个」。如果改报表需求和改业务需求都会动同一个类,就违反 SRP。
- DDD 不是只有大项目才能用:即使单体应用,也可以用 DDD 的分层(Application / Domain / Infrastructure)和术语(聚合根、值对象、领域事件)。好处是代码结构统一,新人能快速定位。
- CQRS 解决的是「读模型和写模型差异大」的问题:如果读和写的字段几乎一样,CQRS 反而增加复杂度。本站课程详情(读)需要聚合讲师、评价、标签,而写只改课程基本信息——适合 CQRS。
- 最终一致性不是漏洞,是权衡:金融转账必须强一致,课程浏览量可以最终一致。选择依据:业务能否接受「短时间内看到旧数据」。
你可能会懵的地方
-
设计模式不就是背 23 种吗? 不是。面试和工作里常用的不到 10 种:单例、工厂、策略、观察者、适配器、装饰器、代理、模板方法。重点不是背定义,而是知道「什么时候用、用了以后代码会变成什么样」。
-
DDD 和 MVC 冲突吗? 不冲突。MVC 是表现层模式,DDD 是领域层设计。可以 MVC + DDD:Controller(V 的 C)调用 ApplicationService(DDD 的应用层),再往下走领域层。
-
聚合根怎么找? 问三个问题:1) 业务上是否有「一个整体,内部对象不能单独存在」?(订单和订单项)2) 是否有不变量需要同时维护?(订单总价 = 各项之和)3) 是否有生命周期一致性?(订单删除时订单项必须一起删)。都符合就是聚合根。
-
CQRS 一定要配事件溯源吗? 不一定。CQRS 可以简单到「写 MySQL + 读 Redis」,事件溯源是 CQRS 的高级玩法(记录所有变更事件)。初学者先掌握简单的读写分离即可。
-
值对象和实体怎么区分? 实体有唯一标识(UserId),值对象没有(Money、Address)。值对象相等看内容,实体相等看 ID。本站里「课程价格」是值对象(100 元和另一个 100 元相等),「课程」是实体(两门同名课程也是不同实体)。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 每个类都用单例 | 单例隐藏依赖,测试困难 | 只在「无状态 + 全局唯一」时用(连接池) |
| Service 层全是贫血 CRUD | 业务逻辑泄露到 Controller | 核心规则放进领域对象/聚合根 |
| 聚合根引用了另一个聚合根 | 破坏边界,加载性能差 | 跨聚合用 ID 引用,需要时通过 Repository 查 |
| 为每个查询都建一个读模型 | 过度设计,维护爆炸 | 读模型按页面/场景聚合,相似查询复用 |
| 忽略不变量,随意修改聚合内部 | 数据不一致 | 聚合根封装所有修改入口,外部只能通过聚合根操作 |
卡点急救
- 代码已经烂了,怎么逐步引入 DDD?:不要大爆炸重构。新需求按 DDD 写,老代码包一层防腐层(Anticorruption Layer),逐步替换。
- 领域事件丢消息怎么办?:本地事务表 + 异步发送。写业务数据时同时写事件表,事务保证原子性;后台任务扫描事件表发送,成功则标记。本站订单服务可用此模式。
- CQRS 怎么保证读写数据一致?:业务上接受延迟时,用消息队列异步同步;不能接受延迟时,读写走同一个数据源,放弃 CQRS。
面试常问
-
什么是 SOLID 原则?举例说明。 S(单一职责):一个类只有一个变更理由。例:CourseService 只管课程,邮件发送交给 NotificationService。 O(开闭):对扩展开放,对修改关闭。例:新增支付方式时,加一个新策略类而不是改 PaymentService 的 if/else。 L(里氏替换):子类可以替换父类。例:PremiumUser extends User,任何用 User 的地方都能用 PremiumUser。 I(接口隔离):不强迫客户端依赖不需要的方法。例:不要一个巨型 Service 接口,拆成 CourseQueryService 和 CourseCommandService。 D(依赖倒置):依赖抽象而非具体。例:Service 依赖 CourseRepository 接口,不依赖 JdbcCourseDao。
-
DDD 的聚合根是什么? 聚合是一组相关对象的集合,聚合根是聚合的入口,外部对象只能通过聚合根访问聚合内部。聚合根负责维护聚合内的不变量。本站中 Order 是聚合根,OrderItem 不能脱离 Order 单独存在。
-
CQRS 的优缺点? 优点:读写模型独立优化,读性能可大幅提升,团队可分工。缺点:数据一致性复杂度上升,需要处理同步延迟,系统复杂度增加。适用:读多写少、读写模型差异大的场景。
自测三问
- 本站课程模块如果要新增「课程套餐」功能,套餐和课程的关系适合用聚合根还是值对象?
- 开闭原则的核心思想用一句话怎么表达?
- CQRS 场景中,如果用户刚修改了课程信息立刻刷新页面,看到旧数据,有哪些解决方案?
小结一句话
设计原则是判断代码好坏的标尺,设计模式是解决问题的工具箱;DDD 给复杂业务提供语言,CQRS 给读写分离提供路径——但一切的前提是「当前复杂度值得这么设计」。
延伸
- 《实现领域驱动设计》(Implementing DDD, Vaughn Vernon)
- 《设计模式:可复用面向对象软件的基础》(GoF)
- 《重构:改善既有代码的设计》(Martin Fowler)
- 下一课:架构风格——单体、分层、微服务、Serverless 的适用边界。