随着代码基数的增长,其复杂性必然会增加。由于这种情况,它往往变得更加难以保持代码的组织和结构,因为最初的意图,这被称为软件熵。在多次迭代中,如果不执行严格的架构准则,保持良好的关注分离以及正确脱钩类和模块将更具挑战性。

在传统的模型视图控制器 (MVC) 架构中,“M"层将掌握所有业务逻辑,但不会就如何保持适当的责任边界提供明确的规则。出现了几个模式来缓解这个问题,但组件之间始终存在逻辑和责任泄露的风险,随着模型的发展,维护性和稳定性变得更加棘手。

另一方面,与业务专家的沟通、需求收集以及技术和非技术团队之间的共识,以正确设计和实施解决业务问题的系统,是一个不断的迭次过程,很容易被误解,并最终使项目偏离其最初目标。

例如,命名内容一直是软件开发人员面临的最困难的挑战之一。我们应该足够清楚,让其他开发人员了解我们在代码中的意图,同时使用适当的命名选项,以促进与业务利益相关者的对话。

域驱动设计 (DDD) 试图通过调和软件项目中碰撞的技术和非技术力量,并提出一套有助于构建成功系统的实践和模式来解决这些挑战。

那么什么是域驱动设计呢?

让我们首先定义"域"一词在此上下文中的含义。我喜欢把它定义为

定义应用程序逻辑解决问题的一组常见要求、术语和功能的特定活动领域或知识领域。

域驱动设计是一种软件设计方法,将系统的实现粘合到不断发展的模型中,而忽略了编程语言、基础技术等不相关的细节。

它主要关注一个商业问题,以及如何严格组织解决它的逻辑。埃里克·埃文斯在他的著作《域驱动设计处理软件核心的复杂性》中首次描述了这种方法。

现在,我们知道 DDD 的定义及其目标是什么,让我们深入探讨此方法的三大支柱。

战略设计:拆分设计,以免失去理智

随着实施过程的不断演变,以及系统的复杂性逐渐增加,保持对系统的控制可能令人望而生畏。因此,一个严格的战略来理解和控制大型系统是根本的。将模型分解成相互交互的绑定上下文 - 它们本身在概念和代码上都有自己的统一模型 - 是避免复杂性陷阱的有效方法。

绑定上下文

边界上下文是围绕应用程序和/或项目在业务领域、团队和代码方面的部分概念边界。它对相关组件和概念进行分组,避免模棱两可,因为其中一些在没有明确上下文的情况下可能具有类似的含义。

例如,在"边界上下文"之外,“字母"可能意味着两种截然不同的东西:字符或写在纸上的消息。通过定义边界和上下文,您可以确定其含义

在许多项目中,团队被边界上下文划分,每个团队都专注于自己的领域专业知识和逻辑。

上下文映射

识别和图形记录项目中的每个边界上下文称为上下文映射。上下文地图有助于更好地了解绑定上下文和团队如何相互关联和沟通。他们清楚地了解了实际边界,并帮助团队直观地描述系统设计的概念细分。

image-20210924134313406

受限上下文之间的关系可能有所不同,具体取决于设计要求和其他项目特定限制,本文将省略某些关系,但以下四个除外:

反腐败层:

下游边界上下文实现一层,该层可以翻译来自上游上下文的数据或对象,确保它支持内部模型。

墨守成规:

下游边界上下文符合并适应上游上下文,如果需要,必须更改。在这种情况下,上游上下文对满足下游需求不感兴趣。

客户/供应商:

上游为下游提供服务,下游环境充当客户,确定需求并要求上游更改以满足其需求。

共享内核:

有时,两个(或更多)上下文不可避免地重叠,最终共享资源或组件。这种关系要求两种上下文在需要更改时保持连续同步,因此应尽可能避免这种关系。

协作建模:丰富的沟通和有效的协作

DDD 建议通过采用协作方式有效地建模该领域,不仅涉及技术,还涉及业务知识的所有各方。正如 Evans 描述的那样,域模型"不仅仅是域专家头脑中的知识:它是一个严格组织和选择性的抽象知识。

开发人员与域专家合作,旨在不断改进域模型,迫使他们学习他们试图解决的业务问题的重要细节和原则,而不仅仅是机械地生成代码。

为了实现业务团队和技术团队之间的这种协作,Domain 模型应使用一种将业务与技术行话连接在一起的语言,并找到所有团队成员都能理解和同意的中间立场,这称为无处不在的语言。使用定义明确的无处不在的语言将改善技术团队和业务团队之间的每一次互动,使他们不那么模棱两可,更有效。

最终,这种无处不在的语言将嵌入到代码中。

战术设计:DDD的螺母和螺栓

域对象之间实现关联并描述其功能一目了然,但应以清晰直观的方式正确区分其含义和存在原因。DDD 提出了一组构造和模式来实现它。

实体

具有独特身份并具有连续性线程的对象称为实体,它们不仅仅由其属性定义,而更多地由_它们是谁_来定义。它们的属性可能会发生变异,其生命周期可能会发生巨大变化,但它们的身份仍然存在。身份通过唯一键或保证为唯一属性的组合进行维护。

例如,在电子商务领域,订单具有唯一的标识符,它经历了几个不同的阶段:打开、确认、发货和其他阶段,因此它被视为域实体。

export class Customer {

    private id: number;
    private name: string;

    protected constructor(name: string) {
        // A uuid guarantees a unique identity for the Customer Entity
        this.id = uuidv4();
        this.name = this.setName(name);
    }

    private setName(name: string): string {
        // Business invariant: Customer name should not be empty
        if (name === undefined || name === '') {
            throw new Error('Name cannot be empty');
        }
        return name;
    }

    public static create(name: string): Customer {
        return new Customer(name);
    }
}

值对象

描述特征且不具有任何独特身份的对象称为值对象,它们只关心它们是什么,而不是它们是谁。

价值对象是多个实体的属性,并且可以共享,例如:两个客户可以具有相同的运费地址。虽然存在风险 - 如果他们的属性之一需要更改,所有共享它们的实体都会受到影响。为了防止这种情况,值对象必须是不变的,当需要更新时,迫使系统以新的实例替换它们。

此外,价值对象的创建应始终取决于用于创建它们的数据的有效性,以及它如何尊重业务不变。因此,如果数据无效,则不会创建对象实例。例如,在北美,具有非字母数字字符的邮政编码将违反业务不变,并会触发地址创建的例外情况。

export class Address {

    private readonly streetAddress: string;
    private readonly postalCode: string

    protected constructor(streetAddress: string, postalCode: string) {
        this.streetAddress = this.getValidStreetAddress(streetAddress);
        this.postalCode = this.getValidPostalCode(postalCode);
    }

    private getValidStreetAddress(streetAddress: string): string {
        // Business invariant: street address should not be longer than 128 characters
        if (streetAddress.length > 128) {
            throw new Error('Address should not be longer than 128 characters');
        }
        return streetAddress;
    }

    private getValidPostalCode(postalCode: string): string {
        // Business invariant: Should be a valid canadian postal code
        const pattern = /[a-z]\d[a-z][ \-]?\d[a-z]\d/g;
        if (!postalCode.match(pattern)) {
            throw new Error('Postal code should only contain alphanumeric caracters and spaces');
        }
        return postalCode;
    }

    public getStreetAddress(): string {
        return this.streetAddress;
    }

    public getPostalCode(): string {
        return this.postalCode;
    }

    public static create(streetAddress: string, postalCode: string): Address {
        return new Address(streetAddress, postalCode);
    }

    public equals(otherAddress: Address): boolean {
        // Value Objects equality is based on their propertie's values
        return objectHelper.isEqual(this, otherAddress);
    }
}

服务业

在许多情况下,域模型要求某些与实体或价值对象没有直接关系的操作或操作,迫使这些操作或操作进入其实施中会导致其定义失真。服务是提供无国籍操作的类。它们通常被命名为动词,而不是实体和值对象的名词,并且根据无处不在的语言命名。

应精心设计服务,始终确保它们不会剥夺实体和价值对象的直接责任和行为。它们还应是无国籍的,以便客户可以在应用程序使用期间无视该实例历史记录的任何给定服务实例。没有域逻辑的实体和值对象被认为是一种称为贫血域模型的反模式。

域对象及其生命周期

域对象通常具有复杂的生命周期,它们被刻解,经历几个变化,与其他对象相互作用,执行操作,持续,重组,被删除,等等。维护他们的完整性,同时确保系统不会管理他们复杂的生命周期,这是实施适当的域模型所代表的主要挑战之一。

要在域模型中保持可管理的复杂性,很难最大限度地减少域对象之间的关系和交互,尤其是在复杂的业务领域,或者正如 Eric Evans 描述的那样:

“很难保证具有复杂关联的模型中对象更改的一致性。需要维护适用于密切相关的对象组的不变物,而不仅仅是离散对象。然而,谨慎的锁定计划导致多个用户相互无意义地相互干扰,使系统无法使用”

集 料

为了减轻上述挑战,需要汇总实体和价值对象,限制违反业务不变。

聚合物是相关实体和价值对象的集合,聚集在一起表示交易边界。每个聚合体都有一个面向外侧的实体,并控制对边界内对象的所有访问,此实体称为聚合根,它是其他对象可以与之交互的唯一对象。聚合内的对象不能直接从外部世界调用,从而保持内部的一致性。

业务不变是保证聚合物完整性的业务规则,换句话说,它是一种确保其状态始终与业务规则一致的机制。例如,当某一产品的库存量为零时,永远无法下订单。

export class Order {

    private id: number;
    private isConfirmed: boolean;
    private total: number;
    private shippingAddress: Address;
    private customer: Customer;
    private items: Product[];
    private payments: Payment[];

    constructor(
        customer: Customer,
        shippingAddress: Address,
        items: Item[],
        payments: Payment[]
    ) {
        // Generate a unique identifier (UUID) for the Order Entity
        this.id = uuidv4();
        this.isConfirmed = false;
        this.total = 0;
        this.customer = customer;
        this.shippingAddress = shippingAddress;
        this.items = items.length ? items : [];
        this.payments = payments.length ? payments : [];
    }

    private getPaymentsTotal(): number {
        return this.payments.reduce((accumulator, payment) => accumulator + payment.total);
    }

    public addPayments(payment: Payment): void {
        this.payments.push(payment);
        this.total += payment.total;
    }

    public addItems(product: Product): void {
        // Business invariant: an order should not have items which are not in stock
        if (!product.getStockQuanity()) {
            throw new Error(`No stock for product id: ${product.id}`);
        }
        this.items.push(product);
    }

    public confirm(): void {
        // Business invariant: only fully paid orders can be confirmed
        if (this.total === this.getPaymentsTotal()) {
            throw new Error('Total amount paid does not equal order total');
        }
        this.isConfirmed = true;
    }
}

工厂

创建复杂的对象和聚合实例可能是一项艰巨的任务,还可以披露对象的内部详细信息。使用工厂,我们可以解决这个问题,并提供必要的封装。

工厂应能够在一次原子操作中构建域对象或聚合体,要求客户端在调用时提供所有需要的数据,并在创建的对象上强制执行所有不变。此活动不是域模型的一部分,但仍属于域层,因为它是适用于系统的业务规则的一部分。

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}`);
        }
    }
}

存储 库

为了能够从我们的持久性中检索对象,无论是内存、文件系统还是数据库,我们需要提供一个界面,向客户端隐藏实现详细信息,以便它不依赖于基础架构细节,而仅仅依赖于抽象。

存储库提供了域层可用于检索存储对象的界面,避免与存储逻辑紧密耦合,并给客户端一种直接从内存中检索对象的错觉。

需要注意的是,所有存储库界面定义都应位于域层,但其具体实现属于基础架构层。

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

将域与其他关注区隔离开来

专门解决域问题的代码部分只是整个代码库的一小部分。如果这部分与解决其他问题的代码交织在一起,将很难理解和改进。明确地将域逻辑与所有其他功能分离将减少泄漏,并避免在大型复杂系统中出现混淆。

DDD 提出了分层架构,通过将代码库分为 4 个主要层(用户界面、应用程序、域和基础结构)来区分问题和避免责任混淆。

这里的主要规则是,每个层中的组件只能取决于同一层的组件或其下方的任何层。上层只需调用其公共接口即可使用下层的组件,下层_只能_通过反向控制(IoC) 向上通信。

  • **用户界面层:**负责显示数据和捕获用户的命令。
  • **应用层:**作为域工作的编排器,它不知道域规则,但组织和委托域对象来完成他们的工作。它也是其他边界上下文中唯一可以访问的层。
  • **域层:**掌握业务逻辑和规则,以及业务状态。这是域模型的居住地。
  • **基础层:**实现应用所需的所有技术功能,以支持更高的层次、持久性、消息传递、层间通信等。

尽管并非每个系统都需要所有层,但域层的存在是 DDD 中的先决条件。

image-20210924134338172

结论

总之,DDD 是一种通过与域专家的丰富协作和严格的设计模式解决业务问题的整体方法,它并不是所有软件项目的通用解决方案,但应用得当,可以带来显著的好处。