第1章 消化知识

几年前,我着手设计一个用于设计印制电路板(PCB)的专用软件工具。但有一个问题,我对电子硬件一无所知。当然,我也曾拜访过一些PCB设计师,但用不了3分钟,他们就令我晕头转向。如何才能了解足够多的知识,以便开始编写这个软件呢?当然,我并不打算在交付期限到来之前成为电子工程师。

我们试着让PCB设计师说明软件具体应该做些什么,但我们错了。虽然他们是优秀的电路设计师,但软件知识却太有限了,往往只知道如何读取一个ASCII文件、对它排序,然后添加一些注释并将它写回文件中,再生成一个报告。这些知识显然无法帮助他们大幅度提高效率。

最初的几次会面令人气馁,但我们在他们要求的报告中也看到了一丝希望。这些报告中总是涉及net这个词以及与其相关的各种细节。在这个领域中,net实质上是一种导线,它可以连接PCB上任意数量的元件,并向它连接的所有元件传递电子信号。这样,我们就得到了领域模型的第一个元素,如图1-1所示。

第1章 消化知识 - 图1 图1-1

就这样,我们一边讨论所需的软件功能,一边开始画图。我使用一种非正式的、稍加变化的对象交互图来走查[1]各种场景,如图1-2所示。

第1章 消化知识 - 图2 图1-2

PCB专家1:元件不一定就是芯片(chip)。

开发人员(我):那它们是不是只应该叫做“元件”?

专家1:我们将它们称作“元件实例”(component instance)。相同的元件可能有很多。

专家2:他把“net”画成和元件实例一样的框了。

专家1:他没有使用我们的符号。我猜想,他要把每一项都画成方框。

开发人员:很抱歉,是这样的。我想我最好对这个符号稍加解释。

他们不断地纠正我的错误,在这个过程中我开始学习他们的知识。我们共同消除了术语上的不一致和歧义,也消除了他们在技术观点上的分歧,在这个过程中,他们也得到了学习。他们的解释更准确和一致了,然后我们开始共同开发一个模型。

专家1:只说一个信号到达一个ref-des是不够明确的,我们必须知道信号到达了哪个引脚。

开发人员:什么是ref-des?

专家2:它就是一个元件实例。我们用的一个专门工具中用ref-des这个名称。

专家1:总之,net将一个实例的某个引脚与另一个实例的某个引脚相连。

开发人员:一个引脚是不是只属于一个元件实例,而且只与一个net相连?

专家1:对,是这样。

专家2:还有,每个net都有一个拓扑结构,也就是电路的布局,它决定了net内部各元件的连接方式。

开发人员:嗯,这样画如何(如图1-3所示)?

第1章 消化知识 - 图3 图1-3

为了让讨论更集中,接下来的一段时间我们探讨了一个特定的功能:探针仿真(probe simulation)。探针仿真跟踪信号的传播,以便检测在设计中可能出现特定类型问题的位臵。

开发人员:现在我已经明白了Net是如何将信号传播给它所连接的所有Pin的,但如何将信号传送得更远呢?这与拓扑结构(topology)有关系吗?

专家2:没有,是元件推送信号前进。

开发人员:我们肯定无法对芯片的内部行为建模,因为这太复杂了。

专家2:我们不必这样做。可以使用一种简化形式。只需列出通过元件可从某些Pin将信号推送到其他引脚即可。

开发人员:类似于这样吗?

(经过反复的尝试和修改,我们终于共同绘制出了一个草图,如图1-4所示。)

第1章 消化知识 - 图4 图1-4

开发人员:但你想从这种计算中知道什么呢?

专家2:我们要查找较长的信号延迟,也就是说,查找超过2或3跳的信号路径。这是一条经验法则。如果路径太长,信号可能无法在时钟周期内到达。

开发人员:超过3跳……这么说我们需要计算路径长度。那么怎样算作一跳呢?

专家2:信号每通过一个Net,就称为1跳。

开发人员:那么我们可以沿着电路来计算跳数,每遇到一个net,跳数就加1,如图1-5所示。

第1章 消化知识 - 图5 图1-5

开发人员:现在我唯一不明白的地方是“推动”是从哪里来的。是否每个元件实例都需要存储该数据?

专家2:一个元件的所有实例的推动行为都是相同的。

开发人员:那么元件的类型决定了推动行为,而每个实例的推动行为都是相同的(如图1-6所示)?

第1章 消化知识 - 图6 图1-6

专家2:这个图的意思我没完全明白,但我猜想每个元件存储的推动行为就差不多是这样的吧。

开发人员:抱歉,这个地方我可能问得有点过细了。我只是想考虑得全面一些……现在,拓扑结构对它有什么影响吗?

专家1:拓扑结构不影响探针仿真。

开发人员:那么可以暂不考虑它,是吗?等用到这些特性时再回来讨论它。

就这样,我们的讨论一直进行下去(其中遇到的困难比上面显示的多得多)。我们一边进行“头脑风暴”式的讨论,一边对模型进行精化,边提问边回答。随着我对领域理解的加深,以及他们对模型在解决方案中作用的理解的加深,模型不断发展。图1-7显示了那个早期模型的类图。

第1章 消化知识 - 图7 图1-7

随后,我们又拿出一部分工作时间进行了几轮这样的讨论,我觉得自己已经理解了足够多的知识,可以试着编写一些代码了。我写了一个非常简单的原型,并用一个自动测试框架来测试它。我避开了所有的基础设施。这个原型没有持久化机制,也没有用户界面(UI)。这样我就可以专注于代码的行为。只不过几天我就能够演示简单的探针仿真了。虽然它使用的是虚拟数据,而且向控制台输出的是原始文本,但确实是使用Java对象对路径长度执行实际的计算。这些Java对象所反映的模型正是我和领域专家们一起开发出来的。

这个具体的原型使得领域专家们更清楚地理解了模型的含义,以及它与最终软件之间的联系。从那时起,我们的模型讨论越来越具有互动性了,因为他们可以看到我如何将新学到的知识融合到模型中,然后反映到软件上。他们也可以从原型得到具体的反馈,从而印证自己的想法。

模型中包含与我们要解决的问题有关的PCB领域知识,这些知识远远比我们在这里演示的复杂。模型将很多同义词和语言描写中的微小差别做了统一,并排除了数百条与问题没有直接关系的事实(虽然工程师们都理解这些事实),如元件的实际数字特性。像我这样的软件专业人员看到这张图后,几分钟内就能明白软件是做什么的。这个模型就相当于一个框架,开发人员可以借助它来组织新的信息并更快地学习,从而更准确地判断哪些部分重要,哪些部分不重要,并更好地与PCB工程师进行沟通。

当PCB工程师提出新的功能需求时,我就让他们带我走查对象交互的场景。当模型对象无法清楚地表达某个重要场景时,我们就通过头脑风暴活动创建新的模型对象或者修改原有的模型对象,并消化理解这些模型对象中的知识。在我们精化模型的过程中,代码也随之一步步演进。几个月后,PCB工程师们得到了一个远远超乎他们期望的功能丰富的工具。

1.1 有效建模的要素

以下几方面因素促使上述案例得以成功。

(1) 模型和实现的绑定。最初的原型虽然简陋,但它在模型与实现之间建立了早期链接,而且在所有后续的迭代中我们一直在维护该链接。

(2) 建立了一种基于模型的语言。最初,工程师们不得不向我解释基本的PCB问题,而我也必须向他们解释类图的含义。但随着项目的进展,双方都能够直接使用模型中的术语,并将它们组织为符合模型结构的语句,而且无需翻译即可理解互相要表达的意思。

(3) 开发一个蕴含丰富知识的模型。对象具有行为和强制性规则。模型并不仅仅是一种数据模式,它还是解决复杂问题不可或缺的部分。模型包含各种类型的知识。

(4) 提炼模型。在模型日趋完整的过程中,重要的概念不断被添加到模型中,但同样重要的是,不再使用的或不重要的概念则从模型中被移除。当一个不需要的概念与一个需要的概念有关联时,则把重要的概念提取到一个新模型中,其他那些不要的概念就可以丢弃了。

(5) 头脑风暴和实验。语言和草图,再加上头脑风暴活动,将我们的讨论变成“模型实验室”,在这些讨论中可以演示、尝试和判断上百种变化。当团队走查场景时,口头表达本身就可以作为所提议的模型的可行性测试,因为人们听到口头表达后,就能立即分辨出它是表达得清楚、简捷,还是表达得很笨拙。

正是头脑风暴和大量实验的创造力才使我们找到了一个富含知识的模型并对它进行提炼,在这个过程中,基于模型的语言提供了很大帮助,而且贯穿整个实现过程中的反馈闭环也对模型起到了“训练”作用。这种知识消化将团队的知识转化为有价值的模型。

1.2 知识消化

金融分析师要消化理解的内容是数字。他们筛选大量的详细数字,对其进行组合和重组以便寻求潜在的意义,查找可以产生重要影响的简单表示方式——一种可用作金融决策基础的理解。

高效的领域建模人员是知识的消化者。他们在大量信息中探寻有用的部分。他们不断尝试各种信息组织方式,努力寻找对大量信息有意义的简单视图。很多模型在尝试后被放弃或改造。只有找到一组适用于所有细节的抽象概念后,工作才算成功。这一精华严谨地表示了所发现的最为相关的知识。

知识消化并非一项孤立的活动,它一般是在开发人员的领导下,由开发人员与领域专家组成的团队来共同协作。他们共同收集信息,并通过消化而将它组织为有用的形式。信息的原始资料来自领域专家头脑中的知识、现有系统的用户,以及技术团队以前在相关遗留系统或同领域的其他项目中积累的经验。信息的形式也多种多样,有可能是为项目编写的文档,有可能是业务中使用的文件,也有可能来自大量的讨论。早期版本或原型将经验反馈给团队,然后团队对一些解释做出修改。

在传统的瀑布方法中,业务专家与分析员进行讨论,分析员消化理解这些知识后,对其进行抽象并将结果传递给程序员,再由程序员编写软件代码。由于这种方法完全没有反馈,因此总是失败。分析员全权负责创建模型,但他们创建的模型只是基于业务专家的意见。他们既没有向程序员学习的机会,也得不到早期软件版本的经验。知识只是朝一个方向流动,而且不会累积。

有些项目使用了迭代过程,但由于没有对知识进行抽象而无法建立起知识体系。开发人员听专家们描述某项所需的特性,然后开始构建它。他们将结果展示给专家,并询问接下来做什么。如果程序员愿意进行重构,则能够保持软件足够整洁,以便继续扩展它;但如果程序员对领域不感兴趣,则他们只会了解程序应该执行的功能,而不去了解它背后的原理。虽然这样也能开发出可用的软件,但项目永远也不会从原有特性中自然地扩展出强大的新特性。

好的程序员会自然而然地抽象并开发出一个可以完成更多工作的模型。但如果在建模时只是技术人员唱独角戏,而没有领域专家的协作,那么得到的概念将是很幼稚的。使用这些肤浅知识开发出来的软件只能做基本工作,而无法充分反映出领域专家的思考方式。

在团队所有成员一起消化理解模型的过程中,他们之间的交互也会发生变化。领域模型的不断精化迫使开发人员学习重要的业务原理,而不是机械地进行功能开发。领域专家被迫提炼自己已知道的重要知识的过程往往也是完善其自身理解的过程,而且他们会渐渐理解软件项目所必需的概念严谨性。

所有这些因素都促使团队成员成为更合格的知识消化者。他们对知识去粗取精。他们将模型重塑为更有用的形式。由于分析员和程序员将自己的知识输入到了模型中,因此模型的组织更严密,抽象也更为整洁,从而为实现提供了更大支持。同时,由于领域专家也将他们的知识输入到了模型中,因此模型反映了业务的深层次知识,而且真正的业务原则得以抽象。

模型在不断改进的同时,也成为组织项目信息流的工具。模型聚焦于需求分析。它与编程和设计紧密交互。它通过良性循环加深团队成员对领域的理解,使他们更透彻地理解模型,并对其进一步精化。模型永远都不会是完美的,因为它是一个不断演化完善的过程。模型对理解领域必须是切实可用的。它们必须非常精确,以便使应用程序易于实现和理解。

1.3 持续学习

当开始编写软件时,其实我们所知甚少。项目知识零散地分散在很多人和文档中,其中夹杂着其他一些无关信息,因此我们甚至不知道哪些知识是真正需要的知识。看起来没什么技术难度的领域很可能是一种错觉——我们并没意识到不知道的东西究竟有多少。这种无知往往会导致我们做出错误的假设。

同时,所有项目都会丢失知识。已经学到了一些知识的人可能干别的事去了。团队可能由于重组而被拆散,这导致知识又重新分散开。被外包出去的关键子系统可能只交回了代码,而不会将知识传递回来。而且当使用典型的设计方法时,代码和文档不会以一种有用的形式表示出这些来之不易的知识,因此一旦由于某种原因人们没有口头传递知识,那么知识就丢失了。

高效率的团队需要有意识地积累知识,并持续学习[Kerievsky 2003]。对于开发人员来说,这意味着既要完善技术知识,也要培养一般的领域建模技巧(如本书中所讲的那些技巧)。但这也包括认真学习他们正在从事的特定领域的知识。

那些善于自学的团队成员会成为团队的中坚力量,涉及最关键领域的开发任务要靠他们来攻克(有关这方面的更多内容,参见第15章)。这个核心团队头脑中积累的知识使他们成为更高效的知识消化者。

读到这里,请先停一下来问自己一个问题。你是否学到了一些PCB设计知识?虽然这个示例只对该领域作了些表面处理,但当讨论领域模型时,仍会学到一些知识。我学习了大量知识,但并没有学习如何成为一名PCB工程师,因为这不是我的目的。我的目的是学会与PCB专家沟通,理解与应用有关的主要概念,并学会检查所构建的内容是否合理。

事实上,我们的团队最终发现探针仿真并不是一项重要的开发任务,因此最后彻底放弃了这个功能。连同它一起删除的还有模型中的一些部分,这些部分只是帮助我们理解如何通过元件推动信号以及如何计算跳数。这样,应用程序的核心就转移到了别处,而且模型也随之改变,将新的重点作为核心。在这个过程中,领域专家们也学到了很多东西,而且更加清楚地理解了应用程序的目标(第15章会更深入地讨论这些问题)。

尽管如此,那些早期工作还是非常重要的。关键的模型元素被保留下来,而更重要的是,早期工作启动了知识消化的过程,这使得所有后续工作更加高效:团队成员、开发人员和领域专家等都学到了知识,他们开始使用一种公共的语言,而且形成了贯穿整个实现过程的反馈闭环。这样,一个发现之旅悄然开始了。

1.4 知识丰富的设计

通过像PCB示例这样的模型获得的知识远远不只是“发现名词”。业务活动和规则如同所涉及的实体一样,都是领域的核心,任何领域都有各种类别的概念。知识消化所产生的模型能够反映出对知识的深层理解。在模型发生改变的同时,开发人员对实现进行重构,以便反映出模型的变化,这样,新知识就被合并到应用程序中了。

当我们的建模不再局限于寻找实体和值对象时,我们才能充分吸取知识,因为业务规则之间可能会存在不一致。领域专家在反复研究所有规则、解决规则之间的矛盾以及以常识来弥补规则的不足等一系列工作中,往往不会意识到他们的思考过程有多么复杂。软件是无法完成这一工作的。正是通过与软件专家紧密协作来消化知识的过程才使得规则得以澄清和充实,并消除规则之间的矛盾以及删除一些无用规则。

示例 提取一个隐藏的概念

我们从一个非常简单的领域模型开始学习,基于此模型的应用程序用来预订一艘船在一次航程中要运载的货物,如图1-8所示。

第1章 消化知识 - 图8 图1-8

我们规定这个应用程序的任务是将每件货物(Cargo)与一次航程(Voyage)关联起来,记录并跟踪这种关系。现在看来一切都还算简单。应用程序代码中可能会有一个像下面这样的方法:

第1章 消化知识 - 图9

第1章 消化知识 - 图10

由于总会有人在最后一刻取消订单,因此航运业的一般做法是接受比其运载能力多一些的货物。这称为“超订”。有时使用一个简单的容量百分比来表示,如预订110%的载货量。有时则采用复杂的规则——主要客户或特定种类的货物优先。

这是航运领域的一个基本策略,从事航运业的业务人员都知道它,但在软件团队中可能不是所有技术人员都知道这条规则。

需求文档中包含下面这句话:

允许10%的超订。

现在,类图就应该像图1-9这样,代码如下:

*

第1章 消化知识 - 图12 图1-9

现在,一条重要的业务规则被隐藏在上面这段方法代码的一个卫语句[2]中。第4章将介绍LAYERED ARCHITECTURE,它会帮助我们将超订规则转移到领域对象中,但现在我们主要考虑如何把这条规则更清楚地表达出来,并让项目中的每个人都能了解到它。这将使我们得到一个类似的解决方案。

(1) 如果业务规则如上述代码所写,不可能有业务专家会通过阅读这段代码来检验规则,即使在开发人员的帮助下也无法完成。

(2) 非业务的技术人员很难将需求文本与代码联系起来。

如果规则更复杂,情况将更糟。

我们可以改变一下设计来更好地捕获这个知识。超订规则是一个策略,如图1-10所示。策略(policy)其实是STRATEGY模式[3][Gamma et al.1995]的别名。我们知道,使用STRATEGY的动机一般是为了替换不同的规则,虽然在这里并不需要这么做。但我们要获取的概念的确符合策略的含义,这在领域驱动设计中是同等重要的动机(参见第12章)。

第1章 消化知识 - 图13 图1-10

修改后的代码如下:

第1章 消化知识 - 图14

新的Overbooking Policy类包含以下方法:

*

现在所有人都清楚超订是一个独特的策略,而且超订规则的实现即明确又独立。

现在,我并不建议将这样的精细设计应用到领域的每个细节中。第15章将深入阐述如何关注重点以及如何隔离其他问题或使这些问题最小化。这个例子的目的是说明领域模型和相应的设计可用来保护和共享知识。更明确的设计具有以下优点:

(1) 为了实现更明确的设计,程序员和其他各位相关人员都必须理解超订的本质,明白它是一个明确且重要的业务规则,而不只是一个不起眼的计算。

(2) 程序员可以向业务专家展示技术工件,甚至是代码,但应该是领域专家(在程序员指导下)可以理解的,以便形成反馈闭环。

1.5 深层模型

有用的模型很少停留在表面。随着对领域和应用程序需求的理解逐步加深,我们往往会丢弃那些最初看起来很重要的表面元素,或者切换它们的角度。这时,一些开始时不可能发现的巧妙抽象就会渐渐浮出水面,而它们恰恰切中问题的要害。

前面的例子大体上是基于一个集装箱航运项目,这是本书列举的几个项目之一,本书还有几个示例会引用这个项目。本书所举的示例都很简单,即使不是航运专家也能理解它们。但在一个需要团队成员持续学习的真实项目中,要想建立实用且清晰的模型则要求团队成员既精通领域知识,也要精通建模技术。

在这个项目中,由于航运从预订货运开始,因此我们开发了一个能够描述货物和运货航线等事物的模型。这是必要且有用的,但领域专家却不买账。他们有自己的考虑业务的方式,这种方式是我们没有考虑到的。

最后,在经过几个月的知识消化后,我们知道货物的处理主要是由转包商或公司中的操作人员完成的,这包括实际的装货、卸货和运货。航运专家的观点是,各部分之间存在一系列的责任传递。法律责任和执行责任的传递由一个过程控制—从托运人传递到某个本地运输商,再从这家运输商传递到另一家运输商,最后到达收货人。通常,在一些重要的步骤中,货物停放在仓库里。在其他时间里,货物则是通过复杂的物理步骤来运输,而这些与航运公司的业务决策无关。在处理航线的物流之前,必须先确定诸如提单等法律文件以及支付流程。

对航运业务有了更深刻的认识后,我们并没有删除Itinerary(航线)对象,但模型发生了巨大改变。我们对航运业务的认识从“集装箱在各个地点之间的运输”转变为“运货责任在各个实体之间的传递”。处理这些责任传递的特性不再是一些附属于装货作业的次要特性,而是由一个独立的模型来提供支持,这个模型正是在理解了作业与责任之间的重要关系之后开发出来的。

知识消化是一种探索,它永无止境。