大家好,我是飘渺。既然有人催更那今天咱们就继续更新DDD&微服务系列!
在面向对象开发中,所有事物都可以看作是对象。然而,在日常开发中,我们通常从数据出发来设计对象的表现形式,这种做法侧重于数据属性的定义,而忽略了领域逻辑的处理过程。虽然这种做法很常见,但并不是DDD推荐的开发模式。在DDD中,我们关注的是领域数据对象,而非仅仅是数据本身。领域模型对象在本质上不同于数据,它包含了一系列的标识和行为定义,而不仅仅是数据属性的定义。
在DDD中,领域模型对象可以分为聚合、实体和值对象三种类型。实体和值对象是聚合的组成部分,而值对象同时也是实体的组成部分。它们之间的关系如下图所示:
在上一篇文章[[DailyMart02:DDD领域分解与微服务划分]]中,我们已经对DailyMart进行了领域分解和限界上下文的拆分。在本文中,我们将从DDD战术的角度出发,对DaliyMart进行设计,分析各个限界上下文中的实体、值对象和聚合对象。
1. 领域模型对象
让我们首先了解一下这几个关键对象。
- 实体(Entity):实体是具有唯一标识符(ID) 的对象,它在整个生命周期中保持不变。实体的属性可以发生变化,但其ID始终保持不变。例如,在DailyMart系统中,用户可以是一个实体,因为每个用户都有一个唯一的ID,即使他们的姓名、昵称、密码等信息发生变化,这个ID也不会改变。
- 值对象(ValueObject):值对象与实体对象相反,它是一种不具有唯一标识符(ID) 的对象,它们是通过其属性值来定义的,它的一个重要特点是它们是不可变的,一旦创建,它们的属性就不能被修改。如果需要修改值对象的属性,我们需要创建一个新的值对象来替换旧的值对象。值对象通常表示一些简单的概念,如颜色、地址或者金额等。例如在跨境电商系统中,商品的价格通常会被设计成值对象,价格由货币类型和数值组成,我们关心的是这两个属性的组合,而不是价格对象本身的唯一性。
- 聚合(Aggregate):聚合是一组相关的实体和值对象的集合,它们共同组成一个整体。聚合有一个根实体,也称为聚合根,它是聚合中最重要的实体,负责维护聚合内部的完整性和一致性。聚合根可以包含其他实体和值对象,但是其他实体和值对象不能直接访问聚合根以外的实体和值对象。这种限制可以保证聚合内部的一致性和完整性,同时也可以简化聚合的设计和实现。
2. 领域建模
接下来,我们将进行领域模型分析,并从用户限界上下文开始。
2.1 用户限界上下文
为了完成限界上下文的领域建模,我们需深入分析业务场景和需求,以形成业务领域的通用语言。
通过与业务人员的深入沟通,我们总结出以下通用语言:
- 商城用户首先需要注册,填写账号、密码、邮箱、手机号码等信息,注册成功后,系统赠送100积分。
- 用户购买商品后,系统会赠送一定积分。这些积分在支付时可以抵扣现金,用户还可以查看自己的积分记录。
- 用户登录系统后,可以维护自己的收货地址。每个用户可以维护多个地址,可以选择删除其中一个地址或将其中一个地址设置为默认收货地址。商品购买后,系统会将商品配送到默认地址。
根据这些业务需求,我们识别出了一些关键的领域模型:
CustomerUser
- 用户,显然是一个实体。它包含Username
、Password
、Email
、PhoneNumber
等属性,并拥有Points
(积分)属性、DeliveryAddresses
(收货地址列表)和PointsRecord
(积分记录)。PointsRecord
- 积分记录。由于我们需要追踪每次积分的变化,即使属性相同,每个积分记录也是独一无二的,因此它是一个实体。DeliveryAddress
- 收货地址。用户可以删除某个地址或将其他地址设置为默认地址,这意味着我们需要对地址进行操作和管理。在这种情况下,将收货地址作为实体可能更合适。Points
- 积分,即使只有一个属性,它也是一个值对象。将Points
设计为值对象的原因是它代表了一个具有特定含义和行为的概念。例如,积分不能为负数,并且在某些操作时需要增加或减少特定的数量。通过将其设计为值对象,我们可以封装这些规则和行为,保证数据的完整性。- 在用户限界上下文中,
CustomerUser
也是一个聚合,它封装了与用户相关的所有数据和行为,包括用户的基本信息、积分、收货地址和积分历史记录。
在实际操作中,实体与值对象的区别可能并不那么明显。例如,我们也可以把
PointsRecord
看作是值对象,因为系统只允许查看记录,不允许修改。从这个角度来看,积分记录就像用户积分变化的一个快照。然而,在这里,我们还是选择将其设计为实体,因为我们关心的是每一条独特的积分变化记录。
2.2 订单限界上下文
完成了用户限界上下文的设计后,现在我们进行订单限界上下文的领域模型设计。在“订单”限界上下文中,通过与业务人员深入沟通,我们总结出了以下通用语言:
- 用户可以在DailyMart网站上下单购买书籍,每个订单可以包含多本书籍。
- 订单需要包含收货人姓名、地址、联系电话等信息。
- 订单需要记录订单状态,比如待支付、待发货、已发货、已完成等。
- 用户可以在订单中查看每个书籍的详细信息,包括书名、价格、作者、ISBN号等。
- 用户可以在订单中查看订单的配送状态,比如已发货、运输中、已签收等。
根据这些业务需求,我们确定了一些关键的领域模型:
Order
- 订单是一个聚合,因为它有其自己的生命周期,每个订单有唯一的订单ID作为标识。订单包含了RecipientInfo
(收货人的信息,包括姓名、地址、联系电话),OrderStatus
(订单的状态),以及订单中的书籍列表。OrderItem
- 订单项是Order
聚合中的一个实体,代表订单中的一本书。它包含了书籍的详细信息,例如书名、价格、作者、ISBN号等。由于每个订单项都是独一无二的,并且在订单中具有持久性,因此被设计为实体。RecipientInfo
- 收货人信息是一个值对象,它没有自己的唯一标识,一旦订单创建就不能被修改,只能被替换。OrderStatus
- 订单状态是一个值对象,可以被设计成枚举类型,表示订单的不同状态,例如待支付、待发货、已发货、已完成等。DeliveryStatus
- 配送状态也是一个值对象,可以设计成枚举类型,表示配送的不同状态,例如已发货、运输中、已签收等。
2.3 商品限界上下文
接下来完成商品限界上下文的领域模型设计。在“商品”限界上下文中,通过与业务人员进行讨论和沟通,我们总结出了以下通用语言:
- DailyMart主要是销售书籍(纸质版/电子版)。商品属性相对简单,只需要包含书名、价格、作者、ISBN号、简介、出版日期等几个重要属性。
- 一本书会属于某一个分类,比如计算机类、设计类、心理学,管理员在另一个运营系统中可以配置分类。
- 管理员可以对书籍进行推荐。
- 用户登录后可以对书进行评论。
根据这些业务需求,我们识别出了一些关键的领域模型:
Book
- 书籍显然是一个实体,它需要一个唯一标识符。书籍包含ISBN
、title
、author
、price
、等属性,以及bookType
(电子书或纸质书),这可以设计成一个枚举类型。此外,Book还有Category
分类属性。Category
- 分类,由于管理员在另一个运营系统中进行配置和维护,分类不需要有太多复杂的行为和属性。在此上下文中,我们可以将Category
设计为值对象。Review
- 评论,Review 具有持久性的唯一标识(评价ID),应该也是一个实体对象。- 在商品限界上下文中,
Book
和Review
都设计成独立的聚合。Book
作为核心实体设计为聚合是很自然的,因为它包含了书籍的所有重要属性。而Review
则被设计为独立的聚合,使我们可以更方便地处理评论信息,包括添加、修改和查询等。
2.4 购物车上下文
最后,完成购物车限界上下文的领域模型设计。
在“购物车”限界上下文中,通过与业务人员进行深入沟通,我们总结出了以下通用语言:
- 用户登录后可以将商品添加到自己的购物车中,每个商品可以添加多个,同时可以添加多个商品
- 在购物车界面需要显示购物车所有商品的数量,以及每个商品的部分信息,如书名、作者、简介等。
- 在购物车中可以对商品进行选中,选中商品后自动计算商品的价格合计。
- 选中购物车的商品可以进行结算,但具体的订单生成和管理在另一个“订单”限界上下文中处理。
根据这些业务需求,我们识别出了一些关键的领域模型:
CartItem
- 购物车项,代表了用户添加到购物车的商品以及数量。由于购物车项有其自身的生命周期,可以单独创建、更新和删除,所以它应该被设计成一个实体。购物车项需要包含商品的部分信息,如书名、作者、简介等,以便在购物车界面中显示。Cart
- 购物车,这也是一个实体,购物车包含了多个购物车项,每个购物车项代表了用户添加到购物车的商品以及数量。
在购物车限界上下文中,Cart
被设计为核心聚合。它代表了用户的购物车状态,包含了用户希望购买的所有商品的信息。这样,我们可以更方便地处理购物车信息,包括添加商品、移除商品、计算总价等。
...
同样地,我们还可以分析出库存限界上下文和物流限界上下文的领域模型。下面是最终完整的领域模型:
3. 构建领域模型
完成领域建模后,我们可以根据领域模型生成关键领域对象的代码,以便更加准确地实现业务需求。例如,在订单上下文中,几个重要的对象包括:
@Data public class RecipientInfo { private String name; private String address; private String phoneNumber; } public enum OrderStatus { AWAITING_PAYMENT, AWAITING_SHIPMENT, SHIPPED, COMPLETED } public enum DeliveryStatus { SHIPPED, IN_TRANSIT, DELIVERED } @Data public class Order { private Long orderId; private Long userId; private LocalDateTime createdTime; private double totalAmount; private OrderStatus orderStatus; private DeliveryStatus deliveryStatus; private RecipientInfo recipientInfo; private List<OrderItem> orderItems; } @Data public class OrderItem { private Long itemId; private String bookTitle; private String author; private String ISBN; private double price; }
4. 设计数据库
同样的,还可以根据领域模型设计数据库表结构,以确保数据库能够准确地反映业务领域中的实体、值对象和聚合等概念。例如,下面是订单表的数据表设计:
create table order ( order_id bigint , user_id bigint , created_time datetime , total_amount decimal(10, 4) , order_status varchar(10) , deliveryStatus varchar(10) , recipient_name varchar(50) , recipient_address varchar(255) , recipient_phone_number varchar(255) , constraint order_pk primary key (order_id) ); create table order_item ( item_id bigint, orderId bigint, bookTitle VARCHAR(255), author VARCHAR(255), ISBN VARCHAR(255), price decimal(10, 4), constraint order_item_pk primary key (item_id) );
5. 小结
本文采用DDD战术,对 DailyMart 进行领域模型设计。通过识别出实体、值对象和聚合等概念,我们得到了 DailyMart 的完整领域模型,并根据该模型完成了关键领域对象的代码构建和关键表设计。
需要注意的是,领域模型并不是一成不变的,它会随着业务需求的变化而不断调整和优化,随着DailyMart商城系统的开发深入,我也会不断优化和补充它的领域模型。