第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 实战建议
最后,给几个实战中的小建议:
- 先找实体,再找值对象。实体是业务的骨架,先把骨架搭起来。
- 值对象别偷懒。很多团队把“地址”直接写成字符串,结果后面要按城市统计、按区域筛选,全得改。一开始就设计成值对象,后面省大事。
- 聚合别太大,也别太小。聚合太大,事务范围广,性能差;聚合太小,一致性保证不了。我一般控制在3-5个对象以内。
- 画图别追求完美。UML类图是沟通工具,不是艺术品。能让人看懂就行,不用把所有getter/setter都画上去。
嗯,这一章就到这里。下一章,咱们聊聊如何从领域模型过渡到架构设计。到时候你会发现,模型画好了,架构设计就是水到渠成的事。