领域驱动设计(5)保持模型⼀致性
本章涉及的是需要若⼲个团队通⼒配合的⼤型项⽬。
当有多个团队时,我们必须⾯对⼀系列不同的挑战,开发这样⼀下项⽬的认为需要不同的管理和协作。
企业级项⽬通常都是⼤型项⽬需要使⽤许多的技术和资源。
这样的项⽬的设计将仍然基于⼀个领域模型展开,并且我们需要采⽤适当的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)接⼝的⾃动化验收测试。将这些测试增加到供应商团队的测试集⾥,作为它的持续集成的⼀部分运⾏。
这个测试能使供应商团队放⼼地做修改,⽽不⽤担⼼影响客户团队的应⽤。
顺从者
在两个团队都有兴趣合作时,客户-供应商关系是可⾏的。
客户⾮常依赖于供应商,但供应商不是。
如果有管理保证合作的执⾏,供应商会给于客户需要的关注,并聆听客户的要求。
如果管理没有清晰地界定在两个团队之间需要完成什么,或者管理很差,或者就没有管理,供应商慢慢地会越来越关⼼它的模型和设计,⽽也越来越疏于帮助客户。
毕竟他们有⾃⼰的⼯作完成底线。即使他们是好⼈,愿意帮助其他团队,时间的压⼒却不允许他们这么做,客户团队深受其害。
在团队属于不同公司的情况下,这样的事情也会发⽣。交流是困难的,供应商的公司也许没兴趣在关系沟通上投资太多。
他们要么提供少许帮助,或者直接拒绝合作。结果是客户团队孤⽴⽆援,只能尽⾃⼰的努⼒摸索模型和设计。
当两个开发团队有客户-供应商关系,⽽且供应商团队没有动⼒为客户团队的需要提供帮助时,客户团队是⽆助的。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。