本节书摘来自异步社区《Microsoft.NET企业级应用架构设计(第2版)》一书中的第2章,第2.1节,作者: 【意】Dino Esposito(埃斯波西托) , Andrea Saltarello(索尔塔雷罗)著,更多章节内容可以访问云栖社区“异步社区”公众号查看
第2章 为成功而设计
我们认为成功的软件项目是在充分了解业务需求的情况下采用靠谱解决方案的项目。我们认为成功设计的软件是在项目成功的前提下能够(在任何可能的地方)重用现有代码和基础设施,并根据可用的技术和广为人知的最佳实践不断改善的软件。
今天,成功设计的软件对于任何类型、任何规模的商业来说都是至关重要的,但更为关键的是避免质量低下的软件。烂的软件会使组织在很多地方遭受损失,比如说,响应很慢的页面会导致访问者离开你的网站,笨拙的用户界面会带来入口瓶颈,导致你提供的服务不得不面对处理队列,甚至未处理异常也会触发不可控的连锁反应,造成不可预测的后果。
软件项目很少符合预期。我们觉得每个手捧本书的读者都对这条表述有所了解。那么,什么妨碍了软件设计的成功?如果我们要对造成软件项目无法完全满足预期的原因寻根究底,我们将会无可避免地触及“大泥球”(BBM)。
BBM是一个用来描述“软件灾难”的优雅说辞。
在我们的定义里,软件灾难是指系统的发展出了问题,不受控制,并且很难修复。有时候,设计有问题的行业系统打上补丁也能勉强工作,但最终会变成遗留代码等候其他人处理。一般而言,我们认为团队应该总是把成功设计软件作为目标,即使“磁带盒”的故事能够名垂青史。事实上,根据Space.com的报道,1961年进入太空的第一个宇航员Yuri Gagarin在发射之前按照指示撕开了一个磁带盒,并对某个齿轮做了调整。
注意:
BBM的一个最新的绝佳案例是HealthCare.gov。它由超过50个供应商组成的集团构建,理论上归美国联邦*管治。实际上,从外面来看,没有任何一个参与构建的供应商对整体质量进行负责。大多数组件没有经过集成测试,甚至不同组件之间的对接也没有及时测试。到最后,如果有人对项目的做法有所顾虑,要么被直接忽略,要么用商业理由或期限紧迫忽悠过去。但最终,他们还是千方百计让网站运行起来了。
2.1 “大泥球”
大泥球(Big Ball of Mud,BBM)这个词几年前就有了,它是指一个系统几乎没有组织,到处都有隐藏的依赖关系以及大量重复的数据和代码,各层和关注点也没有清晰标识,也就是意大利面代码丛林。这个词是由伊利诺伊大学的Brian Foote和Joseph Yoder创造的,在他们的论文里有讨论。
在这篇论文里,作者没有把BBM指责成最糟糕的实践;他们只是建议架构师和开发者时刻准备应对BBM风险,以及学习如何控制它。换句话说,几乎任何超过一定规模的软件项目都会面临BBM威胁。学习如何识别和处理BBM是避免软件灾难的唯一途径。
2.1.1 “大泥球”的成因
关于BBM入侵软件项目有几个基本事实。首先,也是最重要的,BBM并非一夜形成,起初也没有那么大。其次,没有单一开发者可以从头开始创建BBM。BBM总是团队的产物。
为了找到问题的根源,我们给出几个可能导致BBM的主要成因,通常发生在协作中。
1.未能捕获客户的所有需求
架构师和开发者构建软件,特别是企业软件,都有清晰的目的。软件的目的是通过高级声明来表达的,里面包含了客户想要达到的目标以及想要解决的问题。软件工程科学有一个完整的分支处理软件需求,并把需求划分成不同层次—业务需求、利益相关者需求、功能性需求、测试需求,或许还有更多。
关键是你怎么把一长串表述粗略的需求变成通过编程语言编码的具体特性。
在第1章“今天的架构师和架构”里,我们把确认需求列为架构师的主要职责之一。需求通常来自多种渠道,体现相同系统的不同视角。因此不必惊讶于某些需求相互矛盾,或者某些需求在不同的利益相关者的眼里有着明显不同的重要性。分析需求以及决定哪些需求直接对应某个特性只是架构师工作的第一阶段。
当入选的特性列表提交验证时就会进入第二阶段。建议的特性列表必须满足所有利益相关者的完整需求列表。某些利益相关者的某些需求被砍掉是可以接受的。
是的,可以接受,只要你可以合理地解释为什么砍掉那些需求。
为了设计系统解决问题,你必须完全理解这个问题及其领域。这不一定马上就能成事,也不是随便读一下需求就能成事。有时候,你不得不说“不”。大多数情况下,你不得不问“为什么”,然后讨论添加一个新的特性支持一组特定的需求有何利弊。
我们在过去几年里得到的教训是,根据不完整的需求理解来写的代码,只要能跑起来也比花几天时间寻找一个完美的解决方案更有帮助。就这点而言,敏捷开发方式更多是基于常识而不是理论。
确认需求需要沟通以及沟通技能。有时候沟通就是无法奏效。有时候双方相信的东西是都错的,然后双方最后都得尝试“救火”。于是,开发者学会抱怨没有得到足够的细节,而业务人员反驳说每一条都在文档里详细列明。
沟通问题的根源是业务人员和开发者使用不同的词汇,使用和期待不同精度的描述。此外,除了开发者,几乎每个人都认为编程远比实际的容易。添加一个新的需求对于业务和销售人员来说就像在文档里添加一行新的内容那么简单。实际上,系统的某些适应性是必要的,但会有附加代价。
因为新增或者修改需求产生的软件适应性是有代价的,但没人愿意付出这个代价,所以某些特性的适应性是通过删除其他特性来实现的,或者更常见的情况是,通过砍掉重构、测试、调试和文档来实现。当这种情况出现时,其实也经常出现,大泥球就会产生。
重要:
在上一章里,我们讨论了我们通常如何处理需求。我们想在这里简要回顾一下。基本上,我们把原始需求分门别类,使用ISO/IEC的分类作为起点。实际的分组流程就像在Microsoft Office Excel工作表里为每个分类创建一个标签那样简单。接着,我们检查各个标签,在里面添加或删除需求,这样做更有效。我们也会细心检查几乎是空的标签,尝试深入了解那些方面。最后,这个流程使我们更主动地寻求更多、更清晰的信息,从已知的数据里提取或者索要更多细节。
2.在系统发展时坚持RAD
一开始,项目看起来很容易管理。客户说这个项目不会发展成为一个大的复杂的系统。因此,你可能会选择某种形式的快速应用程序开发(Rapid Application Development,RAD),不太注意能让应用程序随规模更好向上扩展的设计方面。
如果事实证明系统会发展,RAD方案就会显示出它固有的局限性。
虽然RAD方案对于以数据为中心的小型简单应用程序(如CRUD应用程序)来说可能刚好合适,但事实证明它对于包含大量经常改变的领域规则的大型应用程序来说是一个危险的方案。
3.不准确的估算
业务人员总是想在他们确认委托和打开钱包之前准确地了解他们将会得到什么。但是,业务人员是根据高级特性和行为来理解的。一旦他们说他们想要网站只对验证用户开放,他们相信自己已经把这个问题说清楚了。他们不认为有需要指明用户应该可以通过一大堆社交网络登录。如果后面说这点没有提到,他们会发誓已经写在文档里了。
相反,开发者想要准确地了解他们需要构建什么,以便做出合理估算。在这个阶段,开发者根据具体细节思考,把业务特性分成更小的部分。问题是,只有清楚定义和确认所有需求才能准确估算。而估算会因需求改变而改变。
不确定性占主导地位。以下是一个常见的场景。
业务团队和开发团队就特性和安排初步达成协议,每个人都很满意。开发团队要先提供一个原型。
开发团队交付原型,安排一个演示会议。
这个演示会议让大家对正在构建的系统有了更加具体的了解。业务人员在某种程度上也拓宽了他们对系统的视野,并要求做出一些改变。
开发团队很乐意添加更多项目内容,但需要额外的时间。结果,构建这个系统变得更加昂贵。
但是,对于业务人员来说,这是同一个系统,没有理由付出不同代价。他们所做的只是把精确度提高一个水平!
当你做估算时,指出哪里存在不确定性是极其重要的。
4.缺乏及时的测试
在软件项目里,测试会出现在各种层次。
你有单元测试,判断软件的每个组件是否满足功能性需求。在重构代码时,单元测试对于发现功能的回归也是很重要的。
你有集成测试,判断软件是否兼容环境和基础设施,以及两个或多个组件能否协同工作。
你有验收测试,判断完成的系统,包括所有功能,是否满足客户的所有需求。
单元测试和集成测试与开发团队有关,目的是让团队对软件的质量有信心。测试结果可以告诉团队是否做对以及做好。
就单元测试和集成测试而言,一个关键的方面是测试在什么时候写和运行。
就单元测试而言,有一个广泛的共识是,你应该一边写代码一边测试,并把测试的运行与构建流程整合起来。但是,运行单元测试通常比运行集成测试更快。集成测试可能需要更长时间来设置,每次运行之前也可能需要重设。
在一个项目里,你从其他个人开发者或者团队得到一些组件,这些组件一开始可能没有办法很好地协同工作。有鉴于此,最好把集成工作逐步摊分到整个项目,这样能使问题尽早显现。把集成测试放到最后会导致很大风险,因为这会导致你没有时间在不引入补丁以及在补丁之上再引入补丁的情况下修复问题。
5.不明确的项目所有权
HealthCare.gov这个案例告诉我们,很多供应商一起构建的系统必须有一个明确的项目所有权。
拥有这个项目的供应商或者个人需要对整体质量负责,他的职责包括检查每个部分是否达到最高质量以及兼容系统的其他部分。这号人物可以推动测试及时完成,以便尽早发现集成问题。与此同时,这号人物还可以协调团队与利益相关者之间的安排和需要,以便每个供应商都能在不损害其他合作伙伴的情况下完成自己的任务。
当项目的领导关系没有明确定义,或者像HealthCare.gov那样定义了但没有严格执行时,对项目负责就只能依靠个别供应商的美好意愿了,但每个供应商都没有理由处理他们合同以外的事情。尤其在压力之下,更容易只关注眼下可工作的东西,而不顾及整合和设计等方面。
当这一切发生时,即使只是几个迭代,意大利面代码也会产生。
6.忽略“危机”状态
技术困难在软件项目里是常见的事物,而不是新奇的事物。
面对这种困难,保持含糊和安抚客户都是没有意义的。即使项目顺利完成,隐藏问题也不会让你得到额外的奖励。但是,如果项目失败了,隐藏问题肯定会为你带来很多额外的麻烦。
作为一名架构师,你应该尽力做到开源软件那样开放、坦率。
如果你识别出某种危机,让他们知道,告诉他们你在做什么以及打算做什么。就修复提供详细的计划可能是工作的最难部分。但是,利益相关者需要知道发生什么事,以及明确团队朝着正确方向前进。有时候,详细计划的更新和已经完成的工作量足以避免你的压力增大到超出你能承受的范围。
如何识别出与大泥球有关的“危机”状态和成功项目?
再次说明,这是常识问题。对困难保持开放和坦率会为你敲响警钟,这样不好的事情就有可能在无可挽回之前被制止。
2.1.2 “大泥球”的征兆
毫无疑问,作为一名架构师,项目经理,或者两者兼有,你应该尽最大努力避免BBM。但是,有没有一些清晰不含糊的征兆可以表明你正处在泥球滚动的轨道上呢?
下面给出一些普遍存在的迹象,它们会提醒你设计是否朝着有问题的方向发展。
1.僵硬,因而脆弱
你可以掰弯一块木头吗?如果你保持这样做会有什么后果?
一块木头通常是一种僵硬物质,具有抵抗变形的特点。当施加足够的力时,就会造成永久变形,木头也会断裂。
僵硬的软件呢?
僵硬软件具有某种抵抗变化的特点。抵抗程度是根据回归来衡量的。你对一个类做出改变,改变的影响会顺延到一组依赖的类。结果很难预计一个改变(任何改变,即使是最简单的)实际将会耗费多长时间。
如果你敲打一块玻璃,或者任何其他易碎物质,你会把它弄成碎片。类似地,当你在软件里引入一个更改并且把它分散到多个地方时,这个软件毫无疑问就会变得“易碎”。
就像在现实生活里一样,易碎性和僵硬性在软件里也是成对出现的。当改变软件里的一个类导致(很多)其他类因为(隐藏的)依赖遭到破坏时,你就很清楚地看到坏设计的征兆了,你需要尽快修复。
2.易于使用,但不易于重用
假设你有一个软件在一个项目里工作良好,你想在另一个项目里重用它。但是,在新的项目里复制这个类或者链接这个程序集并不管用。
为什么会这样?
当你把相同的代码移到另一个项目却不管用时,通常是因为依赖性或者它的设计没有考虑共享。二者之中,依赖性是最大问题。
真正的问题并不仅仅是依赖性,还有依赖的数量和深度。为了在另一个项目里重用一块功能,你不得不导入更多功能。到了最后,不再进行任何重用,代码会从头开始重写。
这也不是好设计的迹象。这种负面设计效果通常被称为不可移动性(Immobility)。
3.易于变通,不易于修复
在修改一个软件的类时,你通常会找到两种或更多方式来做。大多数情况下,一种方式绝妙、优雅,并且符合设计,但实现起来很困难,也很辛苦。相反,另一种方式编码起来很顺畅,也很快速,但它只是一种修补。
你应该怎么做?
事实上,两种方式都可以用来解决问题,取决于给定的期限以及你的经理的安排。
一般而言,变通方案(workaround)比正确的解决方案看起来更快更易实现并不是一个理想的情况。事实上,问题并不仅仅是单纯的额外劳动。有时候,你就是害怕基本变通方案之外的选择。回归才是真正让你感到害怕的东西。如果你有好的充足的单元测试,至少你可以肯定任何回归一旦出现都能很快捕获。但接着你又开始想,单元测试又不能修复代码,或许变通方案就是解决问题的最快方式了!
一个特性更易修补(hack)而不是修复(fix)对于你的整个设计来说并不是一个很好的评述。它只是意味着类之间存在太多没有必要的依赖,而你的类也没有形成特别有凝聚力的代码。因此,这已经足够吓唬你远离正确的解决方案了,它可能比快速修复更有强迫感,也需要更深层次的重构。
这种负面设计效果通常被称为粘稠性(Viscosity)。
2.1.3 使用指标检测BBM
文字上,另一个经常用来表达在烂代码上构建软件密集型系统的词语是技术债务(Technical Debt,TDBT)。
Ward Cunningham提出的债务比喻非常形象,正如财务债务,烂代码也会随着时间增长,累积需要偿付的利息。从长远来说,TDBT会变成一个重担,影响甚至妨碍后续的偿付措施。改编温斯顿·丘吉尔爵士的一句名言,我们可以说,“对于开发团队来说,TDBT就像一个站在木桶里的人尝试通过手柄把它抬起。”
TDBT是一个抽象的概念,要有工具来测量甚至去除,就像要求你会施展魔法一样。但是,仔细观察一些事实,并对此进行一些分析总结出一些指标。虽然结果不一定就是你想要的,但至少它们提高了你的警觉。
下面来看一些指标和工具,它们可以帮你判断BBM是否在试图咬你。
1.静态代码分析
一般而言,团队的人知道大多数问题出在哪里,但有时候你需要提供与代码有关的问题的证据,而不是仅仅在口头上提及它们。静态代码分析工具为你扫描和探测代码,为日后讨论提供一份有用的报告。
Microsoft Visual Studio 2013有它自己的静态代码分析器,可以计算代码覆盖率和圈复杂度(Cyclomatic Complexity)。其他类似的框架包含在CodeRush和ReSharper等产品(本章稍后会有讨论)里,或者同一个供应商也提供独立的产品。
一个有趣的静态分析工具是NDepend,它也可以为你创建依赖图,便于查看最有问题的区域的数量和位置。
重要:
不管看起来怎么样,静态代码分析工具既不会告诉你技术债务的根源是什么,也不会告诉你需要做什么才能减少它。它们只是为你的决定提供输入,而不是为你做决定。
2.知识孤岛
另一组可以用来识别某种TDBT的有趣指标是团队的瓶颈技能的数量。如果团队只有一个人拥有某些技能,或者负责某个子系统或者模块,一旦这个人离开公司或者突然不可用了就会出大问题。
知识孤岛或者信息孤岛通常用来描述代码的所有权落在个人的肩膀上。这对于个人来说可能不是问题,但对于团队来说绝对是个问题。
术语:
我们刚才使用的两个术语在不同的团队里可能有不同的含义。这些术语是模块和子系统。在这里,我们只是用这些术语来表示代码块,除此之外没有其他特别的含义。子系统通常是指整个系统的一个子集;模块则是子系统的一个部分。