两文读懂DDD领域驱动设计,举例说明,通俗易懂【值得收藏】

最近对架构莫名的感兴趣,慢慢觉得架构本身是为了提供方便,定制规范,目标一致并更好的协作,它的变动也并不是像变形金刚一样,而是像幼苗一样按规律成长起来的

DDD是一种方法也是一种思想,大家前面个别概念看不懂没关系,后面会举例讲,下面一一说一下DDD

DDD是什么

DDD(Domain-Driven Design,领域驱动设计)是一种软件开发方法,也是一种思想,它强调软件系统设计应该以问题领域为中心,而不是技术实现为主导。DDD通过一系列手段如统一语言、业务抽象、领域划分和领域建模等来控制软件复杂度,主要用来指导如何解耦业务系统、划分业务模块、定义业务领域模型及其交互,让开发以及相关人员能够清晰明了的开发和协作

DDD的优点

  • 提高业务理解能力:DDD强调与业务专家的紧密合作,有助于开发人员深入理解业务需求。
  • 降低系统复杂度:通过领域划分和领域建模,将复杂的业务系统拆分成若干个相对简单的子领域,从而降低系统复杂度。
  • 提高代码可维护性:领域模型为代码提供了清晰的业务语义,使得代码更易于理解和维护。
  • 提高开发效率:通过统一的领域模型和语言,减少了团队成员之间的沟通成本,提高了开发效率。
  • 促进团队合作,大家对于架构的理解是一致的

DDD的挑战

DDD很好,但是也不是谁都会

  1. 学习曲线陡峭:DDD概念复杂,理解需要时间,可以由架构师完成DDD的设计,讲解设计完成的架构以及开发时的相关使用问题以及原因
  2. 设计难度大:很多公司和项目没有足够的时间
  3. 工具支持不足:DDD几乎完全靠经验,工具很少

DDD设计的步骤

  • 1.需求分析:明确理解系统的业务需求和功能需求。
  • 2.梳理核心概念:通过领域调研,识别业务属性和概念
  • 3.定义限界上下文:进行业务分析,确定业务领域中的“限界上下文”,找到核心业务领域。
  • 4.领域建模:基于领域分析的结果,构建领域模型,包括实体、值对象、聚合、领域服务等。
  • 5.设计应用服务:应用层的设计,负责编排领域服务
  • 6.实现基础设施层:如数据库、适配器等等
  • 7.核心业务逻辑实现:根据领域模型,实现核心业务逻辑。
  • 8.持续改进和文档化

核心概念

DDD分层架构

图片

1.用户接口层(interfaces)

面向前端提供服务适配,接口就写在这里

  1. 处理restful请求,解析并基础校验
  2. 组装数据转换成DTO或者context
  3. 传递给应用层
  4. 处理应用层返回的数据转换VO给页面(正常原则为页面需要什么vo给什么,多余的不给,只是在实际开发过程中很多人不转直接就返回给前端了,把最大的数据体暴露给前端,虽然达到了最大的需要,但是消息体的大小和数据安全都有很多问题)

2.应用层(application)

  1. 组合和编排领域层的调用
  2. 向上为用户接口层提供数据的展示与支持服务
  3. 基础校验

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实体

  1. 通过领域服务进行协调封装
  2. 通过基础层查询

实体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;
	}		
}

值对象

值对象代表了业务中的不可变数据,它的身份由属性决定,比如地址,年龄,性别等等

特性
  • 无唯一标识符:它的身份由其属性决定,就是说比如性别这个属性决定了这个人的值对象
  • 不可变性:一旦创建,属性就不能改变,比如性别不能改变
  • 业务意义:值对象通常代表业务中的具体概念,如地址、货币金额、日期时间等等

使用场景:

  1. 描述性信息:描述实体的某些特性,比如地址,颜色,金额等等
  2. 复合属性
  3. 数学对象:如坐标
  4. 不可变数据:如订单中总价
示例
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对象就在这一层维护

注意

  1. 代码边界一定要清晰,尽可能松耦合和低关联,聚合的逻辑要在应用层实现对领域层的编排调用,不可以领域A调用领域B,领域层逻辑相对独立,若存在对其他领域的调用,应该拆分小粒度,由应用层去编排这个调用,这种松耦合的代码关联,在以后业务发展和需求变更时,可以很方便地实现业务功能和聚合代码的重组,在微服务架构演进中将会起到非常重要的作用
  2. 代码分层代码职责要清晰,各个层负责的是什么要清楚,核心业务逻辑不要放到应用层和接口层,否则就变成了传统的三层架构模型了,不要有坏代码的味道(这时你有疑问了,复杂业务逻辑必然是调用多方,无法放到领域层,别着急,后面专门解释)

好这篇就先写到这,大家可以理解一下,下一篇会介绍服务依赖关系、典型目录结构及示例、数据对象转换、设计思考 see you later~

这里路由到DDD二~~~~~~~~~~~~~~~~~~~~~
两文读懂DDD领域驱动设计(二),举例说明,通俗易懂【值得收藏】

参考文章:领域驱动设计DDD:https://mp.weixin.qq.com/s/mujGrnl6GZs0RmDr2vP9Kg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是小酒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值