第4章 分离领域
在软件中,虽然专门用于解决领域问题的那部分通常只占整个软件系统的很小一部分,但其却出乎意料的重要。要想实现本书的想法,我们需要着眼于模型中的元素并且将它们视为一个系统。绝不能像在夜空中辨认星座一样,被迫从一大堆混杂的对象中将领域对象挑选出来。我们需要将领域对象与系统中的其他功能分离,这样就能够避免将领域概念和其他只与软件技术相关的概念搞混了,也不会在纷繁芜杂的系统中完全迷失了领域。
分离领域的复杂技术早已出现,而且都是我们耳熟能详的,但是它对于能否成功运用领域建模原则起着非常关键的作用,所以我们要从领域驱动的视角对它进行简要的回顾。
4.1 模式:LAYERED ARCHITECTURE
在一个运输应用程序中,要想支持从城市列表中选择运送货物目的地这样的简单用户行为,程序代码必须包括:(1) 在屏幕上绘制一个屏幕组件(widget);(2) 查询数据库,调出所有可能的城市;(3) 解析并验证用户输入;(4) 将所选城市与货物关联;(5) 向数据库提交此次数据修改。上面所有的代码都在同一个程序中,但是只有一小部分代码与运输业务相关。
软件程序需要通过设计和编码来执行许多不同类型的任务。它们接收用户输入,执行业务逻辑,访问数据库,进行网络通信,向用户显示信息,等等。因此程序中的每个功能都可能需要大量的代码来实现。
在面向对象的程序中,常常会在业务对象中直接写入用户界面、数据库访问等支持代码。而一些业务逻辑则会被嵌入到用户界面组件和数据库脚本中。这么做是为了以最简单的方式在短期内完成开发工作。
如果与领域有关的代码分散在大量的其他代码之中,那么查看和分析领域代码就会变得异常困难。对用户界面的简单修改实际上很可能会改变业务逻辑,而要想调整业务规则也很可能需要对用户界面代码、数据库操作代码或者其他的程序元素进行仔细的筛查。这样就不太可能实现一致的、模型驱动的对象了,同时也会给自动化测试带来困难。考虑到程序中各个活动所涉及的大量逻辑和技术,程序本身必须简单明了,否则就会让人无法理解。
要想创建出能够处理复杂任务的程序,需要做到关注点分离——使设计中的每个部分都得到单独的关注。在分离的同时,也需要维持系统内部复杂的交互关系。
软件系统有各种各样的划分方式,但是根据软件行业的经验和惯例,普遍采用LAYERED ARCHITECTURE(分层架构),特别是有几个层基本上已成了标准层。分层这种隐喻被广泛采用,大多数开发人员都对其有着直观的认识。许多文献对LAYERED ARCHITECTURE也进行了充分的讨论,有些是以模式的形式给出的[Buschmann et al.1996,pp.31-51]。LAYERED ARCHITECTURE的基本原则是层中的任何元素都仅依赖于本层的其他元素或其下层的元素。向上的通信必须通过间接的方式进行,这些将在后面讨论。
分层的价值在于每一层都只代表程序中的某一特定方面。这种限制使每个方面的设计都更具内聚性,更容易解释。当然,要分离出内聚设计中最重要的方面,选择恰当的分层方式是至关重要的。在这里,经验和惯例又一次为我们指明了方向。尽管LAYERED ARCHITECTURE的种类繁多,但是大多数成功的架构使用的都是下面这4个概念层的某种变体。
有些项目没有明显划分出用户界面层和应用层,而有些项目则有多个基础设施层。但是将领域层分离出来才是实现MODEL-DRIVEN DESIGN的关键。
因此:
给复杂的应用程序划分层次。在每一层内分别进行设计,使其具有内聚性并且只依赖于它的下层。采用标准的架构模式,只与上层进行松散的耦合。将所有与领域模型相关的代码放在一个层中,并把它与用户界面层、应用层以及基础设施层的代码分开。领域对象应该将重点放在如何表达领域模型上,而不需要考虑自己的显示和存储问题,也无需管理应用任务等内容。这使得模型的含义足够丰富,结构足够清晰,可以捕捉到基本的业务知识,并有效地使用这些知识。
将领域层与基础设施层以及用户界面层分离,可以使每层的设计更加清晰。彼此独立的层更容易维护,因为它们往往以不同的速度发展并且满足不同的需求。层与层的分离也有助于在分布式系统中部署程序,不同的层可以灵活地放在不同服务器或者客户端中,这样可以减少通信开销,并优化程序性能[Fowler 1996]。
示例 为网上银行功能分层
该应用程序能提供维护银行账户的各种功能。其中一个功能就是转账,用户可以输入或者选择两个账户号码,填写要转的金额,然后开始转账。
为了让这个例子更容易实现,这里省略了一些主要的技术特性,特别是安全性方面的一些特性。领域设计也尽量简化。(在现实生活中,银行业务的复杂性只会增加对LAYERED ARCHITECTURE的需求。)此外,这个例子中的基础设施只是为了使程序更简单和清楚一些而已——我并不建议你使用这个设计。简化后的功能所要完成的任务将会按照图4-1来分层。
注意,负责处理基本业务规则的是领域层,而不是应用层——在这个例子中,业务规则就是“每笔贷款必须有与其数目相同的借款”。
这个应用程序没有设定转账请求的发起方。程序中假定包含了用户输入界面,界面中有账户号码和转账金额的输入字段以及一些命令按钮。但是也可以用基于XML的电汇请求来替换,这并不会影响应用层及其下面的各层。这种解耦至关重要,这并不是因为在项目中经常需要用电汇请求来代替用户界面,而是因为关注点的清晰分离可以使每一层的设计更易理解和维护。
事实上,图4-1本身也略微说明了不分离领域层会出现的问题。这张图需要包含从请求到事务控制的所有方面,所以不得不简化领域层来保证整个交互过程简单易懂。如果我们专注于研究独立领域层的设计,就可以构思并绘制出更好地表达领域规则的模型,也许模型中会包含分类账、贷款和借款对象,或者是现金交易对象。
图4-1 对象所执行的任务与其所在层一致,并且与同层其他对象的联系更为紧密
4.1.1 将各层关联起来
到目前为止,我们的讨论主要集中在层次划分以及如何分层才能改进程序各个方面的设计上,特别是集中在领域层上。但是显然,各层之间也需要互相连接。在连接各层的同时不影响分离带来的好处,这是很多模式的目的所在。
各层之间是松散连接的,层与层的依赖关系只能是单向的。上层可以直接使用或操作下层元素,方法是通过调用下层元素的公共接口,保持对下层元素的引用(至少是暂时的),以及采用常规的交互手段。而如果下层元素需要与上层元素进行通信(不只是回应直接查询),则需要采用另一种通信机制,使用架构模式来连接上下层,如回调模式或OBSERVERS模式[Gamma et al.1995]。
最早将用户界面层与应用层和领域层相连的模式是MODEL-VIEW-CONTROLLER(MVC,模型—视图—控制器)框架。它是为Smalltalk语言发明的一种设计模式,创建于20世纪70年代。随后出现的许多用户界面架构都是受到它的启发而产生的。Fowler在[Fowler 2002]中讨论了这种模式以及几个实用的变体。Larman也在MODEL-VIEW SEPARATION模式中探讨了这些问题,他提出的APPLICATION COORDINATOR(应用协调器)是连接应用层的一种方法[Larman 1998]。
还有许多其他连接用户界面层和应用层的方式。对我们而言,只要连接方式能够维持领域层的独立性,保证在设计领域对象时不需要同时考虑可能与其交互的用户界面,那么这些连接方式就都是可用的。
通常,基础设施层不会发起领域层中的操作,它处于领域层“之下”,不包含其所服务的领域中的知识。事实上这种技术能力最常以SERVICE的形式提供。例如,如果一个应用程序需要发送电子邮件,那么一些消息发送的接口可以放在基础设施层中,这样,应用层中的元素就可以请求发送消息了。这种解耦使程序的功能更加丰富。消息发送接口可以连接到电子邮件发送服务、传真发送服务或任何其他可用的服务。但是这种方式最主要的好处是简化了应用层,使其只专注于自己所负责的工作:知道何时该发送消息,而不用操心怎么发送。
应用层和领域层可以调用基础设施层所提供的SERVICE。如果SERVICE的范围选择合理,接口设计完善,那么通过把详细行为封装到服务接口中,调用程序就可以保持与SERVICE的松散连接,并且自身也会很简单。
然而,并不是所有的基础设施都是以可供上层调用的SERVICE的形式出现的。有些技术组件被设计成直接支持其他层的基本功能(如为所有的领域对象提供抽象基类),并且提供关联机制(如MVC及类似框架的实现)。这种“架构框架”对于程序其他部分的设计有着更大的影响。
4.1.2 架构框架
如果基础设施通过接口调用SERVICE的形式来实现,那么如何分层以及如何保持层与层之间的松散连接就是相当显而易见的。但是有些技术问题要求更具侵入性的基础设施。整合了大量基础设施需求的框架通常会要求其他层以某种特定的方式实现,如以框架类的子类形式或者带有结构化的方法签名。(子类在父类的上层似乎是违反常理的,但是要记住哪个类反映了另一个类的更多知识。)最好的架构框架既能解决复杂技术问题,也能让领域开发人员集中精力去表达模型,而不考虑其他问题。然而使用框架很容易为项目制造障碍:要么是设定了太多的假设,减小了领域设计的可选范围;要么是需要实现太多的东西,影响开发进度。
项目中一般都需要某种形式的架构框架(尽管有时项目团队选择了不太合适的框架)。当使用框架时,项目团队应该明确其使用目的:建立一种可以表达领域模型的实现并且用它来解决重要问题。项目团队必须想方设法让框架满足这些需求,即使这意味着抛弃框架中的一些功能。例如,早期的J2EE应用程序通常都会将所有的领域对象实现为“实体bean”。这种实现方式不但影响程序性能,还会减慢开发速度。现在,取而代之的最佳实践是利用J2EE框架来实现大粒度对象,而用普通Java对象来实现大部分的业务逻辑。不妄求万全之策,只要有选择性地运用框架来解决难点问题,就可以避开框架的很多不足之处。明智而审慎地选择框架中最具价值的功能能够减少程序实现和框架之间的耦合,使随后的设计决策更加灵活。更重要的是,现在许多框架的用法都极其复杂,这种简化方式有助于保持业务对象的可读性,使其更富有表达力。
架构框架和其他工具都在不断的发展。新框架将越来越多的应用技术问题变得自动化,或者为其提供了预先设定好的解决方案。如果框架使用得当,那么程序开发人员将可以更加专注于核心业务问题的建模工作,这会大大提高开发效率和程序质量。但与此同时,我们必须要保持克制,不要总是想着要寻找框架,因为精细的框架也可能会束缚住程序开发人员。
4.2 领域层是模型的精髓
现在,大部分软件系统都采用了LAYERED ARCHITECTURE,只是采用的分层方案存在不同而已。许多类型的开发工作都能从分层中受益。然而,领域驱动设计只需要一个特定的层存在即可。
领域模型是一系列概念的集合。“领域层”则是领域模型以及所有与其直接相关的设计元素的表现,它由业务逻辑的设计和实现组成。在MODEL-DRIVEN DESIGN中,领域层的软件构造反映出了模型概念。
如果领域逻辑与程序中的其他关注点混在一起,就不可能实现这种一致性。将领域实现独立出来是领域驱动设计的前提。
4.3 模式:THE SMART UI“反模式”
上面总结了面向对象程序中广泛采用的LAYERED ARCHITECTURE模式。在项目中,人们经常会尝试分离用户界面、应用和领域,但是成功分离的却不多见,因此,分层模式的反面就很值得一谈。
许多软件项目都采用并且应该会继续采用一种不那么复杂的设计方法,我称其为SMART UI (智能用户界面)。但是SMART UI是另一种设计方法,与领域驱动设计方法迥然不同且互不兼容。如果你选择了SMART UI,那么本书中所讲的大部分内容都不适合你。我感兴趣的是那些不应该使用SMART UI的情况,这也是我半开玩笑地称其为“反模式”的原因。本节讨论SMART UI是为了提供一种有益的对比,其将帮助我们认清在本书后面章节中的哪些情况下需要选择相对而言更难于实现的领域驱动设计模式。
假设一个项目只需要提供简单的功能,以数据输入和显示为主,涉及业务规则很少。项目团队也没有高级对象建模师。
如果一个经验并不丰富的项目团队要完成一个简单的项目,却决定使用MODEL-DRIVEN DESIGN以及LAYERED ARCHITECTURE,那么这个项目组将会经历一个艰难的学习过程。团队成员不得不去掌握复杂的新技术,艰难地学习对象建模。(即使有这本书的帮助,这也依然是一个具有挑战性的任务!)对基础设施和各层的管理工作使得原本简单的任务却要花费很长的时间来完成。简单项目的开发周期较短,期望值也不是很高。所以,早在项目团队完成任务之前,该项目就会被取消,更谈不上去论证有关这种方法的许多种令人激动的可行性了。
即使项目有更充裕的时间,如果没有专家的帮助,团队成员也不太可能掌握这些技术。最后,假如他们确实能够克服这些困难,恐怕也只会开发出一套简单的系统。因为这个项目本来就不需要丰富的功能。
经验丰富的团队则不会做出这样的选择。身经百战的开发人员能够更容易学习,进而减少管理各层所需要的时间。领域驱动设计只有应用在大型项目上才能产生最大的收益,而这也确实需要高超的技巧。不是所有的项目都是大型项目;也不是所有的项目团队都能掌握那些技巧。
因此,当情况需要时:
在用户界面中实现所有的业务逻辑。将应用程序分成小的功能模块,分别将它们实现成用户界面,并在其中嵌入业务规则。用关系数据库作为共享的数据存储库。使用自动化程度最高的用户界面创建工具和可用的可视化编程工具。
这真是异端邪说啊!福音(所有地方,包括本书其他地方,都在倡导的原则)说应该是领域和UI彼此独立。事实上,不将领域和用户界面分离,则很难运用本书后面所要讨论的方法,因此在领域驱动设计中,可以将SMART UI看作是“反模式”。然而在其他情况下,它也是完全可行的。其实,SMART UI也有其自身的优势,在某些情况下它能发挥最佳的作用——这也是它如此普及的原因之一。在这里介绍SMART UI能够帮助我们理解为什么需要将应用程序与领域分离,而且更重要的是,还能让我们知道什么时候不需要这样做。
优点
效率高,能在短时间内实现简单的应用程序。
能力较差的开发人员可以几乎不经过培训就采用它。
甚至可以克服需求分析上的不足,只要把原型发布给用户,然后根据用户反馈快速修改软件产品即可。
程序之间彼此独立,这样,可以相对准确地安排小模块交付的日期。额外扩展简单的功能也很容易。
可以很顺利地使用关系数据库,能够提供数据级的整合。
可以使用第四代语言工具。
移交应用程序后,维护程序员可以迅速重写他们不明白的代码段,因为修改代码只会影响到代码所在的用户界面。
缺点
不通过数据库很难集成应用模块。
没有对行为的重用,也没有对业务问题的抽象。每当操作用到业务规则时,都必须重复这些规则。
快速的原型建立和迭代很快会达到其极限,因为抽象的缺乏限制了重构的选择。
复杂的功能很快会让你无所适从,所以程序的扩展只能是增加简单的应用模块,没有很好的办法来实现更丰富的功能。
如果项目团队有意识地应用这个模式,那么就可以避免其他方法所需要的大量开销。项目团队常犯的错误是采用了一种复杂的设计方法,却无法保证项目从头到尾始终使用它。另一种常见的也是代价高昂的错误则是为项目构建一种复杂的基础设施以及使用工业级的工具,而这样的项目根本不需要它们。
大部分灵活的编程语言(如Java)对于小型应用程序来说是大材小用了,并且使用它们的开销很大。第四代语言风格的工具就足以满足这种需要了。
记住,在项目中使用智能用户界面后,除非重写全部的应用模块,否则不能改用其他的设计方法。使用诸如Java这类的通用语言并不能让你在随后的开发过程中放弃使用SMART UI,因此,如果你选择了这条路线,就应该采用与之匹配的开发工具。不要浪费时间去同时采用多种选择。只使用灵活的编程语言并不一定会创建出灵活的软件系统,反而有可能会开发出一个维护代价十分高昂的系统。
同样道理,采用MODEL-DRIVEN DESIGN的项目团队从项目初始就应该采用模型驱动的设计。当然,即使是经验丰富的项目团队在开发大型软件系统时,也不得不从简单的功能着手,然后在整个开发过程中使用连续的迭代开发。但是最初试探性的工作也应该是由模型驱动的,而且要分离出独立的领域层,否则很有可能项目进行到最后就变成智能用户界面模式了。
这里讨论SMART UI只是为了让你认清为什么以及何时需要采用诸如LAYERED ARCHITECT-URE这样的模式来分离出领域层。
除SMART UI和LAYERED ARCHITECTURE之外,还有一些其他的设计方案。例如,Fowler在 [Fowler 2002]中描述了TRANSACTION SCRIDT(事务脚本),它将用户界面从应用中分离出来,但却并不提供对象模型。总而言之:如果一个架构能够把那些与领域相关的代码隔离出来,得到一个内聚的领域设计,同时又使领域与系统其他部分保持松散耦合,那么这种架构也许可以支持领域驱动设计。
其他的开发风格也有各自的用武之地,但是必须要考虑到各种对于复杂度和灵活性的限制。在某些条件下,将领域设计与其他部分混在一起会产生灾难性的后果。如果你要开发复杂应用软件并且决定使用MODEL-DRIVEN DESIGN,那么做好准备,咬紧牙关,雇用必不可少的专家,并且不要使用SMART UI。
4.4 其他分离方式
遗憾的是,除了基础设施和用户界面之外,还有一些其他的因素也会破坏你精心设计的领域模型。你必须要考虑那些没有完全集成到模型中的领域元素。你不得不与同一领域中使用不同模型的其他开发团队合作。还有其他的因素会让你的模型结构不再清晰,并且影响模型的使用效率。在第14章中,会讨论这方面的问题,同时会介绍其他模式,如BOUNDED CONTEXT和ANTICORRUPTION LAYER。非常复杂的领域模型本身是难以使用的,所以,第15章将会说明如何在领域层内进行进一步区分,以便从次要细节中突显出领域的核心概念。
但是,这些都是后话。接下来,我们将会讨论一些具体细节,即如何让一个有效的领域模型和一个富有表达力的实现同时演进。毕竟,把领域隔离出来的最大好处就是可以真正专注于领域设计,而不用考虑其他的方面。
