第3章:领域建模入门——什么是领域模型?用UML类图描述业务实体,实体、值对象、聚合的区分

好,我们进入第三讲。这一章,咱们聊聊领域建模。

说实话,很多刚入行的朋友一听到“领域模型”四个字,就觉得特别高大上。其实没那么玄乎。说白了,领域模型就是你对业务世界的“翻译”。你把客户嘴里的那些业务术语、规则、流程,翻译成代码世界里能看懂的结构。

我个人习惯,拿到一个需求文档后,第一件事不是画架构图,而是先画领域模型。为什么?因为架构是骨架,领域模型才是血肉。骨架长歪了,后面怎么填肉都别扭。

3.1 什么是领域模型?

领域模型,就是业务概念的可视化表达。

举个例子。你做一个电商系统。客户说:“我们要管理商品、订单、用户。” 好,那“商品”、“订单”、“用户”就是领域概念。它们之间有什么关系?一个用户可以有多个订单,一个订单包含多个商品。把这些画出来,就是领域模型。

领域模型的核心作用有三个:

  • 统一语言:让开发、产品、测试、业务方,都用同一套词说话。别开发说“SKU”,产品说“货号”,业务说“商品编码”,那不乱套了?
  • 理清边界:哪些东西是核心,哪些是附属,一目了然。
  • 指导设计:模型画清楚了,数据库表怎么设计、接口怎么划分,心里就有数了。

核心观点:领域模型不是UML图,UML图只是表达领域模型的一种工具。领域模型是你脑子里的业务认知,UML图是把它画出来给别人看。

3.2 用UML类图描述业务实体

UML类图,是画领域模型最常用的工具。它用方框表示“类”,用连线表示“关系”。

咱们来看一个实际的例子。假设我们要做一个“在线课程平台”的领域模型。核心业务实体有哪些?

  • 课程(Course):一门课,比如“Java从入门到精通”。
  • 章节(Chapter):课程下面的一个单元。
  • 讲师(Instructor):谁来讲这门课。
  • 学生(Student):谁在学这门课。
  • 订单(Order):学生购买课程的记录。

用UML类图表示,大概长这样:

+----------------+          +------------------+
|    Course       |          |    Chapter       |
+----------------+          +------------------+
| - courseId     |<-------->| - chapterId      |
| - title        |  1      N| - title          |
| - description  |          | - duration       |
| - price        |          | - videoUrl       |
| - status       |          | - orderIndex     |
+----------------+          +------------------+
        |
        | 1
        |
        | N
+------------------+
|    Instructor    |
+------------------+
| - instructorId   |
| - name           |
| - bio            |
| - rating         |
+------------------+

+------------------+          +------------------+
|     Student      |          |     Order        |
+------------------+          +------------------+
| - studentId      |          | - orderId        |
| - name           |<--------->| - studentId      |
| - email          |  1      N| - courseId       |
| - enrolledDate   |          | - amount         |
+------------------+          | - orderTime      |
                              | - status         |
                              +------------------+

你看,每个方框就是一个实体。方框里分三部分:类名、属性、方法(这里为了简洁,方法没写)。连线表示关系,比如“一个课程有多个章节”,就是1对N的关系。

小技巧:画UML类图时,别急着把所有属性都列出来。先抓核心属性。比如“课程”,核心属性就是课程ID、标题、价格。那些“创建时间”、“更新时间”之类的,可以后面再加。我刚开始画图时,总想把所有字段都塞进去,结果图乱得跟蜘蛛网似的。

3.3 实体、值对象、聚合的区分

好,这是本章的重点。很多人在DDD(领域驱动设计)里,被这三个概念搞晕过。我当年也一样。

咱们一个一个说。

3.3.1 实体(Entity)

实体,是有唯一标识的。说白了,就是“你是谁”比“你长什么样”更重要。

举个例子。你是一个学生,你有学号。哪怕你改了名字、换了手机号、转了专业,你还是你,因为你的学号没变。学号就是你的唯一标识。

在代码里,实体通常有一个ID字段。两个实体比较,比的不是属性值,而是ID。

public class Student {
    private StudentId studentId;  // 唯一标识
    private String name;
    private String email;
    private LocalDate enrolledDate;

    // 两个学生是否相等,看ID
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return Objects.equals(studentId, student.studentId);
    }
}

实体的特点是:可变、有生命周期。学生可以入学、毕业、休学,状态一直在变,但身份不变。

3.3.2 值对象(Value Object)

值对象,没有唯一标识。它只关心“是什么”,不关心“是谁”。

比如“地址”。两个地址,如果省、市、区、街道、门牌号都一样,那它们就是同一个地址。你不需要给每个地址分配一个ID。

再比如“金额”。100元就是100元,不管它是张三的100元还是李四的100元,数值一样就是一样。

值对象的特点是:不可变、无标识、通过属性比较

public class Address {
    private final String province;
    private final String city;
    private final String district;
    private final String street;
    private final String detail;

    // 构造时赋值,之后不可修改
    public Address(String province, String city, String district, String street, String detail) {
        this.province = province;
        this.city = city;
        this.district = district;
        this.street = street;
        this.detail = detail;
    }

    // 比较时,比较所有属性
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return Objects.equals(province, address.province) &&
               Objects.equals(city, address.city) &&
               Objects.equals(district, address.district) &&
               Objects.equals(street, address.street) &&
               Objects.equals(detail, address.detail);
    }
}

注意:值对象一定要设计成不可变的。我曾经在一个项目里,把“地址”设计成了可变的,结果两个地方引用同一个地址对象,一个改了另一个也跟着变,查了半天bug。从那以后,我所有值对象都加final。

3.3.3 聚合(Aggregate)

聚合,是一组相关对象的集合。它有一个“根”,叫聚合根。外部只能通过聚合根来访问聚合内的对象。

你想想看,一个“订单”包含多个“订单项”。你不能直接去修改订单项的数量,你得通过订单来操作。订单就是聚合根,订单项是聚合内的实体。

聚合的作用是保证数据一致性。比如,订单的总金额必须等于所有订单项金额之和。如果外部可以直接修改订单项,那总金额就可能对不上。通过聚合根来操作,就能保证这个约束。

// 聚合根:Order
public class Order {
    private OrderId orderId;
    private List<OrderItem> items;
    private Money totalAmount;

    // 外部只能通过这个方法添加商品
    public void addItem(Product product, int quantity) {
        OrderItem item = new OrderItem(product, quantity);
        items.add(item);
        recalculateTotalAmount();  // 重新计算总金额
    }

    private void recalculateTotalAmount() {
        this.totalAmount = items.stream()
            .map(OrderItem::getSubTotal)
            .reduce(Money.ZERO, Money::add);
    }
}

// 聚合内的实体:OrderItem
public class OrderItem {
    private Product product;
    private int quantity;
    private Money unitPrice;

    // 没有公开的setter,只能通过Order来操作
    Money getSubTotal() {
        return unitPrice.multiply(quantity);
    }
}

聚合的边界怎么定?我有个简单的判断方法:如果两个对象之间,存在强一致性的约束,那它们就应该在同一个聚合里。如果只是弱关联,比如“用户”和“订单”,那就可以分属不同的聚合。

3.4 三者对比总结

特性 实体(Entity) 值对象(Value Object) 聚合(Aggregate)
唯一标识 聚合根有
可变性 可变 不可变 聚合根可变
比较方式 按ID比较 按属性比较 按聚合根ID比较
生命周期
典型例子 学生、订单、课程 地址、金额、颜色 订单(含订单项)

一句话总结:实体看“身份”,值对象看“值”,聚合管“一致性”。

3.5 实战建议

最后,给几个实战中的小建议:

  1. 先找实体,再找值对象。实体是业务的骨架,先把骨架搭起来。
  2. 值对象别偷懒。很多团队把“地址”直接写成字符串,结果后面要按城市统计、按区域筛选,全得改。一开始就设计成值对象,后面省大事。
  3. 聚合别太大,也别太小。聚合太大,事务范围广,性能差;聚合太小,一致性保证不了。我一般控制在3-5个对象以内。
  4. 画图别追求完美。UML类图是沟通工具,不是艺术品。能让人看懂就行,不用把所有getter/setter都画上去。

嗯,这一章就到这里。下一章,咱们聊聊如何从领域模型过渡到架构设计。到时候你会发现,模型画好了,架构设计就是水到渠成的事。