《Microsoft.NET企业级应用架构设计(第2版)》——2.3 走出混乱

本节书摘来自异步社区《Microsoft.NET企业级应用架构设计(第2版)》一书中的第2章,第2.3节,作者: 【意】Dino Esposito(埃斯波西托) , Andrea Saltarello(索尔塔雷罗)著,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.3 走出混乱

即使做了最好准备,也不管团队的努力如何,系统的设计都可能在某个时刻陷入困境。BBM的形成通常是一个缓慢的过程,会在一段相对较长的时间里使设计恶化。在这个过程里,你的类到处都有修补和变通方案,最终大片代码开始变得难以维护和进化。

这个时候问题就很严重了。

管理者面临与魔鬼交易的抉择,要么采用更多修补和变通方案,要么根据审核的需求和新的架构选择做一次彻底的重新设计。

重新设计一个完整系统与完全从头开始开发之间的区别是什么?就采取的措施而言,区别是极小的,如果存在任何区别的话。但心理层面的选择是不同的。如果要求重新设计,管理层传递的信息是团队正在迎头赶上并且快速修复东西。如果要求重写,管理层则是在承认失败。在软件项目里很少愿意接受失败。

当管理层要求对现有的代码库进行重大整改时,就证明了团队制造了一个软件灾难。现有的代码库就变成了某种令人感到难受的遗留代码了。

2.3.1 有一种奇怪的东西叫作“遗留代码”
在软件里,你通常会继承你必须维护、支持或者使之保持现状并与新的东西整合的现有代码。这种代码通常被称为遗留代码。但是,架构师和开发者所面临的主要挑战不是与现有的遗留代码抗争,而是不要创建更多遗留代码。

遗留代码就是字面意思表达的那种东西。它是代码,它是遗留的。根据牛津词典,遗留这个词是指前任留下或者转交的东西。此外,这个字典还为这个词添加了一个软件特定的定义,意思是某些东西废弃了但因广泛使用而很难替换掉。

我们对遗留代码的一般定义是任何你拥有但不想拥有的代码。遗留代码被继承下来是因为它可工作。设计得好和写得好的可工作代码与设计得差和写得差的可工作代码之间没有根本区别。当你不得不着手处理遗留代码,并通过某种方式维护和进化它时,问题就来了。

遗留代码在某种程度上与软件灾难有关。但是,项目里有遗留代码本身不是灾难。当项目里的遗留代码是你造成的时候,事情就开始恶化了。如何把遗留代码(不管是你的还是从别人那里继承的)变成更可管理的代码,不会妨碍整个项目的进化和按照预期展开呢?
2.3.2 在3招之内将杀(checkmate)
总而言之,从软件灾难恢复类似于从创伤恢复。假设有一次放假,你决定出去长跑,后来遭受一些严重损伤,比如严重的跟腱拉伤。

对于非常活跃的人来说,嗯,这就是一场灾难。

你去看医生,医生进行了一些简单但有效的疗程。第一,医生要求你停止任何物理活动,包括在某些情况下行走。第二,医生在受伤的踝关节附近甚至在整条腿上绑上绷带。第三,医生建议你在感觉好一点的时候尝试走动,如果感觉不舒服就停止。这种方法行得通,很多人都能成功完成这个疗程。

相同的疗程也可以用到软件创伤上,而且通常都行得通。更确切地说,这个策略理论上是有效的,但实际效果取决于创伤的严重程度。

1.停止新的开发工作
在实施把写的烂得代码变成更可管理的东西的任何策略之前都必须先停止系统的开发工作。事实上,添加新的代码只会让设计得烂的系统变得更加糟糕。但是,停止开发工作并不意味着你要停止这个系统的工作。它只意味着,直到你把当前系统重组成更可维护的代码库,可以在现有特性不必做出让步的情况下接受新的特性,才开始添加新的特性。

重新设计一个正在进化的系统就像抓住一个正在乱跑的小鸡。你要有一个非常好的状态才能做到。但是,失败过一次的团队真能在这个时候进入良好的状态吗?

2.隔离痛处
就像医生在疼痛的踝关节附近绑上绷带一样,架构师应该用层包围写的烂的代码块。但是,这里的代码块(Block of Code)具体是指什么?指出“代码块”不是什么会不会更容易?

代码块不是一个类,而是某种跨越多个类的东西,如果不考虑多个模块的话。代码块实际上标识了系统的行为(正在执行的函数),包含了牵涉在内的所有软件组件。关键是标识出宏函数,并为它们每个定义一个不变的接口。你定义一个层实现这个接口并且成为这个行为的外观。最后,你修改代码,使每个需要触发那个行为的地方都通过这个外观(facede)来实施。

举个例子,看一下图2-1。这幅图表示了一个混乱的代码库的一个小的部分包含了两个关键组件,C1和C2,它们与其他组件有太多依赖。
《Microsoft.NET企业级应用架构设计(第2版)》——2.3 走出混乱
这里的目的是把图2-1的布局转成某种带有更多独立区块的东西。你需要理解的是,你不一定在一开始就得到正确的设计。隔离痛点只是第一步,而且你不应该太在意你引入的层的大小。回到医生那个类比,有时候,医生会在整条腿上绑上绷带,即使受伤的地方只是踝关节附近。但是,几天休息之后,医生可能减少绷带,只覆盖踝关节附近的地方。

类似地,隔离软件痛处是一项迭代性的工作。图2-2给出了隔离图2-1的痛处的一种可能的方式。

看到图2-2时,你可能会认为_C_1和_C_2重复了;不过,这只是中间步骤,但为了得到牵涉同一个调用方的两个严格分离的子系统,这是必要的一步。分离的子系统应该像黑盒一样。

值得注意的是,代码块甚至可能跨越逻辑层,有时候也会跨越物理层。就像医生的绷带,减少隔离痛处所覆盖的区域是这项工作的最终目的。
《Microsoft.NET企业级应用架构设计(第2版)》——2.3 走出混乱
术语:
本书将会大量提及领域驱动设计(DDD),尤其从第5章“发现领域架构”开始。我们认为这里提到的隔离区块概念与DDD的绑定上下文概念有着非常重要的关系。在DDD项目里,绑定上下文也可以是一个遗留代码黑盒。在DDD里,绑定上下文有一个与之相关的独特模型,可以由多个模块组成。最终,一些模块可能会共享相同的模型。
3.测试覆盖
一旦你把系统重构成一堆严格分离的区块,你的系统应该还能工作—只是从设计的角度来说更贴切。但是,你不能就此停下脚步。因此,强烈建议你在这个时候引入测试,它们可以告诉你系统在进一步重构之后是否仍然工作。

在这里,测试通常是指集成测试,顾名思义,测试可能会跨越多个模块、逻辑层和物理层。这种测试很难设置,比如说,需要使用专门的仿真数据填充的数据库,需要连接服务,这种测试还要长时间运行。但是,它们是绝对要有的,也是绝对要运行的。在前面提到deFeathers的论文里,他使用了“测试覆盖”这个术语来表示为后续更改定义行为不变性的测试。

注意:
测试在任何重构工作里都扮演着关键角色。任何重构之后,你都肯定需要可以检测是否出现任何回归的测试来结束这个过程。但是,在某些情况下,你可能会发现在你开始隔离痛块之前就准备好测试很有帮助。
术语:
逻辑层(Layer)这个术语通常是指逻辑边界。相反,物理层(Tier)是指物理边界。更具体地说,当我们提到逻辑层时,我们指的是在相同的进程空间里逻辑分离的代码块(即类或者程序集)。物理层意味着物理距离以及不同的进程空间。物理层通常也是一个部署目标。一些代码放到逻辑层还是物理层只是选择和设计的问题。边界才是真正的问题。一旦你有了清晰的边界,你就能决定哪些属于逻辑层,哪些又属于物理层。比如说,在ASP.NET项目里,你可以让一些应用程序的逻辑部署在与核心ASP.NET和页面一样的应用程序池的进程里,也可以部署到在另一台IIS机器上寄宿的不同的物理层里。
4.持续重构
在测试覆盖的首个迭代之后,你应该有一个稳定的系统了,也在某种程度上控制了它的行为。你甚至可能向测试输入一些数据,然后检测出来的行为。接着,你进入重构循环,在这个过程中,你尝试简化你所创建的黑盒结构。在这种情况下,你从黑盒里取出一些代码,然后通过新的接口重用它。来看一下图2-3。
《Microsoft.NET企业级应用架构设计(第2版)》——2.3 走出混乱
如你所见,C1和C2现在已经从子系统移出来,并且封装到一个新的可测试的黑盒里。重复这个过程,你可以逐渐减少黑盒的大小。一个好的影响是,现存的集成测试现在可以重写成更加接近单元测试的形式了。

2.3.3 决定是否添加人手
Frederick P. *s在他的《The Mythical Man Month》(Addison-Wesley,1995)里有一句名言:向一个已经延迟的项目添加人手只会使之更加延迟。确实是这样,这样做不可能对日程安排有太大影响(这句是我们的)。然而,当项目延迟时,第一个涌进脑海的就是增加劳动力。但是,项目的活动有顺序的约束(比如说,调试要在开发之后),添加劳动力根本没有任何好处。根据*s的说法,孕育一个孩子需要9个月,9个女人不可能在一个月内生出一个孩子。

所以问题就变成:当项目延迟时你应该做什么?你永远都不应该考虑添加人手吗?这取决于项目延迟原因的实际分析结果。

1.需要更多时间
项目延迟的明显因素是每个特性需要的时间比预计的多。如果任务本身是顺序执行的,团队有更多的人意味着需要更多的管理工作,可能导致分配的资源没有达到最佳效用。

这个现实把焦点转移到估算上。软件人员的工作量很难准确估算。软件人员通常会认为事情最终会好起来,一切只需再多几个小时的工作就可以修复。这种态度也使监督进度变得困难。本章最开始的引言已经表明一切—一个项目每次延迟一天。如果进度可以及时跟踪,进度落后不需要很长时间就可以得到修复。在最坏的情况下,这个额外的时间可以分摊到一段更长的时间里。

但即使技术工作量的估算是正确的,另一个方面也常常被忽略。任何项目都有直接成本和间接成本。直接成本包括薪水和差旅费。间接成本包括设备和行政事务。此外,还有一些成本无法确知的东西:会议,修订,以及所有没和每个人沟通或者没被完全理解的小任务。估算疏漏是很常见的,而疏漏只能通过经验弥补—你的直接经验或者这方面专家的直接经验。很明显,你应该总是尽力清楚地写下每个需要完成的小任务,并且把它们的时间考虑进来。

一个务实的方案是在项目完成时总是比较一下实际成本和估算。如果发现有出入,把它换算成百分比,不管是低了还是高了,下次你可以使用这个因子去乘以你的估算。

一般而言,根据实际的工作模式,软件项目有两个主要的变化模式:固定价格或者时间/物资。对于前面那种情况,如果你意识到需要更多时间,作为一名项目经理,你可能会尝试寻找一种方式来重新调整计划安排。你尝试降低质量—缩减开发时间和测试,减少文档和任何对通过本次迭代的最终测试并非完全必要的活动。你也可能尝试重新商议,仔细回顾需求,看看是否有任何已经达成协议的东西可以重新界定为需求更改。如果你使用时间/物资模式,你可以设法计算过去迭代里估算时间和实际时间之间的差值,然后用它来修正每个新的迭代的估算。

2.需要更多专业技能
项目延迟的另一个原因可能是某些人并不胜任这项任务导致实现某些特性需要更长时间。如果你需要更多专业技能,不要害怕把你能找到的最好人才带进来。但是,当你需要有才能的人时,应该清楚为什么你想要把他们带进来。你可以找专家给你的人做培训,或者找他们解决问题。没有哪个做法比另一个更好。

培训带来附加价值,因为培训的结果留在公司里面,期待增值作用。同时,培训(即使提供专门定制的课件)针对的主题通常在一个比较通用的层次,需要一些额外的时间才能在当前项目里采用。另一方面,寻求咨询理论上更有效,但你要允许专家完全接触代码、人员和文档。代码库越是错综复杂,所需的时间可能越长,结果的可靠性也可能越低。

专家不会变魔法,魔法在软件里并不存在。如果一个专家会变魔法,它可能只是戏法。生活中也是如此。

上一篇:6、PXE安装ESXI6.0


下一篇:【转】Android四大基本组件介绍与生命周期