CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
软件架构师入门/课时/设计模式与原则
RawBlame
课时目录12 节

软件架构师入门

  1. 01软件架构是什么
  2. 02架构师软技能
  3. 03设计模式与原则
  4. 04架构风格
  5. 05数据架构
  6. 06API 与集成
  7. 07安全架构
  8. 08云与设计模式
  9. 09网络基础
  10. 10技术栈选型
  11. 11治理与方法论
  12. 12综合练习

设计模式与原则

SOLID、DDD、CQRS 与 TDD。

35 分钟

设计模式与原则

这一课你将能做到

  1. 面对一段「又臭又长」的代码,用 SOLID 原则准确指出它违反了哪几条,并给出重构方向
  2. 画出本站课程域的 DDD 限界上下文图,标出聚合根和领域事件
  3. 解释 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 分钟过期)。

讲清楚要点

  1. SOLID 是设计原则,不是设计模式:原则是「好设计的标准」,模式是「常见问题的套路」。先学原则,再学模式,否则模式用错地方。
  2. 单一职责是最难也是最重要的:判断标准不是「这个类做了几件事」,而是「这个类变更的理由有几个」。如果改报表需求和改业务需求都会动同一个类,就违反 SRP。
  3. DDD 不是只有大项目才能用:即使单体应用,也可以用 DDD 的分层(Application / Domain / Infrastructure)和术语(聚合根、值对象、领域事件)。好处是代码结构统一,新人能快速定位。
  4. CQRS 解决的是「读模型和写模型差异大」的问题:如果读和写的字段几乎一样,CQRS 反而增加复杂度。本站课程详情(读)需要聚合讲师、评价、标签,而写只改课程基本信息——适合 CQRS。
  5. 最终一致性不是漏洞,是权衡:金融转账必须强一致,课程浏览量可以最终一致。选择依据:业务能否接受「短时间内看到旧数据」。

你可能会懵的地方

  • 设计模式不就是背 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 查
为每个查询都建一个读模型过度设计,维护爆炸读模型按页面/场景聚合,相似查询复用
忽略不变量,随意修改聚合内部数据不一致聚合根封装所有修改入口,外部只能通过聚合根操作

卡点急救

  1. 代码已经烂了,怎么逐步引入 DDD?:不要大爆炸重构。新需求按 DDD 写,老代码包一层防腐层(Anticorruption Layer),逐步替换。
  2. 领域事件丢消息怎么办?:本地事务表 + 异步发送。写业务数据时同时写事件表,事务保证原子性;后台任务扫描事件表发送,成功则标记。本站订单服务可用此模式。
  3. 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 的优缺点? 优点:读写模型独立优化,读性能可大幅提升,团队可分工。缺点:数据一致性复杂度上升,需要处理同步延迟,系统复杂度增加。适用:读多写少、读写模型差异大的场景。

自测三问

  1. 本站课程模块如果要新增「课程套餐」功能,套餐和课程的关系适合用聚合根还是值对象?
  2. 开闭原则的核心思想用一句话怎么表达?
  3. CQRS 场景中,如果用户刚修改了课程信息立刻刷新页面,看到旧数据,有哪些解决方案?

小结一句话

设计原则是判断代码好坏的标尺,设计模式是解决问题的工具箱;DDD 给复杂业务提供语言,CQRS 给读写分离提供路径——但一切的前提是「当前复杂度值得这么设计」。

延伸

  • 《实现领域驱动设计》(Implementing DDD, Vaughn Vernon)
  • 《设计模式:可复用面向对象软件的基础》(GoF)
  • 《重构:改善既有代码的设计》(Martin Fowler)
  • 下一课:架构风格——单体、分层、微服务、Serverless 的适用边界。
加载本节练习…
← 上一课架构师软技能下一课 →架构风格