《领域驱动设计精简版》读书笔记(4)——保持模型一致性

保持模型一致性

当遇到需要若干个团队通力配合的大型项目,我们应该做的是有意识地将大模型分解成数个较小的部分。只要遵守相绑定的契约,整合得好的小模型会越来越有独立性。每个模型都应该有一个清晰的边界,模型之间的关系也应该被精确地定义。有一整套技术可以保证模型的完整性:
保证模型完整性技术

界定上下文(Defined Context)

定义模型的范围,画出它的上下文的边界,然后尽最大可能保持模型的一致性。要在模型涵盖整个企业项目时保持它的纯洁是很困难的,但是在它被限定到一个特定区域时就相对容易很多。要在应用到模型的地方明确定义上下文。在团队组织里明确定义边界,在应用的具体部分明确定义用法,以及像代码库和数据库 Schema 的物理显示。保持模型在这些边界里的严格一致,不要因外界因素的干扰而有异动。被界定的上下文不是模型,界定的上下文提供有模型参与的逻辑框架。模块被用来组织模型的要素,因此界定的上下文包含模块

当不同的团队不得不共同工作于一个模型时,我们必须要各司其职。要时刻意识到任何针对模型的变化都有可能破坏现有的功能。当使用多个模型时,每个人可以自由使用自己的那一部分。我们都知道自己模型的局限,都恪守在这些边界里。我们需要确保模型的纯洁、一致和完整。每个模型应能使重构尽可能容易,而不会影响到其他的模型。而且为了达到纯洁的最大化,设计还要可以被精简和提炼。

持续集成(Continuous Integration)

一个团队工作于一个界定的上下文,也有犯错误的空间。在团队内部我们需要充分的沟通,以确保每个人都能理解模型中每个部分所扮演的角色。如果一个人不理解对象间的关系,他就可能会以和原意完全相反的方式修改代码。如果我们不能百分之百地专注于模型的纯洁性,就会很容易犯这种错误。团队的某个成员可能会在不知道已经有自己所需代码的情况下增加重复代码,或者担心破坏现有的功能而不改变已有的代码选择重复增加。

模型不是一开始就被完全定义。模型先被创建,然后基于对领域新的发现和来自开发过程的反馈等再继续完善,这意味着新的概念会进入模型,新的部分也会被增加到代码中。所有的这些需求都会被集成进一个统一的模型,进而用代码实现之。这也就是为什么持续集成在界定的上下文中如此必要的原因。我们需要这样一个集成的过程,以确保所有新增的部分和模型原有的部分配合得很好,在代码中也被正确地实现。我们需要有个过程来合并代码,合并得越早越好。

持续集成是基于模型中概念的集成,然后再通过测试实现。任何不完整的模型在实现过程中都会被检测出来。持续集成应用于界定的上下文,不会被用来处理相邻上下文之间的关系。

上下文映射(Context Map)

一个企业应用有多个模型,每个模型有自己界定的上下文。建议用上下文作为团队组织的基础。在同一个团队里的人们能更容易地沟通,也能很好地将模型集成和实现。但是每个团队都工作于自己的模型,所以最好让每个人都能了解所有的模型。上下文映射是指抽象出不同界定上下文和它们之间关系的文档,它可以是像下面所说的一个示意图(Diagram),也可以是其他任何写就的文档。详细的层次各有不同。它的重要之处是让每个在项目中工作的人都能够得到并理解它。
示意图
只有独立的统一模型还不够。它们还要被集成,因为每个模型的功能都只是整个系统的一部分。在最后,单个的部分要被组织在一起,整个的系统必须能正确工作。如果上下文定义的不清晰,很有可能彼此之间互相覆盖。如果上下文之间的关系没有被抽象出来,在系统被集成的时候它们就有可能不能工作。

共享内核(Shared Kernel)

当缺少功能集成时,持续集成可能就遥不可及了。尤其是在团队不具备相关的技术或者行政组织来维护持续集成,或者是某个团队又大又笨拙的时候。协同工作于有紧密关系的应用程序上的不协调团队有时会进展很快,但他们所做的有可能很难整合。他们在转换层和技巧花样上花费了过多的时间,而没有在最重要的持续集成上下功夫,做了许多重复劳动也没有体味到通用语言带来的好处。因此,需要指派两个团队统一共享的领域模型子集,当然除了模型的子集部分,还要包括代码自己或者和模型相关联的数据库设计子集。这个明确被共享的东西有特别的状态,没有团队之间的沟通不能做修改。要经常整合功能系统,但是可以不用像在团队里进行持续集成那么频繁。在集成的时候,在两个团队里都要运行测试。

共享内核的目的是减少重复,但是仍保持两个独立的上下文。对于共享内核的开发需要多加小心。两个开发团队都有可能修改内核代码,还要必须整合所做的修改。如果团队用的是内核代码的副本,那么要尽可能早地融合(Merge)代码,至少每周一次。还应该使用测试工具,这样每一个针对内核的修改都能快速地被测试。内核的任何改变都应该通知另一个团队,团队之间密切沟通,使大家都能了解最新的功能。

客户-供应商(Customer-Supplier)

我们经常会遇到两个子系统之间关系特殊的时候:一个严重依赖另一个。两个子系统所在的上下文是不同的,而且一个系统的处理结果被输入到另外一个。它们没有共享的内核,因为从概念上理解也许不可以有这样一个内核,或者对两个子系统而言要共享代码在技术上也不可能实现。

我们曾讨论了一个关于在线商店应用的模型,包括报表和通讯两部分内容。我们已经解释说最好要为所有的那些上下文创建各自分开的模型,因为只有一个模型时会在开发过程中遇到瓶颈和资源的争夺。假设我们同意有分开的模型,那么在Web 商店字系统和报表系统间的关系是什么样子的呢?共享内核看上去不是好的选择。子系统很可能会用不同的技术被实现。一个是纯浏览器体验,而另一个可能是丰富的 GUI 应用。尽管如果报表应用是用 Web 接口实现,各自模型的注意概念也是不同的。也许会有越界的情况,但还不足以应用共享内核。所以我们选择走不同的道路。另外,E 商店子系统并不全依赖报表系统。E 商店应用的用户是 Web 客户,是那些浏览商品并下单的人。所有的客户、商品和订单数据被放在一个数据库里。就是这样。E 商店应用不会真的关心各自的数据发生了什么。而同时,报表应用非常关心和需要由 E 商店应用保存的数据。它还需要一些额外的信息以执行它提供的报表服务。客户可能在购物篮里放了一些商品,但在结账的时候又去掉了。某个客户访问的链接可能多于其他人等。这样的信息对 E 商店应用没有什么意义,但是对报表应用却意义非凡。由此,供应商子系统不得不实现一些客户子系统会用到的规范。这是联系两个子系统的纽带。

当我们面对这样一个场景时,应该就开始“演出”了。报表团队应该扮演客户角色,而 E 商店团队应该扮演供应商角色。两个团队应该定期碰面或者提邀请,像一个客户对待他的供应商那样交谈。客户团队应该代表系统的需求,而供应商团队据此设置计划。当客户团队所有的需求都被激发出来后,供应商团队就可以决定实现它们的时间表。如果认为一些需求非常重要,那么应该先实现它们,延迟其他的需求。客户团队还需要输入和能被供应商团队分享的知识。

在两个团队之间确定一个明显的客户/供应商关系。在计划场景里,让客户团队扮演和供应商团队打交道的客户角色。为客户需求做充分的解释和任务规划,让每个人理解相关的约定和日程表。

顺从者(Compliant)

在两个团队都有兴趣合作时,客户-供应商关系是可行的。客户非常依赖于供应商,但供应商不是。如果有管理保证合作的执行,供应商会给于客户需要的关注,并聆听客户的要求。如果管理没有清晰地界定在两个团队之间需要完成什么,或者管理很差,或者就没有管理,供应商慢慢地会越来越关心它的模型和设计,而也越来越疏于帮助客户。毕竟他们有自己的工作完成底线。即使他们是好人,愿意帮助其他团队,时间的压力却不允许他们这么做,客户团队深受其害。在团队属于不同公司的情况下,这样的事情也会发生。交流是困难的,供应商的公司也许没兴趣在关系沟通上投资太多。他们要么提供少许帮助,或者直接拒绝合作。结果是客户团队孤立无援,只能尽自己的努力摸索模型和设计。

如果客户不得不使用供应商团队的模型,而且这个模型做得很好,那么就需要顺从了。客户团队遵从供应商团队的模型,完全顺从它。这和共享内核很类似,但有一个重要的不同之处——客户团队不能对内核做更改,他们只能用它做自己模型的一部分,可以在所提供的现有代码上完成构建。在很多情况下,这种方案是可行的。当有人提供一个丰富的组件,并提供了相应的接口时,我们就可以将这个组件看作我们自己的东西构建我们的模型。如果组件有一个小的接口,那么最好只为它简单地创建一个适配器,在我们的模型和组件模型之间做转换。这会隔离出我们的模型,可以有很高的自由度去开发它。

防崩溃层(Anticorruption Layer)

我们会经常遇到所创建的新应用需要和遗留软件或者其他独立应用相交互的情况。对领域建模器而言,这又是一个挑战。很多遗留应用从前没有用领域建模技术构建,而且它们的模型模糊不清,难于理解,也很难使用。即使做得很好,遗留应用的模型对我们也不是那么有用,因为我们的模型很可能与它完全不同。因此,在我们的模型和遗留模型之间就须要有一个集成层,这也是使用旧应用的需求之一。

让我们的客户端系统和外面的系统交互有很多种方法。一种是通过网络连接,两个应用需要使用同一种网络通信协议,客户端需要遵从使用外部系统使用的接口。另外一个交互的方法是数据库。外部系统使用存储在数据库里的数据。客户端系统被假定访问同样的数据库。在这两个案例中,我们所处理的两个系统之间传输的都是原始数据。但是这看上去有些简单,事实是原始数据不包括任何和模型相关的信息。我们不能将数据从数据库中取出来,全部作为原始数据处理。在这些数据后面隐含着很多语义。一个关系型数据库含有和创建关系网的其他原始数据相关的原始数据。数据语义非常重要,并且需要被充分考虑。客户端应用不能访问数据库,也不能不理解被使用数据的含义就进行写入操作。我们看到外部模型的部分数据被反映在数据库里,然后进入我们的模型。

如果我们允许这样的事情发生,那么就会存在外部模型修改客户端模型的风险。我们不能忽视和外部模型的交互,但是我们也应该小心地将我们的模型和它隔离开来。我们应该在我们的客户端模型和外部模型之间建立一个防崩溃层。从我们模型的观点来看,防崩溃层是模型很自然的一部分,并不像一个外部的什么东西。它对概念和行为的操作和我们的模型类似,但是防崩溃层用外部语言和外部模型交流,而不是客户端语言。这个层在两个域和语言之间扮演双向转换器,它最大的好处在于可以使客户端模型保持纯洁和持久,不会受到外部模型的干扰。

实现防奔溃层一个非常好的方案是将这个层看作从客户端模型来的一个服务。使用服务是非常简单的,因为它抽象了其他系统并让我们在自己的范围内定位它。服务会处理所需要的转换,所以我们的模型保持独立。考虑到实际的实现,可以将服务看作比作一个Facade。除了这一点,防崩溃层最可能需要一个适配器(Adapter)。适配器可以使你将一个类的接口转换成客户端能理解的语言。在我们的这个例子中,适配器不需要一定包装类,因为它的工作是在两个系统之间做转换。
防奔溃层
防崩溃层也许包含多个服务。每一个服务都有一个相应的 Facade,对每一个 Facade 我们为之增加一个适配器。我们不应该为所有的服务使用一个适配器,因为这样会使我们无法清晰地处理繁多的功能。

我们还必须再增加一些组件。适配器将外部系统的行为包装起来。我们还需要对象和数据转换,这会使用一个转换器来解决。它可以是一个非常简单的对象,有很少的功能,满足数据转换的基本需要。如果外部系统有一个复杂的接口,最好在适配器和接口之间再增加一个额外的 Facade。这会简化适配器的协议,将它和其他系统分离开来。

独立方法(Independent Method)

独立方法模式适合一个企业应用可由几个较小的应用组成,而且从建模的角度来看彼此之间有很少或者没有相同之处的情况。它有一套自己的需求,从用户角度看这是一个应用,但是从建模和设计的观点来看,它可以由有独立实现的独立模型来完成。我们应该先看看需求,然后了解一下它们是否可以被分割成两个或者多个不太相同的部分。如果可以这样做,那么我们就创建独立的界定上下文(Bounded Context),并独立建模。这样做的好处是有选择实现技术的自由。我们正创建的应用可能会共享一个通用的瘦 GUI,作为链接和按钮的一个门户来访问每一个程序。相对于集成后端的模型,组织应用是一个较小的集成。在继续谈论独立方法之前,我们需要明确的是我们不会回到集成系统。独立开发的模型是很难集成的,它们的相通之处很少,不值得这样做。

开放主机服务

当我们试图集成两个子系统时,通常要在它们之间创建一个转换层。这个层在客户端子系统和我们想要集成的外部子系统之间扮演缓冲的角色。这个层可以是个永久层,这要看关系的复杂度和外部子系统是如何设计的。如果外部子系统不是被一个客户端子系统使用,而是被多个服务端子系统使用的话,我们就需要为所有的服务端子系统创建转换层。所有的这些层会重复相同的转换任务,也会包含类似的代码。当一个子系统要和其他许多子系统集成时,为每一个子系统定制一个转换器会使整个团队陷入困境。会有越来越多的代码需要维护,当需要做出改变时,也会越来越担心。

解决这一个问题的方法是,将外部子系统看作服务提供者。如果我们能为这个系统创建许多服务,那么所有的其他子系统就会访问这些服务,我们也就不需要任何转换层。问题是每一个子系统也许需要以某种特殊的方式和外部子系统交互,那么要创建这些相关的服务可能会比较麻烦。

定义一个能以服务的形式访问你子系统的协议。开放它,使得所有需要和你集成的人都能获取到。然后优化和扩展这个协议,使其可以处理新的集成需求,但某团队有特殊需求时除外。对于特殊的需求,使用一个一次性的转换器增加协议,从而使得共享的协议保持简洁和精干。

参考

《领域驱动设计精简版》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值