最近对架构莫名的感兴趣,慢慢觉得架构本身是为了提供方便,定制规范,目标一致并更好的协作,它的变动也并不是像变形金刚一样,而是像幼苗一样按规律成长起来的
DDD是一种方法也是一种思想,大家前面个别概念看不懂没关系,后面会举例讲,下面一一说一下DDD
DDD是什么
DDD(Domain-Driven Design,领域驱动设计)是一种软件开发方法,也是一种思想,它强调软件系统设计应该以问题领域为中心,而不是技术实现为主导。DDD通过一系列手段如统一语言、业务抽象、领域划分和领域建模等来控制软件复杂度,主要用来指导如何解耦业务系统、划分业务模块、定义业务领域模型及其交互,让开发以及相关人员能够清晰明了的开发和协作
DDD的优点
- 提高业务理解能力:DDD强调与业务专家的紧密合作,有助于开发人员深入理解业务需求。
- 降低系统复杂度:通过领域划分和领域建模,将复杂的业务系统拆分成若干个相对简单的子领域,从而降低系统复杂度。
- 提高代码可维护性:领域模型为代码提供了清晰的业务语义,使得代码更易于理解和维护。
- 提高开发效率:通过统一的领域模型和语言,减少了团队成员之间的沟通成本,提高了开发效率。
- 促进团队合作,大家对于架构的理解是一致的
DDD的挑战
DDD很好,但是也不是谁都会
- 学习曲线陡峭:DDD概念复杂,理解需要时间,可以由架构师完成DDD的设计,讲解设计完成的架构以及开发时的相关使用问题以及原因
- 设计难度大:很多公司和项目没有足够的时间
- 工具支持不足:DDD几乎完全靠经验,工具很少
DDD设计的步骤
- 1.需求分析:明确理解系统的业务需求和功能需求。
- 2.梳理核心概念:通过领域调研,识别业务属性和概念
- 3.定义限界上下文:进行业务分析,确定业务领域中的“限界上下文”,找到核心业务领域。
- 4.领域建模:基于领域分析的结果,构建领域模型,包括实体、值对象、聚合、领域服务等。
- 5.设计应用服务:应用层的设计,负责编排领域服务
- 6.实现基础设施层:如数据库、适配器等等
- 7.核心业务逻辑实现:根据领域模型,实现核心业务逻辑。
- 8.持续改进和文档化
核心概念
DDD分层架构
1.用户接口层(interfaces)
面向前端提供服务适配,接口就写在这里
- 处理restful请求,解析并基础校验
- 组装数据转换成DTO或者context
- 传递给应用层
- 处理应用层返回的数据转换VO给页面(正常原则为页面需要什么vo给什么,多余的不给,只是在实际开发过程中很多人不转直接就返回给前端了,把最大的数据体暴露给前端,虽然达到了最大的需要,但是消息体的大小和数据安全都有很多问题)
2.应用层(application)
- 组合和编排领域层的调用
- 向上为用户接口层提供数据的展示与支持服务
- 基础校验
3.领域层(domain)
领域的核心逻辑,这一层聚集了领域模型的聚合、聚合根、实体、值对象、领域服务、事件等,以及他们组合所形成的业务能力
聚合
先说聚合,领域层中的聚合很重要,是由聚合根,实体,值对象组成;
聚合根是聚合的入口点,它本身就是一个实体,保证聚合内部的一致性,提供事务边界,封装内部逻辑(这个所谓的内部逻辑是指聚合某些属性和值对象)
比如有一个订单的聚合
- 聚合根:订单(Order)
- 实体:订单项(OrderLineItem)后面讲实体
- 值对象:地址(Address)后面讲值对象
//你看,它就是一个实体
public class Order {
private String orderId;
private List<OrderLineItem> items;
private Address deliveryAddress;
private OrderStatus status;
public Order(String orderId, Address deliveryAddress) {
this.orderId = orderId;
this.deliveryAddress = deliveryAddress;
this.items = new ArrayList<>();
this.status = OrderStatus.NEW;
}
public void addProduct(Product product, int quantity) {
OrderLineItem lineItem = new OrderLineItem(product, quantity);
this.items.add(lineItem);
}
public void placeOrder() {
// Business logic for placing the order
this.status = OrderStatus.PLACED;
}
public String getOrderId() {
return orderId;
}
public OrderStatus getStatus() {
return status;
}
public Address getDeliveryAddress() {
return deliveryAddress;
}
}
public class OrderLineItem {
private Product product;
private int quantity;
public OrderLineItem(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
public Product getProduct() {
return product;
}
public int getQuantity() {
return quantity;
}
}
public class Address {
private String street;
private String city;
private String state;
private String zipCode;
public Address(String street, String city, String state, String zipCode) {
this.street = street;
this.city = city;
this.state = state;
this.zipCode = zipCode;
}
// Getters and setters
}
问题来了,聚合根是个实体的话,如果我A聚合根想调用B实体,那不是耦合了吗?
是的,所以不能这么调用,那如何让A实体(聚合根)调用B实体
- 通过领域服务进行协调封装
- 通过基础层查询
实体Entity
这个实体并非是对应数据库表结构,它通常包含与业务紧密相关的行为,一般方法比较简单,因为只改实体本身,记住通常与单一实体状态相关的逻辑封装在实体中(涉及多个实体或复杂业务逻辑封装在领域服务中
),一般不直接调用基础层
示例如下
public class Order {
private PaymentStatus paymentStatus;
public void initializePayment(PaymentMethod paymentMethod) {
// 初始化支付,设置初始状态
this.paymentStatus = PaymentStatus.INITIALIZED;
}
public void confirmPayment(PaymentConfirmation confirmation) {
// 确认支付,更新状态
this.paymentStatus = PaymentStatus.CONFIRMED;
}
}
值对象
值对象代表了业务中的不可变数据,它的身份由属性决定,比如地址,年龄,性别等等
特性
- 无唯一标识符:它的身份由其属性决定,就是说比如性别这个属性决定了这个人的值对象
- 不可变性:一旦创建,属性就不能改变,比如性别不能改变
- 业务意义:值对象通常代表业务中的具体概念,如地址、货币金额、日期时间等等
使用场景:
- 描述性信息:描述实体的某些特性,比如地址,颜色,金额等等
- 复合属性
- 数学对象:如坐标
- 不可变数据:如订单中总价
示例
public final class PostalAddress {
private final String street;
private final String city;
private final String state;
private final String zipCode;
// 私有构造函数
private PostalAddress(String street, String city, String state, String zipCode) {
this.street = street;
this.city = city;
this.state = state;
this.zipCode = zipCode;
}
// 工厂方法
public static PostalAddress create(String street, String city, String state, String zipCode) {
return new PostalAddress(street, city, state, zipCode);
}
//只提供getter,没有setter
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
public String getState() {
return state;
}
public String getZipCode() {
return zipCode;
}
//重写equals,hashCode,toString()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PostalAddress that = (PostalAddress) o;
return Objects.equals(street, that.street) &&
Objects.equals(city, that.city) &&
Objects.equals(state, that.state) &&
Objects.equals(zipCode, that.zipCode);
}
@Override
public int hashCode() {
return Objects.hash(street, city, state, zipCode);
}
@Override
public String toString() {
return "PostalAddress{" +
"street='" + street + '\'' +
", city='" + city + '\'' +
", state='" + state + '\'' +
", zipCode='" + zipCode + '\'' +
'}';
}
}
问题来了
如何使用值对象呢?
- 在实体中使用值对象,可以看到依赖了
public class Customer {
private final String customerId;
private final String name;
private final PostalAddress address;
public Customer(String customerId, String name, PostalAddress address) {
this.customerId = customerId;
this.name = name;
this.address = address; // 值对象作为属性
}
public String getCustomerId() {
return customerId;
}
public String getName() {
return name;
}
public PostalAddress getAddress() {
return address;
}
}
- 在领域服务中使用,比如总价Money类中使用币种
public final class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
this.amount = amount;
this.currency = currency;
}
public static Money create(BigDecimal amount, Currency currency) {
return new Money(amount, currency);
}
public BigDecimal getAmount() {
return amount;
}
public Currency getCurrency() {
return currency;
}
public Money add(Money other) {
if (!currency.equals(other.getCurrency())) {
throw new IllegalArgumentException("Cannot add money with different currencies.");
}
return new Money(amount.add(other.getAmount()), currency);
}
}
领域服务
封装了不属于任何单一实体或值对象的复杂业务逻辑,领域服务通常包含跨多个实体的操作或者协调多个聚合
注意和聚合根的区别
事件
使用事件用来解耦,当某个实体发生变化时,可以发布一个领域事件,其他感兴趣的实体或者服务可以通过监听事件来响应,避免直接依赖,支持异步处理
4.基础层(infrastructure)
贯穿所有层,提供基础资源服务,如附件、配置、三方调用、数据库服务等,其中对应数据库表结构的PO对象就在这一层维护
注意
- 代码边界一定要清晰,尽可能松耦合和低关联,聚合的逻辑要在应用层实现对领域层的编排调用,不可以领域A调用领域B,领域层逻辑相对独立,若存在对其他领域的调用,应该拆分小粒度,由应用层去编排这个调用,这种松耦合的代码关联,在以后业务发展和需求变更时,可以很方便地实现业务功能和聚合代码的重组,在微服务架构演进中将会起到非常重要的作用
- 代码分层代码职责要清晰,各个层负责的是什么要清楚,核心业务逻辑不要放到应用层和接口层,否则就变成了传统的三层架构模型了,不要有坏代码的味道(这时你有疑问了,复杂业务逻辑必然是调用多方,无法放到领域层,别着急,后面专门解释)
好这篇就先写到这,大家可以理解一下,下一篇会介绍服务依赖关系、典型目录结构及示例、数据对象转换、设计思考 see you later~
这里路由到DDD二~~~~~~~~~~~~~~~~~~~~~
两文读懂DDD领域驱动设计(二),举例说明,通俗易懂【值得收藏】
参考文章:领域驱动设计DDD:https://mp.weixin.qq.com/s/mujGrnl6GZs0RmDr2vP9Kg