本文共 6382 字,大约阅读时间需要 21 分钟。
目录
当创建一个对象或创建整个AGGREGATE时,如果创建工作很复杂,或者暴露了过多的内部结构,则可以使用FACTORY进行封装。
对象的功能主要体现在其复杂的内部配置以及关联方面。我们应该一直对对象进行提炼,直到所有与其意义或在交互中的角色无关的内容被完全剔除为止。一个对象在它的生命周期中要承担大量职责。如果再让复杂对象负责自身的创建,那么职责过载将会导致问题。
汽车发动机是一种复杂的机械装置,它由数十个零件共同协作来履行发动机的职责——使轴转动。我们可以试着设计一种发动机组,让它自己抓取一组活塞并塞到汽缸中,火花塞也可以自己找到插孔并把自己拧进去。但这样组装的复杂机器可能没有我们常见的发动机那样可靠或高效。相反,我们用其他东西来装配发动机。或许是机械师,或者是工业机器人。无论是机器人还是人,实际上都比二者要装配的发动机复杂。装配零件的工作与使轴旋转的工作完全无关。只是在生产汽车时才需要装配工,我们驾驶时并不需要机器人或机械师。由于汽车的装配和驾驶永远不会同时发生,因此将这两种功能合并到同一个机制中是毫无价值的。同理,装配复杂的复合对象的工作也最好与对象要执行的工作分开。但将职责转交给另一个相关方——应用程序中的客户(client)对象——会产生更严重的问题。客户知道需要完成什么工作,并依靠领域对象来执行必要的计算。如果指望客户来装配它需要的领域对象,那么它必须要了解一些对象的内部结构。为了确保所有应用于领域对象各部分关系的固定规则得到满足,客户必须知道对象的一些规则。甚至调用构造函数也会使客户与所要构建的对象的具体类产生耦合。结果是,对领域对象实现所做的任何修改都要求客户做出相应修改,这使得重构变得更加困难。
当客户负责创建对象时,它会牵涉不必要的复杂性,并将其职责搞得模糊不清。这违背了领域对象及所创建的AGGREGATE的封装要求。更严重的是,如果客户是应用层的一部分,那么职责就会从领域层泄漏到应用层中。应用层与实现细节之间的这种耦合使得领域层抽象的大部分优势荡然无存,而且导致后续更改的代价变得更加高昂。
对象的创建本身可以是一个主要操作,但被创建的对象并不适合承担复杂的装配操作。将这些职责混在一起可能产生难以理解的拙劣设计。让客户直接负责创建对象又会使客户的设计陷入混乱,并且破坏被装配对象或AGGREGATE的封装,而且导致客户与被创建对象的实现之间产生过于紧密的耦合。
复杂的对象创建是领域层的职责,然而这项任务并不属于那些用于表示模型的对象。在有些情况下,对象的创建和装配对应于领域中的重要事件,如“开立银行账户”。但一般情况下,对象的创建和装配在领域中并没有什么意义,它们只不过是实现的一种需要。为了解决这一问题,我们必须在领域设计中增加一种新的构造,它不是ENTITY、VALUE OBJECT,也不是SERVICE。这与前一章的论述相违背,因此把它解释清楚很重要。我们正在向设计中添加一些新元素,但它们不对应于模型中的任何事物,而确实又承担领域层的部分职责。每种面向对象的语言都提供了一种创建对象的机制(例如,Java和C++中的构造函数,Smalltalk中创建实例的类方法),但我们仍然需要一种更加抽象且不与其他对象发生耦合的构造机制。这就是FACTORY,它是一种负责创建其他对象的程序元素。如下图所示。
正如对象的接口应该封装对象的实现一样(从而使客户无需知道对象的工作机理就可以使用对象的功能),FACTORY封装了创建复杂对象或AGGREGATE所需的知识。它提供了反映客户目标的接口,以及被创建对象的抽象视图。
因此:
应该将创建复杂对象的实例和AGGREGATE的职责转移给单独的对象,这个对象本身可能没有承担领域模型中的职责,但它仍是领域设计的一部分。提供一个封装所有复杂装配操作的接口,而且这个接口不需要客户引用要被实例化的对象的具体类。在创建AGGREGATE时要把它作为一个整体,并确保它满足固定规则。
FACTORY有很多种设计方式。中详尽论述了几种特定目的的创建模式,包括FACTORY METHOD(工厂方法)、ABSTRACT FACTORY(抽象工厂)和BUILDER(构建器)。该书主要研究了适用于最复杂的对象构造问题的模式。本书的重点并不是深入讨论FACTORY的设计问题,而是要表明FACTORY的重要地位——它是领域设计的重要组件。正确使用FACTORY有助于保证MODEL-DRIVEN DESIGN沿正确的轨道前进。
任何好的工厂都需满足以下两个基本需求
一般来说,FACTORY的作用是隐藏创建对象的细节,而且我们把FACTORY用在那些需要隐藏细节的地方。这些决定通常与AGGREGATE有关。
例如,如果需要向一个已存在的AGGREGATE添加元素,可以在AGGREGATE的根上创建一个FACTORY METHOD。这样就可以把AGGREGATE的内部实现细节隐藏起来,使任何外部客户看不到这些细节,同时使根负责确保AGGREGATE在添加元素时的完整性,如下图所示。
另一个示例是在一个对象上使用FACTORY METHOD,这个对象与生成另一个对象密切相关,但它并不拥有所生成的对象。当一个对象的创建主要使用另一个对象的数据(或许还有规则)时,则可以在后者的对象上创建一个FACTORY METHOD,这样就不必将后者的信息提取到其他地方来创建前者。这样做还有利于表达前者与后者之间的关系。
在下图中,Trade Order不属于Brokerage Account所在的AGGREGATE,因为它从一开始就与交易执行应用程序进行交互,所以把它放在Brokerage Account中只会碍事。尽管如此,让Brokerage Account负责控制Trade Order的创建却是很自然的事情。Brokerage Account含有会被嵌入到Trade Order中的信息(从自己的标识开始),而且它还包含与交易相关的规则——这些规则控制了哪些交易是允许的。隐藏Trade Order的实现细节还会带来一些其他好处。例如,我们可以将它重构为一个层次结构,分别为Buy Order和Sell Order创建一些子类。FACTORY可以避免客户与具体类之间产生耦合。
我曾经在很多代码中看到所有实例都是通过直接调用类构造函数来创建的,或者是使用编程语言的最基本的实例创建方式。FACTORY的引入提供了巨大的优势,而这种优势往往并未得到充分利用。但是,在有些情况下直接使用构造函数确实是最佳选择。FACTORY实际上会使那些不具有多态性的简单对象复杂化。
在以下情况下最好使用简单的、公共的构造函数。
不要在构造函数中调用其他类的构造函数。构造函数应该保持绝对简单。复杂的装配,特别是AGGREGATE,需要使用FACTORY。使用FACTORY METHOD的门槛并不高。Java类库提供了一些有趣的例子。所有集合都实现了接口,接口使得客户与具体实现之间不产生耦合。然而,它们都是通过直接调用构造函数创建的。但是,集合类本来是可以使用FACTORY来封装集合的层次结构的。而且,客户也可以使用FACTORY的方法来请求所需的特性,然后由FACTORY来选择适当的类来实例化。这样一来,创建集合的代码就会有更强的表达力,而且新增集合类时不会破坏现有的Java程序。
但在某些场合下使用具体的构造函数更为合适。首先,在很多应用程序中,实现方式的选择对性能的影响是非常敏感的,因此应用程序需要控制选择哪种实现(尽管如此,真正智能的FACTORY仍然可以满足这些因素的要求)。不管怎样,集合类的数量并不多,因此选择并不复杂。虽然没有使用FACTORY,但抽象集合类型仍然具有一定价值,原因就在于它们的使用模式。
集合通常都是在一个地方创建,而在其他地方使用。这意味着最终使用集合(添加、删除和检索其内容)的客户仍可以与接口进行对话,从而不与实现发生耦合。集合类的选择通常由拥有该集合的对象来决定,或是由该对象的FACTORY来决定。0当设计FACTORY的方法签名时,无论是独立的FACTORY还是FACTORY METHOD,都要记住以下两点。
每个操作都必须是原子的。我们必须在与FACTORY的一次交互中把创建对象所需的所有信息传递给FACTORY。同时必须确定当创建失败时将执行什么操作,比如某些固定规则没有被满足。可以抛出一个异常或仅仅返回null。为了保持一致,可以考虑采用编码标准来处理所有FACTORY的失败。
Factory将与其参数发生耦合。如果在选择输入参数时不小心,可能会产生错综复杂的依赖关系。耦合程度取决于对参数(argument)的处理。如果只是简单地将参数插入到要构建的对象中,则依赖度是适中的。如果从参数中选出一部分在构造对象时使用,耦合将更紧密。
最安全的参数是那些来自较低设计层的参数。即使在同一层中,也有一种自然的分层倾向,其中更基本的对象被更高层的对象使用(第10章将从不同方面讨论这样的分层,第16章也会论述这个问题)。
另一个好的参数选择是模型中与被构建对象密切相关的对象,这样不会增加新的依赖。在前面的Purchase Order Item示例中,FACTORY METHOD将Catalog Part作为一个参数,它是Item的一个重要的关联。这在Purchase Order类和Part之间增加了直接依赖。但这3个对象组成了一个关系密切的概念小组。不管怎样,Purchase Order的AGGREGATE已经引用了Part。因此将控制权交给AGGREGATE根,并封装AGGREGATE的内部结构是一个不错的折中选择。使用抽象类型的参数,而不是它们的具体类。FACTORY与被构建对象的具体类发生耦合,而无需与具体的参数发生耦合。
FACTORY负责确保它所创建的对象或AGGREGATE满足所有固定规则,然而在把应用于一个对象的规则移到该对象外部之前应三思。FACTORY可以将固定规则的检查工作委派给被创建对象,而且这通常是最佳选择。
但FACTORY与被创建对象之间存在一种特殊关系。FACTORY已经知道被创建对象的内部结构,而且创建FACTORY的目的与被创建对象的实现有着密切的联系。在某些情况下,把固定规则的相关逻辑放到FACTORY中是有好处的,这样可以让被创建对象的职责更明晰。对于AGGREGATE规则来说尤其如此(这些规则会约束很多对象)。但固定规则的相关逻辑却特别不适合放到那些与其他领域对象关联的FACTORY METHOD中。
虽然原则上在每个操作结束时都应该应用固定规则,但通常对象所允许的转换可能永远也不会用到这些规则。可能ENTITY标识属性的赋值需要满足一条固定规则。但该标识在创建后可能一直保持不变。VALUE OBJECT则是完全不变的。如果逻辑在对象的有效生命周期内永远也不被用到,那么对象就没有必要携带这个逻辑。在这种情况下,FACTORY是放置固定规则的合适地方,这样可以使FACTORY创建出的对象更简单。
ENTITY FACTORY与VALUE OBJECT FACTORY有两个方面的不同。由于VALUE OBJECT是不可变的,因此,FACTORY所生成的对象就是最终形式。因此FACTORY操作必须得到被创建对象的完整描述。而ENTITY FACTORY则只需具有构造有效AGGREGATE所需的那些属性。对于固定规则不关心的细节,可以之后再添加。
我们来看一下为ENTITY分配标识时将涉及的问题(VALUE OBJECT不会涉及这些问题)。正如第5章所指出的那样,既可以由程序自动分配一个标识符,也可以通过外部(通常是用户)提供一个标识符。如果客户的标识是通过电话号码跟踪的,那么该电话号码必须作为参数被显式地传递给FACTORY。当由程序分配标识符时,FACTORY是控制它的理想场所。尽管唯一跟踪ID实际上是由数据库“序列”或其他基础设施机制生成的,但FACTORY知道需要什么样的标识,以及将标识放到何处。
到目前为止,FACTORY只是发挥了它在对象生命周期开始时的作用。到了某一时刻,大部分对象都要存储在数据库中或通过网络传输,而在当前的数据库技术中,几乎没有哪种技术能够保持对象的内容特征。大多数传输方法都要将对象转换为平面数据才能传输,这使得对象只能以非常有限的形式出现。因此,检索操作潜在地需要一个复杂的过程将各个部分重新装配成一个可用的对象。
用于重建对象的FACTORY与用于创建对象的FACTORY很类似,主要有以下两点不同。
下两张图显示了两种重建。当从数据库中重建对象时,对象映射技术就可以提供部分或全部所需服务,这是非常便利的。当从其他介质重建对象时,如果出现复杂情况,FACTORY是个很好的选择。
总之,必须把创建实例的访问点标识出来,并显式地定义它们的范围。它们可能只是构造函数,但通常需要有一种更抽象或更复杂的实例创建机制。为了满足这种需求,需要在设计中引入新的构造——FACTORY。FACTORY通常不表示模型的任何部分,但它们是领域设计的一部分,能够使对象更明确地表示出模型。
FACTORY封装了对象创建和重建时的生命周期转换。还有一种转换大大增加了领域设计的技术复杂性,这是对象与存储之间的互相转换。这种转换由另一种领域设计构造来处理,它就是REPOSITORY。
转载地址:http://hzovi.baihongyu.com/