【Domain】领域驱动设计(Domain Driven Design)

Posted by 西维蜀黍 on 2021-08-29, Last Modified on 2023-08-25

Domain-driven design (DDD)

Domain-driven design (DDD) is the concept that the structure and language of software code (class names, class methods, class variables) should match the business domain. So that the designed system is high conhesion and low coupling

For example, if a software processes loan applications, it might have classes such as LoanApplication and Customer, and methods such as AcceptOffer and Withdraw.

DDD connects the implementation to an evolving model.

Domain-driven design is predicated on the following goals:

  • placing the project’s primary focus on the core domain and domain logic;
  • basing complex designs on a model of the domain;
  • initiating a creative collaboration between technical and domain experts to iteratively refine a conceptual model that addresses particular domain problems.

Criticisms of domain-driven design argue that developers must typically implement a great deal of isolation and encapsulation to maintain the model as a pure and helpful construct. While domain-driven design provides benefits such as maintainability, Microsoft recommends it only for complex domains where the model provides clear benefits in formulating a common understanding of the domain.

The term was coined by Eric Evans in his book (Domain-Driven Design: Tackling Complexity in the Heart of Software) of the same title

DDD 的发展史

相信之前或多或少一定听说过领域驱动(DDD),繁多的概念会不会让你眼花缭乱?抽象的逻辑是不是感觉缺少落地实践?可能这也是 DDD 一直没得到盛行的原因吧。

话说 1967 年有了 OOP,1982 年有了 OOAD(面向对象分析和设计),它是成熟版的 OOP,目标就是解决复杂业务场景,这个过程中逐渐形成了一个领域驱动的思潮,一转眼到 2003 年的时候,Eric Evans 发表了一篇著作 Domain-driven Design: Tackling Complexity in the Heart of Software,正式定义了领域的概念,开始了 DDD 的时代。算下来也有接近 20 年的时间了,但是,事实并不像 Eric Evans 设想的那样容易,DDD 似乎一直不温不火,没有能“风靡全球”。

2013 年,Vaughn Vernon 写了一本 Implementing Domain-Driven Design 进一步定义了 DDD 的领域方向,并且给出了很多落地指导,它让人们离 DDD 又进了一步。

同时期,随着互联网的兴起,Rod Johnson 这大哥以轻量级极简风格的 Spring Cloud 抢占了所有风头,虽然 Spring 推崇的失血模式并非 OOP 的皇家血统,但是谁用关心这些呢?毕竟简化开发的成本才是硬道理。

就在我们用这张口闭口 Spring 的时候,我们意识到了一个严重的问题,我们应对复杂业务场景的时候,Spring 似乎并不能给出更合理的解决方案,于是分而治之的思想下应生了微服务,一改以往单体应用为多个子应用,一下子让人眼前一亮,于是我们没日没夜地拆分服务,加之微服务提供的注册中心、熔断、限流等解决方案,我们用得不亦乐乎。

人们在踩过诸多拆分服务的坑(拆分过细导致服务爆炸、拆分不合理导致频分重构等)之后,开始死锁原因了,到底有没有一种方法论可以指导人们更加合理地拆分服务呢?众里寻他千百度,DDD 却在灯火阑珊处,有了 DDD 的指导,加之微服务的事件,才是完美的架构。

DDD 与微服务的关系

背景中我们说到,有 DDD 的指导,加之微服务的事件,才是完美的架构,这里就详细说下它们的关系。

Discussion 1

系统的复杂度越来越来高是必然趋势,原因可能来自自身业务的演进,也有可能是技术的创新,然而一个人和团队对复杂性的认知是有极限的,就像一个服务器的性能极限一样,解决的办法只有分而治之,将大问题拆解为小问题,最终突破这种极限。微服务在这方面都给出来了理论指导和最佳实践,诸如注册中心、熔断、限流等解决方案,但微服务并没有对“应对复杂业务场景”这个问题给出合理的解决方案,这是因为微服务的侧重点是治理(即要进行分而治之,且通过 service registry and service discovery 的方式进行分而治之),但并具体进行“分而治之”时,对于“微服务的service之间的boundary如何划分”的问题,微服务并没有回答。

这时候,DDD 就登场了,或者称为 DDD-oriented microservice

我们都知道,架构一个系统的时候,应该从以下几方面考虑:

  1. 功能维度
  2. 质量维度(包括性能和可用性)
  3. 工程维度

微服务在第二个做得很好,但第一个维度和第三个维度做的不够。这就给 DDD 了一个“可乘之机”,DDD 给出了微服务在功能划分上没有给出的很好指导这个缺陷。所以说它们在面对复杂问题和构建系统时是一种互补的关系。

从架构角度看,微服务中的服务所关注的范围,正是 DDD 所推崇的六边形架构中的领域层,和整洁架构中的 entity 和 use cases 层。如下图所示:

Discussion 2

领域驱动设计是由 Eric Evans 在一本《领域驱动设计》书中提出的,它是针对复杂系统设计的一套软件工程方法;而微服务是一种架构风格,一个大型复杂软件应用是由一个或多个微服务组成的,系统中的各个微服务可被独立部署,各个微服务之间是松耦合的,每个微服务仅关注于完成一件任务并很好地完成该任务。

两者之间更深入的关系,主要体现在领域驱动设计中限界上下文与微服务之间的映射关系。假如限界上下文之间需要跨进程通信,并形成一种零共享架构,则每个限界上下文就成为了一个微服务。在微服务架构大行其道的当今,我们面临的一个棘手问题是:如何识别和设计微服务?领域驱动的战略设计恰好可以在一定程度上解决此问题。

DDD 和 Clean Architecture

总结来说,DDD主要为了解决业务架构的问题,而 Clean Architecture更多为了解决技术架构(项目架构)的问题。

整洁架构最主要原则是依赖原则,它定义了各层的依赖关系,越往里,依赖越低,代码级别越高。外圆代码依赖只能指向内圆,内圆不知道外圆的任何事情。一般来说,外圆的声明(包括方法、类、变量)不能被内圆引用。同样的,外圆使用的数据格式也不能被内圆使用。

DDD and DAO

传统分层开发模式 action/service/dao 所导致的贫血模式就存在这样的问题,service层只关注行为,而do/dto等对象只关注数据,这在简单业务阶段其实是很敏捷的一种开发思路。然而随著业务趋于复杂,你可能会发现service层的行为逻辑很难再直观地与业务功能点对应起来。从一个功能接口进去,直面的就是层层嵌套的判断循环逻辑与数据对象,而数据对象又不一定能够直接映射为业务核心对象,这无疑是增加了业务理解的难度的。

相反地,DDD通过在其战术设计里提倡的富血模型(实体里既有数据又有行为),使得我们的代码抽象能够更加贴近业务与现实,毕竟在现实场景里,数据模型与行为总是不可分割的。

DDD 实践

一个系统(或者一个公司)的业务范围和在这个范围里进行的活动,被称之为领域,领域是现实生活中面对的问题域,和软件系统无关,领域可以划分为子域,比如电商领域可以划分为商品子域、订单子域、发票子域、库存子域 等,在不同子域里,不同概念会有不同的含义,所以我们在建模的时候必须要有一个明确的边界,这个边界在 DDD 中被称之为限界上下文,它是系统架构内部的一个边界,《整洁之道》这本书里提到:

系统架构是由系统内部的架构边界,以及边界之间的依赖关系所定义的,与系统中组件之间的调用方式无关。 所谓的服务本身只是一种比函数调用方式成本稍高的,分割应用程序行为的一种形式,与系统架构无关。

所以复杂系统划分的第一要素就是划分系统内部架构边界,也就是划分上下文,以及明确之间的关系,这对应之前说的第一维度(功能维度),这就是 DDD 的用武之处。其次,我们才考虑基于非功能的维度如何划分,这才是微服务发挥优势的地方。

DDD 中的概念

Domain

领域

映射概念:切分的服务。

领域就是范围。范围的重点是边界。领域的核心思想是将问题逐级细分来减低业务和系统的复杂度,这也是 DDD 讨论的核心。

子域

映射概念:子服务。

领域可以进一步划分成子领域,即子域。这是处理高度复杂领域的设计思想,它试图分离技术实现的复杂性。这个拆分的里面在很多架构里都有,比如 C4。

核心域

映射概念:核心服务。

在领域划分过程中,会不断划分子域,子域按重要程度会被划分成三类:核心域、通用域、支撑域。

决定产品核心竞争力的子域就是核心域,没有太多个性化诉求。

桃树的例子,有根、茎、叶、花、果、种子等六个子域,不同人理解的核心域不同,比如在果园里,核心域就是果是核心域,在公园里,核心域则是花。有时为了核心域的营养供应,还会剪掉通用域和支撑域(茎、叶等)。

通用域

映射概念:中间件服务或第三方服务。

被多个子域使用的通用功能就是通用域,没有太多企业特征,比如权限认证。

支撑域

映射概念:企业公共服务。

对于功能来讲是必须存在的,但它不对产品核心竞争力产生影响,也不包含通用功能,有企业特征,不具有通用性,比如数据代码类的数字字典系统。

The Ubiquitous Language

The Ubiquitous Language is a methodology that refers to the same language domain experts and developers use when they talk about the domain they are working on. This is necessary because projects can face serious issues with a disrupted language. This happens because domain experts use their own jargon. At the same time, tech professionals use their own terms to talk about the domain.

There’s a gap between the terminology used in daily discussions and the terms used in the code. That’s why it’s necessary to define a set of terms that everyone uses. All the terms in the ubiquitous language are structured around the domain model.

限界上下文(Bounded Context)

A Bounded Context is a conceptual boundary around parts of the application and/or the project in terms of business domain, teams, and code. It groups related components and concepts and avoids ambiguity as some of these could have similar meanings without a clear context.

For example, outside of a Bounded Context, a “letter” could mean two very different things: either a character or a message written on paper. By defining a boundary and context, you can determine its meaning

In many projects, teams are split by Bounded Contexts, each of them specializing on its own domain expertise and logic.

微服务架构众所周知,此处不做赘述。我们创建微服务时,需要创建一个高内聚、低耦合的微服务。而DDD中的限界上下文则完美匹配微服务要求,可以将该限界上下文理解为一个微服务进程。

这里我为你总结了全部的四种领域模式,供你区分和理解:

  1. 失血模型
  2. 贫血模型(anemic model)
  3. 充血模型
  4. 胀血模型

Entities(实体)

Objects that have a unique identity and possess a thread of continuity are called Entities, they are not defined solely by their attributes, but more by who they are. Their attributes might mutate and their life cycles can drastically change, but their identity persists. Identity is maintained via a unique key or a combination of attributes guaranteed to be unique.

In the e-commerce domain for example, an order has a unique identifier and it goes through several different stages: open, confirmed, shipped, and others, therefore it’s considered a Domain Entity.

当一个对象由其标识(而不是属性)区分时,这种对象称为实体(Entity)。

例:最简单的,公安系统的身份信息录入,对于人的模拟,即认为是实体,因为每个人是独一无二的,且其具有唯一标识(如公安系统分发的身份证号码)。

Value Objects

As Eric Evans has noted, “Many objects do not have conceptual identity. These objects describe certain characteristics of a thing.”

An entity requires an identity, but there are many objects in a system that do not, like the Value Object pattern. A value object is an object with no conceptual identity that describes a domain aspect. These are objects that you instantiate to represent design elements that only concern you temporarily. You care about what they are, not who they are. Examples include numbers and strings, but can also be higher-level concepts like groups of attributes.

Something that is an entity in a microservice might not be an entity in another microservice, because in the second case, the Bounded Context might have a different meaning. For example, an address in an e-commerce application might not have an identity at all, since it might only represent a group of attributes of the customer’s profile for a person or company. In this case, the address should be classified as a value object. However, in an application for an electric power utility company, the customer address could be important for the business domain. Therefore, the address must have an identity so the billing system can be directly linked to the address. In that case, an address should be classified as a domain entity.

A person with a name and surname is usually an entity because a person has identity, even if the name and surname coincide with another set of values, such as if those names also refer to a different person.

当一个对象用于对事务进行描述而没有唯一标识时,它被称作值对象(Value Object)。

例:比如颜色信息,我们只需要知道{“name”:“黑色”,”css”:“#000000”}这样的值信息就能够满足要求了,这避免了我们对标识追踪带来的系统复杂性。

Value objects and aggregates

Value objects have attributes, but can’t exist on their own. For example, the shipping address can be a value object. Large and complicated systems have countless entities and value objects. That’s why the domain model needs some kind of structure. This will put them into logical groups that will be easier to manage. These groups are called aggregates. They represent a collection of objects that are connected to each other, with the goal to treat them as units. Moreover, they also have an aggregate root. This is the only entity that any object outside of the aggregate can reference to.


Objects outside the aggregate are allowed to hold references to the root but not to any other object of the aggregate. The aggregate root checks the consistency of changes in the aggregate. Drivers do not have to individually control each wheel of a car, for instance: they simply drive the car. In this context, a car is an aggregate of several other objects (the engine, the brakes, the headlights, etc.).

The Aggregate Root

An aggregate is composed of at least one entity: the aggregate root, also called root entity or primary entity. Additionally, it can have multiple child entities and value objects, with all entities and objects working together to implement required behavior and transactions.

The purpose of an aggregate root is to ensure the consistency of the aggregate; it should be the only entry point for updates to the aggregate through methods or operations in the aggregate root class. You should make changes to entities within the aggregate only via the aggregate root. It is the aggregate’s consistency guardian, considering all the invariants and consistency rules you might need to comply with in your aggregate. If you change a child entity or value object independently, the aggregate root cannot ensure that the aggregate is in a valid state. It would be like a table with a loose leg. Maintaining consistency is the main purpose of the aggregate root.

In Figure 7-9, you can see sample aggregates like the buyer aggregate, which contains a single entity (the aggregate root Buyer). The order aggregate contains multiple entities and a value object.

A DDD domain model is composed from aggregates, an aggregate can have just one entity or more, and can include value objects as well. Note that the Buyer aggregate could have additional child entities, depending on your domain, as it does in the ordering microservice in the eShopOnContainers reference application. Figure 7-9 just illustrates a case in which the buyer has a single entity, as an example of an aggregate that contains only an aggregate root.

Factories

Creating complex object and aggregate instances can be a daunting task, and can also disclose too much of the object’s internal details. Using Factories, we can solve this problem and provide the necessary encapsulation.

A Factory should be able to construct domain objects or aggregates in one atomic operation, requiring all data needed to be provided by the client when called, and enforcing all invariants on the created object. This activity is not part of the Domain Model, but still belongs in the Domain Layer as it is part of the business rules that apply to the system.

export class OrderFactory implements Factory {

    private customerEntity: Customer;
    private addressValue: Address;
    private productsRepository: Repository;
    private paymentsRepository: Repository;

    constructor(customerEntity: Customer, addressValue: Address, productsRepository: Repository, paymentsRepository: Repository) {
        this.customerEntity = customerEntity;
        this.addressValue = addressValue;
        this.productsRepository = productsRepository;
        this.paymentsRepository = paymentsRepository;
    }

    public async createOrder(customerName: string, addressDto: AddressDto, itemDtos: ItemDto[], paymentDtos: PaymentDto[]): Order {
        try {
            const customer = this.customerEntity.create(customerName);
            const shippingAddress = this.addressValue.create(addressDto.streetAddress, addressDto.postalCode);
            const items = await this.productsRepository.getProductCollection(itemDtos);
            const payments = await this.paymentsRepository.getPaymentCollection(paymentDtos);

            return new Order(customer, shippingAddress, items, payments);
        } catch(err) {
            // Error handling logic should go here
            throw new Error(`Order creation failed: ${err.message}`);
        }
    }
}

Repositories

To be able to retrieve objects from our persistence, be it in-memory, filesystem, or database, we need to provide an interface that hides the implementation details from the client, so that it does not depend on the infrastructure specifics, but merely on an abstraction.

Repositories provide an interface that the Domain Layer can use to retrieve stored objects, avoiding tight-coupling with the storage logic and giving the client an illusion that the objects are being retrieved directly from memory.

It’s important to mention that all repository interface definitions should reside in the Domain Layer, but their concrete implementations belong in the Infrastructure Layer.

export class OrderRepository implements Repository {
    private model: OrderModel;
    private mapper: OrderMapper;
    private productsRepository: Repository;
    private paymentsRepository: Repository;

    constructor(orderModel: OrderModel, orderMapper: Mapper, productsRepository: Repository, paymentsRepository: Repository) {
        this.model = orderModel;
        this.mapper = orderMapper;
        this.productsRepository = productsRepository;
        this.paymentsRepository = paymentsRepository;
    }

    public async getById(orderId: number): Promise<Order> {
        const order = await this.model.findOne(orderId);

        if (!order) {
            throw new Error(`No order found with order id: ${orderId}`);
        }
        return this.mapper.toDomain(order);
    }

    public async save(order: Order): Promise<Boolean> {
        const orderRecord: OrderRecord = this.mapper.toPersistence(order);

        try {
            await this.productsRepository.insert(order.items);
            await this.paymentsRepository.insert(order.payments);

            if (!!await this.getById(order.id)) {
                await this.model.update(orderRecord);
            } else {
                await this.model.insert(orderRecord);
            }
        } catch (err) {
            // call to rollback mechanism should go here
            return false;
        }
        return true;
    }
}

Services

In most domains, some operations don’t conceptually belong to any specific object. Previous design methods tried to force those operations into an entity-based model, often with adverse consequences, especially for operations that operated on a group of related Entities. Instead, you model those objects as stand-alone interfaces called Services. The rules for a good service are as follows:

  • It is stateless
  • The interface is defined in terms of other elements of your domain model (Entities and Value Objects)
  • It refers to a domain concept that doesn’t naturally correspond to any particular Entity or Value Object

These operations are part of the business domain—they’re not technical issues of implementation. Things like “Login,” “Authentication,” and “Logging” aren’t appropriate Services of this type. However, a concept like “Funds Transfer” in banking or “Adjudication” in insurance might be.

Example

Reference

Martin Fowler

Misc

Books

  • Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans

  • Domain-Driven Design Reference: Definitions and Pattern Summaries by Eric Evans

  • Implementing Domain-Driven Design by Vaughn Vernon

    • 翻译:《实现领域驱动设计》 滕云
  • Domain-Driven Design Distilled by Vaughn Vernon

  • 《解构领域驱动设计》 - 张逸