本章涉及的是需要若干个团队通力配合的大型项目。
当有多个团队时,我们必须面对一系列不同的挑战,开发这样一下项目的认为需要不同的管理和协作。
企业级项目通常都是大型项目需要使用许多的技术和资源。
这样的项目的设计将仍然基于一个领域模型展开,并且我们需要采用适当的measure 来确保项目成功。
当多个团队开发一个项目时,代码开发是并行的,每个团队都会被指派模型的一个特定部分。
那些部分不是独立的,多少都有些关联性。
它们都是从一个大的模型出发,然后实现其中的一部分。我们可以这样说,其中的一个团队创建了一个模块,然后提供给其他的团队使用。
某个团队的开发人员开始在自己的模块中使用这个模块,但发现还缺少一些功能,于是他增加了这个功能并放到代码库里面,以便所有的人都能使用。
但是他也许没有意识到,这其实是对模型的一个变更,这个变更很有可能破坏系统的功能。
这种情况很容易发生,因为没有人会花时间去完全理解整个模型。
每个人都知道自己的后院里有什么,但对其他地方则并不是非常了解。
好的开始未必就是成功的一半。比较常见的情况是,很多开始良好的模型和流程到最后都一塌糊涂。
模型的首要需求是一致性,条款统一和没有矛盾。模型的内部一致被称为“统一”。
一个企业项目应该有一个模型,涵盖企业的整个领域,没有矛盾和重叠的条款。
统一的企业模型是不容易实现的理想状态,有时甚至都不值得尝试。
这些项目需要许多团队的通力协作。在开发流程中,团队需要高度的独立性,因为他们没有时间去经常开会和讨论设计。
要调和这些团队是很有挑战性的。也许他们属于不同的部门,有着不相干的管理。
当模型的设计开始部分独立时,我们就开始面临失去模型完整性的可能性。
试图为整个企业项目维持一个大的统一模型以获得模型完整性的做法,将不会有什么作为。
解决方案不是那么明显的,因为它远超乎我们所已经学习到的知识。
不是试图保持一个迟早要四分五裂的大模型,我们应该做的是有意识地将大模型分解成数个较小的部分。
只要遵守相绑定的契约,整合得好的小模型会越来越有独立性。
每个模型都应该有一个清晰的边界,模型之间的关系也应该被精确地定义。
我们会提供一整套技术来保持模型的完整性。
下面的章节阐述了这些技术,以及它们之间的关系。
界定的上下文
每一个模型都有一个上下文。在我们处理一个独立的模型时,上下文是固定的。我们不需要去定义它。
当我们创建一个假定要和其他软件,比如一个遗产应用,交互的应用时,很明显新的应用有自己的模型和上下文,它们和遗产模型和它的上下文相分离。
它们不能被合并、混合或者模糊定义。所以当我们开发大的企业应用时,需要为每一个我们创建的模型定义上下文。
在任何大型项目中都存在多个模型。如果基于不同模型的代码被合并,软件就变得不稳定、不可靠而且很难理解。
团队之间的沟通也会不通畅。在哪些上下文里不应该有模型通常都不是非常明确的。
如何把一个大的模型分解成小的部分没有什么具体的公式。
尽量把那些相关联的以及能形成一个自然概念的因素放在一个模型里。
模型应该足够小,以便能分给一个团队去实现。团队协作和沟通如果更畅通,会有助于开发人员共同完成一个模型。
模型的上下文是一个条件集合,用这些条件可以确保应用在模型里的条款都有一个明确的含义。
这儿主要的思想是定义模型的范围,画出它的上下文的边界,然后尽最大可能保持模型的一致性。
要在模型涵盖整个企业项目时保持它的纯洁是很困难的,但是在它被限定到一个特定区域时就相对容易很多。
要在应用到模型的地方明确定义上下文。在团队组织里明确定义边界,在应用的具体部分明确定义用法,以及像代码库和数据库Schema 的物理显示。
保持模型在这些边界里的严格一致,不要因外界因素的干扰而有异动。
被界定的上下文不是模型。界定的上下文提供有模型参与的逻辑框架。
模块被用来组织模型的要素,因此界定的上下文包含模块。
当不同的团队不得不共同工作于一个模型时,我们必须小心不要踩到别人的脚(译者注:意思为各司其职,不越界)。
要时刻意识到任何针对模型的变化都有可能破坏现有的功能。
当使用多个模型时,每个人可以自由使用自己的那一部分。
我们都知道自己模型的局限,都恪守在这些边界里。我们需要确保模型的纯洁、一致和完整。
每个模型应能使重构尽可能容易,而不会影响到其他的模型。
而且为了达到纯洁的最大化,设计还要可以被精简和提炼。
有多个模型时总是会付出些代价。我们需要定义不同模型间的边界和关系。
这需要额外的工作和设计付出,以及可能出现的不同模型间的翻译。
我们不能在不同模型间传递任何对象,也不能在没有边界的情况下自由地激活行为。
但这并不是一个非常困难的任务,而且带来的好处证明克服这些困难是值得的。
比如,我们要创建一个用来在互联网上卖东西的在线应用。
这个应用允许客户注册,然后我们收集他们的个人数据,包括信用卡号码。数据保存在一个关系型数据库里面。客户被允许登录,通过浏览网站寻找商品,下单等。
不论在什么时候下单,应用都需要触发一个事件,因为有人要邮寄需求的货物。
我们还想做一个用于创建报表的报表界面,所以还应该能监视已有的货物数量、哪些是客户感兴趣购买的、哪些是不受欢迎的等信息的状态。
开始的时候,我们用一个模型涵盖整个在线应用的领域。很自然地就会这样做,因为毕竟我们被要求创建一个大的应用。
但是仔细考虑手头的任务之后,我们发现这个E商店应用其实和报表并不是那么相关联。
它们有不同的考虑,在不同的概念下操作,甚至需要用到不同的技术。
唯一共通的地方是客户和商品的数据都存储在数据库里,两个应用都访问到它们。
推荐的做法是为每一个领域创建一个独立的模型,一个为在线交易,一个为报表。
它们两个可以在互不干涉的情况下继续完善,甚至可以变成独立的应用。
也许报表应用会用到在线交易( ecommerce)应用应该存储在数据库里的一些特定数据,但多数情况下它们彼此独立发展。
还需要有个通讯系统来通知仓库管理人员订单信息,这样他们就可以邮寄被购买的货物。
邮寄人员也会用到一个可以提供给他们详细的关于所购买的物品条目、数量、客户地址以及邮寄需求等信息的应用。
不需要使在线商店(e-shop)模型覆盖两个活动领域。
对在线商店而言,用异步通讯的方式给仓库发送包含购买信息的Value对象要相对简单的多。
这样就明确地有两个独立开发的模型,我们只需要保证它们之间的接口工作良好就可以了。
持续集成
一旦界定的上下文被定义,我们就必须保持它的完整性。但多人工作于同一个界定的上下文时,模型很容易被分解。
团队越大,问题越大,不过通常只有三四个人会遇到严重的问题。
但是,系统被破坏成更小的上下文后,基本上也就失去了完整性和一致性的价值。
就是一个团队工作于一个界定的上下文,也有犯错误的空间。
在团队内部我们需要充分的沟通,以确保每个人都能理解模型中每个部分所扮演的角色。
如果一个人不理解对象间的关系,他就可能会以和原意完全相反的方式修改代码。
如果我们不能百分之百地专注于模型的纯洁性,就会很容易犯这种错误。
团队的某个成员可能会在不知道已经有自己所需代码的情况下增加重复代码,或者担心破坏现有的功能而不改变已有的代码选择重复增加。
模型不是一开始就被完全定义。先被创建,然后基于对领域新的发现和来自开发过程的反馈等再继续完善。
这意味着新的概念会进入模型,新的部分也会被增加到代码中。
所有的这些需求都会被集成进一个统一的模型,进而用代码实现之。
这也就是为什么持续集成在界定的上下文中如此必要的原因。
我们需要这样一个集成的过程,以确保所有新增的部分和模型原有的部分配合得很好,在代码中也被正确地实现。
我们需要有个过程来合并代码。合并得越早越好。对小的独立团队,推荐每日合并。
我们还需要适当地采用构建流程。合并的代码需要自动地被构建,以被测试。
另外一个必须的邀请是执行自动测试。
如果团队有测试工具,并创建了测试集,那么测试就可以运行在每个构建上,任何错误都可以被检查出来。
而这时也可以较容易地修改代码以修正报告的错误,因为它们被发现的很早,合并、构建、和测试流程才刚开始。
持续集成是基于模型中概念的集成,然后再通过测试实现。
任何不完整的模型在实现过程中都会被检测出来。
持续集成应用于界定的上下文,不会被用来处理相邻上下文之间的关系。
上下文映射
一个企业应用有多个模型,每个模型有自己的界定的上下文。
建议用上下文作为团队组织的基础。
在同一个团队里的人们能更容易地沟通,也能很好地将模型集成和实现。但是每个团队都工作于自己的模型,所以最好让每个人都能了解所有的模型。
上下文映射(Context Map)是指抽象出不同界定上下文和它们之间关系的文档,它可以是像下面所说的一个试图(Diagram),也可以是其他任何写就的文档。
详细的层次各有不同。它的重要之处是让每个在项目中工作的人都能够得到并理解它。
只有独立的统一模型还不够。它们还要被集成,因为每个模型的功能都只是整个系统的一部分。
在最后,单个的部分要被组织在一起,整个的系统必须能正确工作。
如果上下文定义的不清晰,很有可能彼此之间互相覆盖。
如果上下文之间的关系没有被抽象出来,在系统被集成的时候它们就有可能不能工作。
每个界定的上下文都应该有一个作为Ubiquitous Language 一部分的名字。这在团队之间沟通整个系统的时候非常有用。
每个人也应该知道每个上下文的界限以及在上下文和代码之间的映射等。
一个通常的做法是先定义上下文,然后为每个上下文创建模型,再用一个约定的名称指明每个模型所属的上下文。
在接下来的章节中,我们要讨论不同上下文之间的交互。我们会列举很多可用来创建上下文映射的模式,被创建的上下文有清晰的角色和被指明的关系。
在上下文之间,共享内核(Shared Kernel)和客户-供应商(Customer-Supplier)是具有高级交互的模式。
隔离通道(Separate Way)是在我们想让上下文高度独立和分开运行时要用到的模式。
还有两个模式处理系统和继承系统或者外部系统之间的交互,它们是开放主机服务(Open Host Service)和防崩溃层(Anticorruption Layer)。
共享内核
当缺少功能集成时,持续集成可能就遥不可及了。尤其是在团队不具备相关的技术或者行政组织来维护持续集成,或者是某个团队又大又笨拙的时候。
所以这些界定的上下文可能要被良好地定义和multiple teams formed。
协同工作于有紧密关系的应用程序上的不协调团队有时会进展很快,但他们所做的有可能很难整合。
他们在转换层和技巧花样上花费了过多的时间,而没有在最重要的持续集成上下功夫,做了许多重复劳动也没有体味到通用语言带来的好处。
因此,需要指派两个团队同意共享的领域模型子集。当然除了模型的子集部分,还要包括代码自己或者和模型相关联的数据库设计子集。
这个明确被共享的东西有特别的状态,没有团队之间的沟通不能做修改。
要经常整合功能系统,但是可以不用像在团队里进行持续集成那么频繁。
在集成的时候,在两个团队里都要运行测试。
共享内核的目的是减少重复,但是仍保持两个独立的上下文。对于共享内核的开发需要多加小心。两个开发团队都有可能修改内核代码,还要必须整合所做的修改。
如果团队用的是内核代码的副本,那么要尽可能早地融合(Merge)代码,至少每周一次。
还应该使用测试工具,这样每一个针对内核的修改都能快速地被测试。
内核的任何改变都应该通知另一个团队,团队之间密切沟通,使大家都能了解最新的功能。
客户-供应商
我们经常会遇到两个子系统之间关系特殊的时候:一个严重依赖另一个。
两个子系统所在的上下文是不同的,而且一个系统的处理结果被输入到另外一个。
它们没有共享的内核,因为从概念上理解也许不可以有这样一个内核,或者对两个子系统而言要共享代码在技术上也不可能实现。
让我们回到先前的例子。我们曾讨论了一个关于在线商店应用的模型,包括报表和通讯两部分内容。
我们已经解释说最好要为所有的那些上下文创建各自分开的模型,因为只有一个模型时会在开发过程中遇到瓶颈和资源的争夺。
假设我们同意有分开的模型,那么在Web 商店字系统和报表系统间的关系是什么样子的呢?共享内核看上去不是好的选择。
子系统很可能会用不同的技术被实现。一个是纯浏览器体验,而另一个可能是丰富的GUI 应用。
尽管如果报表应用是用Web 接口实现,各自模型的注意概念也是不同的。
也许会有越界的情况,但还不足以应用共享内核。所以我们选择走不同的道路。
另外,E商店子系统并不全依赖报表系统。E商店应用的用户是Web 客户,是那些浏览商品并下单的人。
所有的客户、商品和订单数据被放在一个数据库里。就是这样。
E商店应用不会真的关心各自的数据发生了什么。
而同时,报表应用非常关心和需要由E商店应用保存的数据。它还需要一些额外的信息以执行它提供的报表服务。
客户可能在购物篮里放了一些商品,但在结账的时候又去掉了。
某个客户访问的链接可能多于其他人等。这样的信息对E商店应用没有什么意义,但是对报表应用却意义非凡。
由此,供应商子系统不得不实现一些客户子系统会用到的规范。这是联系两个子系统的纽带。
另外一个和所用到的数据库相关联的需求是它的Schema。两个应用将使用同一个数据库。
如果E商店应用是唯一访问数据库的应用,那么数据库Schema可以在任何时间被改变以反应它的需要。
但是报表子系统也需要访问数据库,所以它需要一些Schema的稳定性。
在开发过程中,数据库Schema一点也不能改变的情况是不可想像的。
对E商店应用这不代表是个问题,但对报表应用这肯定是一个问题。
这两个团队需要沟通,可能还需要在同一个数据库下工作,然后决定什么时候执行变更。
对报表子系统来说这会是一个限制,因为团队会倾向于随着开发的进展快速地变更和进展,而不是在E商店应用上等待。
如果E商店应用团队有否决权,他们也许会对要在数据库上做的变更强制加上限定,从而伤害到报表团队的行为。
如果E商店团队能独立行动,他们就会迟早破坏约定,然后做一些报表团队还没有准备好的变更。
但这个模式在团队处于统一管理的情况下有效,它会使决策过程变得容易,也能够产生默契。
当我们面对这样一个场景时,应该就开始“演出”了。
报表团队应该扮演客户角色,而E商店团队应该扮演供应商角色。
两个团队应该定期碰面或者提邀请,像一个客户对待他的供应商那样交谈。
客户团队应该代表系统的需求,而供应商团队据此设置计划。
当客户团队所有的需求都被激发出来后,供应商团队就可以决定实现它们的时间表。
如果认为一些需求非常重要,那么应该先实现它们,延迟其他的需求。
客户团队还需要输入和能被供应商团队分享的知识。这个过程Flows one way,但是有时是必要的。
两个子系统之间的接口需要预先明确定义。
另外还要创建一个统一的测试集,在任何接口需求被提出的时候用于测试。
供应商团队可以在他们的设计上大胆地工作,因为接口测试集的保护网会在任何有问题的时候报警。
在两个团队之间确定一个明显的客户/供应商关系。在计划场景里,让客户团队扮演和供应商团队打交道的客户角色。
为客户需求做充分的解释和任务规划,让每个人理解相关的约定和日程表。
联合开发可以验证期望(Expected)接口的自动化验收测试。将这些测试增加到供应商团队的测试集里,作为它的持续集成的一部分运行。
这个测试能使供应商团队放心地做修改,而不用担心影响客户团队的应用。
顺从者
在两个团队都有兴趣合作时,客户-供应商关系是可行的。
客户非常依赖于供应商,但供应商不是。
如果有管理保证合作的执行,供应商会给于客户需要的关注,并聆听客户的要求。
如果管理没有清晰地界定在两个团队之间需要完成什么,或者管理很差,或者就没有管理,供应商慢慢地会越来越关心它的模型和设计,而也越来越疏于帮助客户。
毕竟他们有自己的工作完成底线。即使他们是好人,愿意帮助其他团队,时间的压力却不允许他们这么做,客户团队深受其害。
在团队属于不同公司的情况下,这样的事情也会发生。交流是困难的,供应商的公司也许没兴趣在关系沟通上投资太多。
他们要么提供少许帮助,或者直接拒绝合作。结果是客户团队孤立无援,只能尽自己的努力摸索模型和设计。
当两个开发团队有客户-供应商关系,而且供应商团队没有动力为客户团队的需要提供帮助时,客户团队是无助的。
利他精神也许会使供应商开发者做出许诺,但是他们很有可能完不成。美丽的许诺导致客户团队根据那些从来都不会存在的功能制定计划。
客户的项目会被一直耽搁,知道团队最终学会如何协同工作。符合客户团队需要的接口不在卡片里。
客户团队没有多少选择。最常见的现象是将它从供应商那儿分割出来,自己完全独立。在后面的“分割方法”模式中我们在对它做详细介绍。
有时供应商子系统提供的好处不值得所付出的努力。创建一个分割的模型,以及在不考虑供应商模型的情况下做设计也许更简单些。但并不总是管用。
有时在供应商模型里会有些数据,这时不得不维持一个连接。
但是因为供应商团队不帮助客户团队,所以后者也不得不采取一些措施保护自己,以防止前者对模型所做更改带来的影响。
他们需要实现连接两边上下文的转换层。也有可能供应商团队的模型没有被很好地理解,效用发挥不出来。
虽然客户上下文仍然可以使用它,但是它应该通过使用一个我们后面要讨论的“防崩溃层”来保护自己。
如果客户不得不使用供应商团队的模型,而且这个模型做得很好,那么就需要顺从了。
客户团队遵从供应商团队的模型,完全顺从它。
这和共享内核很类似,但有一个重要的不同之处。客户团队不能对内核做更改。他们只能用它做自己模型的一部分,可以在所提供的现有代码上完成构建。
在很多情况下,这种方案是可行的。当有人提供一个丰富的组件,并提供了相应的接口时,我们就可以将这个组件看作我们自己的东西构建我们的模型。
如果组件有一个小的接口,那么最好只为它简单地创建一个适配器,在我们的模型和组件模型之间做转换。
这会隔离出我们的模型,可以有很高的自由度去开发它。
防崩溃层
我们会经常遇到所创建的新应用需要和遗留软件或者其他独立应用相交互的情况。
对领域建模器而言,这又是一个挑战。很多遗留应用从前没有用领域建模技术构建,而且它们的模型模糊不清,难于理解,也很难使用。
即使做得很好,遗留应用的模型对我们也不是那么有用,因为我们的模型很可能与它完全不同。
因此,在我们的模型和遗留模型之间就须要有一个集成层,这也是使用旧应用的需求之一。
让我们的客户端系统和外面的系统交互有很多种方法。
一种是通过网络连接,两个应用需要使用同一种网络通信协议,客户端需要遵从使用外部系统使用的接口。
另外一个交互的方法是数据库。外部系统使用存储在数据库里的数据。客户端系统被假定访问同样的数据库。
在这两个案例中,我们所处理的两个系统之间传输的都是原始数据。
但是这看上去有些简单,事实是原始数据不包括任何和模型相关的信息。我们不能将数据从数据库中取出来,全部作为原始
数据处理。在这些数据后面隐含着很多语义。一个关系型数据库含有和创建关系网的其他原始数据相关的原始数据。
数据语义非常重要,并且需要被充分考虑。客户端应用不能访问数据库,也不能不理解被使用数据的含义就进行写入操作。
我们看到外部模型的部分数据被反映在数据库里,然后进入我们的模型。
如果我们允许这样的事情发生,那么就会存在外部模型修改客户端模型的风险。
我们不能忽视和外部模型的交互,但是我们也应该小心地将我们的模型和它隔离开来。
我们应该在我们的客户端模型和外部模型之间建立一个防崩溃层。
从我们模型的观点来看,防崩溃层是模型很自然的一部分,并不像一个外部的什么东西。
它对概念和行为的操作和我们的模型类似,但是防崩溃层用外部语言和外部模型交流,而不是客户端语言。
这个层在两个域和语言之间扮演双向转换器,它最大的好处在于可以使客户端模型保持纯洁和持久,不会受到外部模型的干扰。
我们怎么实现防崩溃层?一个非常好的方案是将这个层看作从客户端模型来的一个服务。
使用服务是非常简单的,因为它抽象了其他系统并让我们在自己的范围内定位它。
服务会处理所需要的转换,所以我们的模型保持独立。
考虑到实际的实现,可以将服务看作比作一个Facade ( 参见Gamma et al 在1995 年写作《设计模式》)。
除了这一点, 防崩溃层最可能需要一个适配器(Adapter)。适配器可以使你将一个类的接口转换成客户端能理解的语言。
在我们的这个例子中,适配器不需要一定包装类,因为它的工作是在两个系统之间做转换。
防崩溃层也许包含多个服务。每一个服务都有一个相应的Facade,对每一个Facade 我们为之增加一个适配器。
我们不应该为所有的服务使用一个适配器,因为这样会使我们无法清晰地处理繁多的功能。
我们还必须再增加一些组件。适配器将外部系统的行为包装起来。
我们还需要对象和数据转换,这会使用一个转换器来解决。它可以是一个非常简单的对象,有很少的功能,满足数据转换的基本需要。
如果外部系统有一个复杂的接口,最好在适配器和接口之间再增加一个额外的Facade。
这会简化适配器的协议,将它和其他系统分离开来。
独立方法
到目前为止,我们尽己所能找到了整合子系统的途径,使它们协同工作,并且是在建模和设计都做得很好的情况下。
这需要努力和妥协。工作于各自子系统的团队需要花费大量的时间理清子系统之间的关系,需要持续不断地融合他们的代码,执行测试以保证没有破坏任何部分。
有时,某个团队还需要花费很多时间去实现其他团队需要的一些请求。妥协也是很有必要的。
一方面是你独立做开发,自由地选择概念和联盟,另一方面要确保你的模型适合另一个系统的框架。
我们也许会为了能和另一个子系统协同,而修改模型。或者有可能需要引入特殊的层,在两个子系统之间做转换。
有时我们必须这样做,但有时也可以采用其他的方法。我们需要严格地评估整合的价值,只在做这件事确实有价值时才去做。
如果我们得出的结论是整合难度很大,不值得这样去做,那么就应该考虑独立方法。
独立方法模式适合一个企业应用可由几个较小的应用组成,而且从建模的角度来看彼此之间有很少或者没有相同之处的情况。
它有一套自己的需求,从用户角度看这是一个应用,但是从建模和设计的观点来看,它可以由有独立实现的独立模型来完成。
我们应该现看看需求,然后了解一下它们是否可以被分割成两个或者多个不太相同的部分。
如果可以这样做,那么我们就创建独立的界定上下文(Bounded Context),并独立建模。
这样做的好处是有选择实现技术的自由。我们正创建的应用可能会共享一个通用的瘦GUI,作为链接和按钮的一个门户来访问每一个程序。
相对于集成后端的模型,组织应用是一个较小的集成。
在继续谈论独立方法之前,我们需要明确的是我们不会回到集成系统。
独立开发的模型是很难集成的,它们的相通之处很少,不值得这样做。
开放主机服务
当我们试图集成两个子系统时,通常要在它们之间创建一个转换层。
这个层在客户端子系统和我们想要集成的外部子系统之间扮演缓冲的角色。
这个层可以是个永久层,这要看关系的复杂度和外部子系统是如何设计的。
如果外部子系统不是被一个客户端子系统使用,而是被多个服务端子系统的话,我们就需要为所有的服务端子系统创建转换层。
所有的这些层会重复相同的转换任务,也会包含类似的代码。
当一个子系统要和其他许多子系统集成时,为每一个子系统定制一个转换器会使整个团队陷入困境。
会有越来越多的代码需要维护,当需要做出改变时,也会越来越担心。
解决这一个问题的方法是,将外部子系统看作服务提供者。
如果我们能为这个系统创建许多服务,那么所有的其他子系统就会访问这些服务,我们也就不需要任何转换层。
问题是每一个子系统也许需要以某种特殊的方式和外部子系统交互,那么要创建这些相关的服务可能会比较麻烦。
定义一个能以服务的形式访问你子系统的协议。开放它,使得所有需要和你集成的人都能获取到。
然后优化和扩展这个协议,使其可以处理新的集成需求,但某团队有特殊需求时除外。
对于特殊的需求,使用一个一次性的转换器增加协议,从而使得共享的协议保持简洁和精干。
精炼
精炼是从一个混合物中分离物质的过程。精炼的目的是从混合物中提取某种特殊的物质。在精炼的过程中,可能会得到某些副产品,它们也是很有价值的。
即使在我们提炼和创建很多抽象之后,一个大的领域还是会有一个大的模型。就是在重构多次之后,也依然会很大。
对于这样的情况,就需要精炼了。
思路是定义一个代表领域本质的核心域(Core Domain)。精炼过程的副产品将是组合领域中其他部分的普通子域(Generic Subdomain)。
在设计一个大系统时,有那么多分布式组件,所有的都是那么复杂而且绝对须要不出差错,这使得领域模型的本质,也就是真正的商业资产,变得模糊和不被重视。
当我们处理一个大的模型时,应该试图将本质概念从普通概念中分离出来。
一开始我们举了一个关于飞空中交通监控系统的例子。
我们说飞行计划包括飞机必须遵照的设计好的路线。在这个系统里,路线好像是一个无时不在的概念。实际上,这个概念是一个普通的概念,不是本质上的。
路线概念被用在许多领域里,可以设计一个普通的模型去描述它。
空中交通监控的本质在其他地方。
监控系统知道飞机要遵照的路线,但是它还会接收跟踪飞行中飞机的雷达网络输入。
这个数据显示飞机真正遵照的飞行路线,而它经常和预先描述好的有些偏差。
系统不得不基于飞机当前的飞行参数、飞机特点和天气情况来计算飞行轨道。
这一轨道是一个能完全描述飞机当前飞行路线的思维路线,它可能会在接下来的几分钟里被计算出来,也可能是几十分钟,或者是好几个小时。
每一个计算都有助于决策制定过程。
计算飞机轨道的终极目的是看看是不是会和其他飞机的飞行路线有交叉。
在机场附近,飞机起飞或者降落时,有很多在空中盘旋或者有其他要求。
如果一个飞机偏离了它的计划路线,很有可能会和其他飞机相撞。
空中交通监控系统会计算飞机的轨道,在出现这种交叉飞行时发出警报。
空中交通控制人员需要快速做出决策,改变飞机飞行路线,防止相撞发生。
飞机飞得越远,计算轨道的时间就越长,做出反应的时间也越长。
根据已有的数据同步飞机轨道的模块才是这个业务系统的“心脏”。可以将它标识为核心域。
路线模型更像是一个普通域。
系统的核心域要看我们如何理解系统。一个简单的路线系统会将路线和与它相关概念看作核心域,而空中交通监控系统却把它们看作普通子域。
一个系统的核心域有可能会变成另一个系统的普通子域。正确标识核心,以及它和其他模型之间的关系是非常重要的。
精炼模型。找到核心域,发现一个能轻松地从支持模型和代码中区分核心域的方法。
强调最有价值和特殊的概念。使核心变小。
将你所有的才能都投入到核心域上,不停地努力,找到一种深刻的模型,做一个足够灵活的设计以满足系统的远景。
根据其他部分对提炼后核心的支持,判断投资。
让最好的开发人员去承担核心域的任务是重要的。
开发人员经常沉溺于技术,喜欢学习最好的和最新的语言,相对于业务逻辑他们更关注基础架构。
领域的业务逻辑于他们毫无兴趣可言,也看不到什么回报。
还记得我们前面所谈的飞机轨道案例的要点吗?当项目完成时,所有的知识都成为毫无意义的过去。
但是领域的业务逻辑是业务的核心所在。这个核心的设计和实现中如果有了错误,将会导致整个项目的失败。
如果核心业务逻辑不起作用,所有的技术亮点都等于垃圾。
核心域的创建不是能一朝一夕完成的。这需要一个提炼的过程,另外重构在核心越来越明显之前也是很必要的。
我们需要硬性地将核心放在设计的中心位置,并划定它的界限。
我们还需要重新考虑和新核心相关的其他模型的要素,也许它们也需要被重构,相关的功能也许需要被改变等。
没有特殊的知识,模型的某些部分也会增加复杂度。任何额外的事情都会使核心域难于辨认和被理解。
因为广为人知的普遍原则,或者那些不是你首要关注但是扮演支持角色的细节都会影响模型。
但是那些部分对系统功能完整性和模型的完整表达都依然是必要的。
标识出在你项目中不是推动因素的相关子域。找出那些子域的普通模型,并将它们放在不同的模型中。
不要在这些模型中留下什么特殊的印记。
一旦它们被分离开,就将对它们的持续开发的优先级调得比核心域要低,而且注意不要把你的核心开发人员分配到这些任务中(因为它们从中获取不了多少领域知识)。
另外要考虑现成的解决方案,或者那些已发布的针对普通子域的模型。
每个领域使用其他领域使用的概念。钱和它们相关的概念,比如行情和汇率,可以被包含在不同的系统里。
图表是另外一个被广泛使用的概念,就它本身而言是非常复杂,但是可以被用在许多应用中。
有下面集中方法可以实现普通子域:
1. 购买现成的方案。
这个方法的好处是可以使用别人已经做好的全套方案。随之而来的是学习曲线的问题,而且这样的方案还会引入其他麻烦。
比如如果代码有错误,你只得等待别人来解决。你还需要使用特定的编译器和类库版本。和自己系统的集成也不是那么容易。
2. 外包。
将设计和实现交给另外一个团队,有可能是其他公司的。这样做可以使你专注于核心域,从处理其他领域的重压下释放出来。
不便的地方是集成外包的代码。需要和子域通信的结构需要预先定义好,还要和外包团队保持沟通。
3. 已有模型。
一个取巧的方案是使用一个已经创建的模型。世面上已经有一些关于分析模型的书,可以作为我们子域的灵感来源。
直接复制原有的模型不太现实,但确实有些只需要做少许改动就可以用了。
4. 自己实现。
这个方案的好处是能够做到最好的集成,但这也意味着额外的付出,包括维护的压力等。