书接上回
领域驱动设计(Domain-Driven Design DDD)——模型驱动设计的构造块1-CSDN博客
? ? ? ? 每个对象都有生命周期,管理这些对象面临诸多挑战,主要的挑战有以下两类。
? ? ? ? 我们将用Aggregate、Factory和Repository三种模式来管理对象的全生命周期。
? ? ? ? Aggregate(聚合)来定义对象的所属关系和边界,避免错综复杂的对象关系网来实现模型的内聚。
????????Factory(工厂)来创建和重建复杂对象和Aggregate,从而封装它们的内部结构。
????????Repository(存储库)来固化、持久化对象,并提供查找、检索和归档等功能。
? ? ? ? 使用Aggregate进行建模,并且在设计中结合使用Factory和Repository,这样我们就能够在模型对象的整个生命周期中,以有意义的单元、系统地操纵它们。Aggregate可以划分出一个范围(包含一个或一组模型元素),这个范围内的模型元素在生命周期各个阶段都应该去维护其固定规则。Factory和Repository在Aggregate基础上进行操作,将特定生命周期转换的复杂性封闭起来。
? ? ? ? 在大多数业务领域中的对象都具有十分复杂的联系,最终会形成很长、很深的对象引用路径,我们不得不在这个路径上追踪对象。某种程度上,这是现实,因为现实世界中就少有清晰的边界。但软件设计上却需要有清晰的定义。
? ? ? ? 在具有复杂关联的模型中,要想保证对象更改的一致性是很困难的。不仅互不关联的对象需要遵守一些固定规则,而且紧密关联的各组对象也要遵守一些固定规则。但是,过于谨慎的锁定机制又会导致多个用户之间毫无意义地相互干扰,从而使系统不可用。
? ? ? ? 要想找到一种兼顾各种问题的解决方案,要求我们对领域有深刻的理解。例如,要了解特定类实例间的更改频率这样的深层次因素。需要找到一个使对象间冲突较少而固定规则联系更紧密的模型。
? ? ? ? 为了实现上述要求,首先我们要引入一个抽象的封装模型,来封装相互间影响较深、较重要的关联。Aggregate就是一组相关对象的集合,我们把它作为数据修改的单元。每个Aggregate都有一个根(root)和一个边界(Boundary)。边界定义了Aggregate内部有什么。根则是Aggregate所包含的一个特定Entity。对Aggregate而言,外部对象只可以引用根,而边界内部的对象间则可以互相引用。除根以外的其他Entity都有本地标识,但这些标识只在Aggregate内部才需要加以区别,因为外部除了根Entity外看不到其他对象。
? ? ? ? 在Aggregate成员之间的内部关系,需有一些固定规则(Invariant)以便在数据变化时保持其一致性。而任何跨越Aggregate的规则将不要求时刻保持最新状态。但在每个事务完成时,Aggregate内部所应用的固定规则必须得到满足。现在,为了实现这个概念上的Aggregate,需要对所有事务应用一组规则。
? ? ? ? 我们应该将Entity和Value Object分门别类地聚集到Aggregate中,并定义Aggregate的边界。在每个Aggregate中,选择一个Entity作为根,并通过根来控制对边界内其他对象的访问。只允许外部对象保持对根的引用。对内部成员的临时引用可以被传递出去,但仅在一次操作中有效。由于根控制访问,因此不能绕过它来修改内部对象。这种设计有利于确保Aggregate中的对象满足所有固定规则,也可以确保在任何状态变化时Aggregate作为一个整体满足固定规则。
? ? ? ? Aggregate划分出一个范围,在这个范围内,生命周期的每个阶段都必须满足一些固定规则。
? ? ? ? 当创建一个对象或创建整个Aggregate时,如何创建工作很复杂,或者暴露了过多的内部结构,则可以使用Factory进行封装。
? ? ? ? 对象的功能主要体现在其复杂的内部配置以及关联方面。我们应该一直对对象进行提炼,直到所有与其意义或在交互中的角色无关的内容被完全删除为止。一个对象在它的生命周期中要承担大量职责。如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。
? ? ? ? 复杂的对象创建是领域层的职责,然而这项任务并不属于那些用于表示模型的对象。在有些情况下,对象的创建和装配对应于领域中的重要事件,如“开立银行账户”。但一般情况下,对象的创建和装配在领域中并没有什么意义,它们只不过是实现的一种需要。为了解决这一问题,我们必须在领域设计中增加一种新构造,它不是Entity、Value Object,也不是Service。这与前一章的论述相违背。因此我们正在向设计中添加一些新元素,但它们不对应于模型中的任何事物,而确实又承担领域层的部分职责。
? ? ? ? 每种面向对象的语言都提供一种创建对象的机制,但我们仍然需要一种更加抽象且不与其他对象发生耦合的构造机制。这就是Factory,它是一种负责创建其他对象的程序元素。
? ? ? ? 正如对象的接口封装了对象的实现一样,Factory封装了创建复杂对象或Aggregate所需的知识。它提供了客户目标的接口,以及被创建对象的抽象视图。
? ? ? ? 因此:
? ? ? ? 应该将创建复杂对象的实例和Aggregate的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建Aggregate时要把它作为一个整体,并确保它满足固定规则。
? ? ? ? Factory有很多种设计方式。【Gamma et al. 1995】中详尽论述了几种特定目的的创建模式,包括Factory Method(工厂方法)、Abstract Factory(抽象工厂)和Builder(构造器)。
? ? ? ? Factory是领域设计的重要组件,正确使用Factory有助于保证Model-Driven Design沿正确的轨道前进。
? ? ? ? 任何好的工厂都需要满足以下两个基本需求。
? ? ? ? (1)每个创建方法都是原子的,而且要保证被创建对象或Aggregate的所有固定规则。
? ? ? ? (2)Factory应该被抽象为所需的类型,而不是所要求创建的具体的类。【Gamma et al. 1995】中的高级Factory模式介绍了这一话题。
? ? ? ? 一般来说,Factory的作用是隐藏创建对象的细节,而且我们把Factory用在那在需要隐藏细节的地方。这些决定通常与Aggregate有关。
? ? ? ? 例如,如果需要向一个已存在的Aggregate添加元素,可以在Aggregate的根上创建一个Factory Method。另外,也可以在一个对象上使用Factory Method,这个对象与生成的另一个对象密切相关,但它并不拥有所生成的对象。
? ? ? ? Factory与被构建对象之间是紧密耦合的,因此Factory应该只是被关联到与被构建对象有着密切联系的对象上。当有些细节需要隐藏而双找不到合适的地方来隐藏它们时,必须创建一个专用的Factory对象或Service。
? ? ? ? 常见代码都是直接调用类构造函数来创建的,或者是使用编程语言的最基本的实例创建方式。Factory的引入提供了巨大的优势,而这种优势往往未得到充分利用。在有些情况下直接使用构造函数确实是最佳选择。
? ? ? ? 在以下情况下最好使用简单的、公共的构造函数。
? ? ? ? 不要在构造函数中调用其他类的构造函数。构造函数应该保持绝对简单。复杂的装配,特别是Aggregate,需要使用Factory。使用Factory Method的门槛并不高。
? ? ? ? 虽然没有使用Factory,但抽象集合类型仍然具有一定的价值,原因在于它们使用模式。集合通常在一直地方创建,而在其他地方使用。这意味着最终使用集合的客户仍可以与接口进行对话,从而不与实现发生耦合。集合类的选择通常由拥有该集合的对象来决定,或是由该对象的Factory来决定。
? ? ? ? 当设计Factory的方法签名时,无论是独立的Factory还是Factory Method,都要记住以下两点。
? ? ? ? 使用抽象类型的参数,而不是他们的具体类。Factory与被构建对象的具体类发生耦合,而无需与具体的参数发生耦合。
? ? ? ? Factory可以将固定规则的检查工作委派给被创建对象,而且这通常是最佳选择。
? ? ? ? 在某些情况下,把固定规则的相关逻辑放到Factory中是有好处的,这样可以让被创建对象的职责更明晰。
? ? ? ? Entity Factory与Value Object Factory有两个方面的不同。由于Value Object是不可变的,因此,Factory所生成的对象就是最终形式。因此Factory操作必须得到被创建对象的完整描述。而Entity Factory则只需具有构造有效Aggregate所需的那些属性。对于固定规则不关心的细节,可以之后再添加。
? ? ? ? 在某一时刻,检索操作需要将存储后或网络传输后的数据重新装配成一个可用的对象。用于重建对象的Factory与用于创建对象的Factory很类似,但也有以下两点不同。
? ? ? ? (1)用于重建对象的Entity Factory不分配新的跟踪ID。如果重新分配ID,将丢失与先前对象的连续性。因此,在重建对象的Factory中,标识属性必须是输入参数的一部分。
? ? ? ? (2)当固定规则未满足时,重建对象的Factory采用不同的方式进行处理。若重建对象失败,必须通过某种策略来修复这种失败,这使得重建对象比创建对象更困难。
? ? ? ? Factory封装了对象创建和重建时的生命周期转换。还有一种转换大大增加了领域设计的技术复杂性,这是对象与存储之间的互相转换。这咱转换由另一种领域设计构造来处理,它就是Repository。
? ? ? ? 无论要用对象执行什么操作,都需要保持一个对它的引用。客户需要一种有效的方式来获取对已存在的领域对象的引用。Repository将某种类型的所有对象表示为一个概念集合(通常是模拟的)。它的行为类似于集合(Collection),只是具有更复杂的查询功能。在添加或删除相应类型的对象时,Repository的后台机制负责将对象添加到数据库中,或从数据中删除对象。这个定义将一组紧密相关的职责集中在一起,这些职责提供了对Aggregate根的整个生命周期的全程访问。
? ? ? ? 客户使用查询方法向Repository请求对象,这些查询方法根据客户所指定的条件来挑选对象。Repository检索被请求的对象,并封装数据库查询和元数据映射机制。Repository可以根据客户所要求的各种条件来挑选对象。它们也可以返回汇总信息。
????????
? ? ? ? ?Repository有诸多优点:
? ? ? ? 所有Repository都为客户提供了根据某种条件来查询对象的方法,最容易构建的是用硬编码的方式来实现一些具有特定参数的查询。
? ? ? ? 在一些需要执行大量查询的项目上,可以构建一个支持更灵活查询的Repository框架。如图
? ? ? ? 基于Specification(规格)的查询是将Repository通用化的好办法。客户可以使用规格来描述它需要什么,而不必关心如何获得结果。在这个过程中,可以创建一个对象来实际执行筛选操作。
? ? ? ? 即使一个Repository的设计采取了灵活的查询方式,也应该允许添加专门的硬编码查询。
? ? ? ? 持久化技术的封装可以使得客户变得十分简单,并且客户与Repository的实现之间完全解耦。但像一般的封装一样,开发人员必须知道在封装背后都发生了什么。在使用Repository时,不同的使用方式或工作方式可能会对性能产生极大的影响。
? ? ? ? 底层技术可能会限制我们的建模选择。同样,开发人员要获得Repository的使用及其查询实现之间的双向反馈。
? ? ? ? 根据所使用的持久化技术和基础设施的不同,Repository的实现也将有很大的变化。理想的实现是向客户隐藏所有内部工作细节,这样不管数据是存储在何处,客户代码均相同。Repository将会委托相应的基础设施服务来完成工作。将存储、检索和查询机制封装起来是Repository实现的最基本特性。
Repository的实现概念在很多情况下都适用,可能需要注意以下几点。
? ? ? ? 通常,项目团队会在基础设施层中添加框架,用来支持Repository的实现。在实现Repository这样的构造之前,需要认真思考所使用的基础设施,特别是架构框架。这些框架可能提供了一些可用来轻松创建Repository的服务,但也可能会妨碍创建Repository的工作。
? ? ? ? 一般来讲,在使用框架时要顺其自然。当框架无法切合时,要想办法在大方向上保持领域驱动设计的基本原理,而一些不符的细节则不必过分苛求。
? ? ? ? Factory负责处理对象生命周期的开始,而Repository帮助管理对象生命周期的中间和结束。
? ? ? ? 这种职责上的明确区分有助于Factory摆脱所有持久化职责。Factory的工作是用数据来实例化一个可能很复杂的对象。如果产品是一个新对象,那么客户将知道在创建完成这后应该把它添加到Repository中,由Repository来封装对象在数据库中的存储。
? ? ? ? 另一种情况促使人们将Factory和Repository结合起来使用,这就是想要实现一种“查找或创建”功能,即客户描述它所需的对象,如果找不到这样的对象,则为客户新创建一个。
? ? ? ? 在以面向对象技术为主的软件系统中,最常用的非对象组件就是关系数据库。数据库不仅仅与对象进行交互,而且它还把构成对象的数据存储为持久化形式。已有相当完善的工具可用来创建和管理对象和关系表之间的映射。依靠映射工具的功能,可以实现一些聚合或对象的组合。但至关重要的是:映射要保持透明,并易于理解——能通过代码审查或阅读映射工具中的条目就能搞明白。
? ? ? ? 大多数情况下,关系数据库是面向对象领域中的持久化存储形式,因此简单的对应关系才是最好的。表中的一行应该包含一个对象,也可能还包含Aggregate中的一些附属项。表中的外键应该转换为对另一个Entity对象的引用。
? ? ? ? 有时为了性能不得不违背这种简单的对应关系,但不应该由此全盘放弃简单映射的原则。
? ? ? ? Ubiquitous Language可能有助于将对象和关系组件联系起来,使之成为单一的模型。对象中的元素的名称和关联应该严格地对应于关系表中相应的项。
? ? ? ? 对象世界中越来越盛行的重构实际上并没有对关系数据库设计造成多大的影响。即当对象的行为快速变化或演变的时候,数据库可能并不需要修改。让模型与数据库之间操持松散的关联是很有吸引力的。
DOMAIN-DRIVERN DESIGN
TACKLING COMPLEXITY IN THE HEART OF SOFTWARE
领域驱动设计
软件核心复杂性应对之道
【美】Eric Evans 著? ?赵俐 盛海艳 刘霞 等 译