Post

一文读懂DDD

本文并不是DDD的实践方法论,而是关注DDD的动机和内在逻辑,用程序员最了解的技术视角,来解释DDD与常见的技术的关系.

什么是DDD

DDD(Domain Drive Design)领域驱动设计,是一种针对复杂业务的大型系统的设计思想。 DDD并不是技术视角,也不是某种”软件架构“。这是很多程序员朋友初次接触容易踏入的理解误区。

为何需要DDD

单体应用应该是每一个应用的起点。就以Web MVC举例,最常见的单体分层架构,包含:数据访问层、业务层、应用层。数据访问层目前都被各种ORM占据,在代码中,仅存实体模型(BO),作为数据库表在代码中的映射。业务层则是对数据访问层的读写,组织成各种业务方法,应用层对外暴露接口(Restfual、GRPC)。 在单体应用中,如果业务较多较复杂,也会将业务层切割成不同的模块,例如“库存管理”、“用户管理”、“订单管理”等业务块。 但随着业务越来越大,这种单体的业务分割就难以维护和扩展,业务之间的调用和依赖关系,也会变得复杂晦涩。以上所说的业务规模,通常也不是单体应用可以应付的,分割后的业务需要微服务化,这样业务之间的调用,不再是单个进程内的调用,而是用Grpc、RestFul等方式多个微服务之间的调用,业务之间的副作用,需要用 EventBus 等技术来管理。 此时,划分业务就不再简单,更多的模块,需要更多的开发人员来协作,为了明确业务边界,统一沟通语言,DDD就应运而生。 DDD可以统一业务概念,方便不同职能的项目成员进行沟通,而且可以明确业务边界,让不同域能够独立开发,独立维护。

小结

DDD核心目标: • 解决复杂业务下如何进行合理的领域划分。 • 帮助开发团队通过统一语言(Ubiquitous Language)与领域专家共同构建模型,将业务复杂性以技术手段清晰地表达出来。

DDD技术与实现无关: • DDD 更关注业务规则、上下文边界(Bounded Context)和领域模型的设计,而不是具体使用了什么编程语言、框架或架构风格。 • 例如: • 领域模型可以通过面向对象的方式实现,也可以用函数式编程来表达。 • DDD 的边界划分理念同样适用于微服务架构、单体应用甚至事件驱动系统。

相关技术

尽管DDD并不是技术向的架构思想,但DDD的实践会有一些优秀的技术思想和框架。 如果技术人员想要学习DDD,并应用DDD,可以先从以下技术和概念入手。

充血模型

在 DDD 实践中,充血模型(Rich Domain Model)被广泛使用,尤其是在需要表达复杂业务逻辑的场景中。它更符合 DDD 的思想,因为它强调将业务逻辑直接集中在领域模型中,从而让模型不仅仅是数据的载体,而是业务行为的核心承载者。

充血模型是一种设计模式,其中领域对象(如类)不仅包含数据(属性),还包含与数据相关的业务逻辑(方法)。 与之相对的是贫血模型(Anemic Domain Model),其中对象只包含数据属性,业务逻辑集中在服务类(Service)中。

public class Order
{
    public Guid Id { get; private set; }
    public List<OrderItem> Items { get; private set; }
    public decimal TotalAmount { get; private set; }

    public Order()
    {
        Id = Guid.NewGuid();
        Items = new List<OrderItem>();
    }

    // 业务逻辑直接封装在领域模型中
    public void AddItem(OrderItem item)
    {
        Items.Add(item);
        RecalculateTotal();
    }

    public void RemoveItem(OrderItem item)
    {
        Items.Remove(item);
        RecalculateTotal();
    }

    private void RecalculateTotal()
    {
        TotalAmount = Items.Sum(i => i.Price * i.Quantity);
    }
}

在这个例子中,Order 不仅存储了订单数据,还直接封装了与订单相关的业务逻辑(如添加和删除订单项、计算总金额)。

聚合根(Aggregate Root)在 DDD 中是业务行为的入口,充血模型可以很好地将聚合内的行为和规则封装起来。

事件驱动

事件驱动是一种解耦方式。

在DDD实践中,领域层会定义其领域事件,来表达领域内触发的业务动作。 领域事件的核心特性包括:

  1. 发布-订阅模型

• 领域事件通常通过发布-订阅的方式传播。

• 发布者负责触发事件,订阅者(其他领域或子系统)对事件作出响应。

• 发布者与订阅者之间解耦,订阅者只需关心事件的结果,而不需要了解事件的来源。

  1. 最终一致性

• 事件驱动的系统通常采用最终一致性模型,即事件传播后,相关状态的更新可能是异步的。

• 例如,用户下单后,系统先生成订单,异步触发事件通知库存服务扣减库存。

  1. 时间不可逆性

• 领域事件类似日志,是领域中的历史记录。

• 一旦事件发生,它不能被撤销或修改,只能在系统中产生后续影响。

  1. 语言通用性

• 领域事件通常是领域语言的一部分(Ubiquitous Language),可以被业务和开发人员共同理解。

CQRS读写分离

CQRS 的全称是 命令与查询责任分离,其核心思想是将系统的 写操作(命令)与 读操作(查询)分离开来。 • 命令(Command):负责修改系统的状态(写操作)。例如:下单、支付、更新用户信息。 • 查询(Query):负责读取系统的状态(读操作)。例如:获取订单详情、用户列表。

CQRS 的分离操作通过将查询操作分到一层中而将命令分到另一层中来实现。 每一层都有自己的数据模型(注意,我们说的是模型,不一定是其他数据库),并且使用自己的模式和技术组合来构建。 更重要的是,这两层可以共处于同一层级或同一个微服务(进程)中, 也可以在不同的微服务或进程上实现,以便彼此毫不影响地分别进行优化和横向扩展。

  1. 与领域建模契合: 在 DDD 中,写操作的复杂领域规则通常由聚合(Aggregate)处理,而读操作则更偏向于简单的查询。如果直接用一个模型处理这两种需求,可能会导致模型变得臃肿且复杂。 CQRS 完美解决了这一问题,通过将读写分离,既可以保持写模型的领域内聚性,又可以优化读模型的查询性能。

  2. 分离责任: CQRS 强调命令和查询的职责分离,这与 DDD 中“关注点分离”的思想一致。命令专注于领域行为,查询专注于为界面或用户提供高效的数据。

  3. 配合事件驱动: 在 DDD 中,领域事件(Domain Event)是很重要的一部分。CQRS 常与事件驱动架构结合,写操作通过事件存储系统生成事件,读操作则订阅这些事件并更新自己的投影数据。

This post is licensed under CC BY 4.0 by the author.