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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
软件设计与架构入门/课时/领域驱动设计
RawBlame
课时目录12 节

软件设计与架构入门

  1. 01整洁代码
  2. 02设计原则
  3. 03面向对象
  4. 04编程范式
  5. 05设计模式
  6. 06架构原则
  7. 07架构风格
  8. 08架构模式
  9. 09领域驱动设计
  10. 10企业模式
  11. 11综合架构练习
  12. 12路线图对照复习

领域驱动设计

聚合、实体、值对象与仓储。

35 分钟

领域驱动设计:实体、值对象、聚合与限界上下文

这一课你将能做到

  1. 区分实体(Entity)、值对象(Value Object)、聚合(Aggregate),并在判题平台里各举一例
  2. 用限界上下文(Bounded Context)把"用户管理"和"判题执行"拆成两个独立模型
  3. 用领域事件(Domain Event)表达"提交已判题"这一业务事实

为什么要学

DDD 不是新框架,是"怎么把业务翻译成代码"的方法论。判题平台里,CfUser 和 CfSubmission 看起来都是"数据表",但前者有生命周期(注册→VIP→过期),后者有业务规则(提交→判题→记录)。DDD 帮你把这些差异显式化,让代码结构 = 业务结构。

先建立图像

实体(Entity)              值对象(Value Object)
─────────────               ─────────────────
有唯一标识                   无标识,值相等即相等
有生命周期                   不可变
例:CfUser(id=123)           例:Email("a@b.com")
例:Submission(id=456)        例:Score(85)

聚合(Aggregate)            限界上下文(Bounded Context)
─────────────                ─────────────────────
一组相关对象的边界            模型的语义边界
聚合根控制访问               不同上下文,同一词不同含义
例:Submission + JudgeResult  例:用户上下文 vs 判题上下文

跟着做

步骤 1:实体 vs 值对象

// 实体:有唯一标识,有生命周期
public class CfUser {
    private final UserId id;           // 唯一标识
    private Nickname nickname;
    private Email email;
    private VipStatus vipStatus;
    private LocalDateTime registerAt;

    public void upgradeToVip(LocalDateTime expireAt) {
        this.vipStatus = VipStatus.active(expireAt);
    }
}

// 值对象:无标识,值相等即相等,不可变
public record Email(String value) {
    public Email {
        if (!value.contains("@")) throw new IllegalArgumentException("非法邮箱");
    }
}

public record Score(int value) {
    public Score {
        if (value < 0 || value > 100) throw new IllegalArgumentException("分数越界");
    }
}

UserId 是实体标识,Email 和 Score 是值对象。两个 Email("a@b.com") 相等,但两个 UserId(123) 可能对应不同用户。

步骤 2:聚合与聚合根

聚合是一组"要么一起改,要么都不改"的对象。聚合根是聚合的唯一入口:

// 聚合根:Submission
public class Submission {
    private final SubmissionId id;      // 聚合根标识
    private final UserId userId;
    private final LessonId lessonId;
    private CodeSnippet code;
    private JudgeResult judgeResult;    // 聚合内部对象
    private SubmissionStatus status;

    // 外部只能通过聚合根操作内部对象
    public void assignJudgeResult(JudgeResult result) {
        this.judgeResult = result;
        this.status = result.isAccepted() ? ACCEPTED : REJECTED;
        // 产生领域事件
        DomainEvents.raise(new SubmissionJudged(this.id, result));
    }
}

// 外部代码不能直接操作 JudgeResult,必须通过 Submission
submission.assignJudgeResult(result);   // 对
// submission.getJudgeResult().setScore(100);   // 错:绕过聚合根

聚合保证业务规则不被绕过:改 JudgeResult 必须走 Submission 的方法。

步骤 3:限界上下文——同一词,不同含义

判题平台里,"用户"这个词在不同场景含义不同:

用户上下文                    判题上下文
──────────                   ──────────
CfUser                       JudgeUser
- nickname                   - userId
- email                      - currentSubmissionCount
- vipStatus                  - rateLimitQuota
- registerAt                 - lastSubmitAt

同一个"用户",但模型完全不同
// 用户上下文
package com.coderoadmap.user;
public class CfUser {
    private UserId id;
    private Nickname nickname;
    private Email email;
    private VipStatus vipStatus;
}

// 判题上下文
package com.coderoadmap.judge;
public class JudgeUser {
    private UserId userId;
    private int currentSubmissionCount;
    private RateLimitQuota rateLimitQuota;

    public boolean canSubmit() {
        return currentSubmissionCount < rateLimitQuota.maxPerMinute();
    }
}

两个上下文通过 UserId 关联,但模型独立演化。用户上下文改昵称不影响判题上下文。

步骤 4:领域事件——业务事实的显式表达

public record SubmissionJudged(SubmissionId submissionId, JudgeResult result) implements DomainEvent {}

@Service
public class SubmissionService {
    private final SubmissionRepository repository;
    private final DomainEventPublisher eventPublisher;

    public void judge(SubmissionId id, JudgeResult result) {
        Submission submission = repository.findById(id).orElseThrow();
        submission.assignJudgeResult(result);   // 聚合内产生事件
        repository.save(submission);

        // 发布事件,让其他上下文响应
        eventPublisher.publish(new SubmissionJudged(id, result));
    }
}

// 判题上下文监听
@Component
public class PracticeRecordUpdater {
    @EventListener
    public void onSubmissionJudged(SubmissionJudged event) {
        // 更新 cf_practice_record
    }
}

领域事件让"提交已判题"成为显式的业务事实,其他上下文可以订阅响应。

讲清楚要点

  1. 实体:有唯一标识,有生命周期,可追踪变化。CfUser、Submission 是实体。
  2. 值对象:无标识,值相等即相等,不可变。Email、Score、JudgeResult 是值对象。
  3. 聚合:一组相关对象的边界,聚合根是唯一入口。Submission 是聚合根,JudgeResult 是聚合内部对象。
  4. 限界上下文:模型的语义边界,同一词在不同上下文含义不同。用户上下文 vs 判题上下文。
  5. 领域事件:业务事实的显式表达,让跨上下文协作解耦。

你可能会懵的地方

  • 实体和值对象怎么区分? 问"这个对象需要追踪历史变化吗?"需要→实体;不需要,只看值→值对象。CfUser 要追踪注册时间、VIP 变更,是实体;Email 改了就是新对象,是值对象。
  • 聚合根怎么选? 问"外部操作这个对象时,是否需要保护内部规则?"需要→聚合根。Submission 保护"判题结果不能乱改",是聚合根;JudgeResult 只是数据,不是聚合根。
  • 限界上下文会不会导致重复代码? 会有一点,但换来的是模型独立演化。用户上下文改昵称不影响判题上下文,比"一个 User 类被 10 个模块共用"好维护。
  • 领域事件和消息队列什么关系? 领域事件是"业务事实",消息队列是"传输机制"。领域事件可以本地发布(Spring Event),也可以发到 MQ(Kafka/RabbitMQ)。前者用于进程内解耦,后者用于跨服务解耦。
  • DDD 是不是过度设计? 对简单 CRUD 是;对复杂业务(如判题平台的多上下文协作)不是。判断标准:业务规则是否复杂到需要显式建模?

错法对照

错法问题正解
所有类都当实体,到处 @Entity值对象也带 id,冗余值对象用 record,无 id
外部直接改 judgeResult.score绕过聚合根,规则失效通过 submission.assignJudgeResult()
一个 User 类被所有模块共用模型臃肿,改昵称影响判题限界上下文:用户上下文 + 判题上下文
业务规则写在 Service 里领域对象贫血,规则散落规则内聚在聚合根方法里
领域事件直接调其他 Service紧耦合发布事件,监听者各自响应

卡点急救

  1. 不知道实体还是值对象:先写个类,问"我需要 equals 按 id 还是按值?"按 id→实体;按值→值对象。
  2. 聚合根边界不清:画个"对象关系图",看哪些对象"要么一起改,要么都不改",圈起来就是聚合。
  3. 限界上下文拆不动:先找"同一词不同含义"的地方(如"用户"在用户管理和判题中的差异),从那里拆。

面试常问

  • 实体和值对象的区别? 答:实体有唯一标识,生命周期可追踪;值对象无标识,值相等即相等,不可变。判题平台里 CfUser 是实体(id 追踪),Email 是值对象(值相等即相等)。
  • 聚合根的作用? 答:聚合根是聚合的唯一入口,保护业务规则不被绕过。外部要改 JudgeResult,必须通过 Submission.assignJudgeResult(),不能直接 setScore。
  • 限界上下文怎么落地? 答:按业务域分包(如 user、judge),同一词在不同上下文用不同模型(如 CfUser vs JudgeUser),通过 id 关联,通过事件协作。

自测三问

  1. JudgeResult 应该是实体还是值对象?为什么?
  2. 判题平台里,Submission 和 JudgeResult 谁是聚合根?为什么?
  3. 用户上下文和判题上下文都有"用户"概念,应该怎么设计模型?

小结一句话

DDD 让代码结构 = 业务结构:实体管身份,值对象管数据,聚合管边界,限界上下文管语义,领域事件管协作。

延伸

  • 《领域驱动设计》Eric Evans,第 5/6 章
  • 《实现领域驱动设计》Vaughn Vernon,第 8/9 章
  • DDD 参考 - Domain Language
  • 下一课:企业模式——PoEAA 在判题平台的落地。
加载本节练习…
← 上一课架构模式下一课 →企业模式