5.4 Lava Flow(岩浆流)
|
反模式名称:Lava Flow 别名:Dead Code(死代码) 最常见规模:应用层 重构方案名称:Architectural Configuration Management(架构配置管理) 重构方案类型:过程 根源:贪婪、懒惰 不平衡的力量:功能管理、性能管理、复杂性管理 轶事证据:“哦,那些啊。Ray和Emil(他们已经不在这个公司了)写了那段例程,当时Jim(他上个月离开了)正在尝试绕过Irene(她现在也去了另一个部门)的输入处理代码。我想现在没什么地方还使用它,不过我也不确定。Irene并没有很清楚地给它写文档,所以我们估计现在还是应该让它保持原样。毕竟这东西可以工作,不是吗?” |
5.4.1 背景
在一次数据挖掘的探索中,我们开始寻找为特定类型的系统开发标准界面所需的深入理解。我们挖掘的系统与我们希望最终可以支持正在研究的那些标准的系统非常相似。它也是一个面向研究的系统,而且非常复杂。随着研究的深入,我们与许多开发人员进行了交谈,他们分别和我们打印出的无数页代码中的特定部分相关。我们一再得到相同的回答:“我不知道那个类的目的,我来之前它就写好了。”我们渐渐认识到,这个复杂系统中有大约30%~50%的实际代码没有被当前处理该系统的任何人所理解或写下文档。而且随着我们的分析,我们发现那些可疑的代码实际上在当前系统中没有任何作用;它们只是早已离开的开发人员遗留下来的以前采用过的尝试或方法。当前的开发人员虽然很聪明,但是也不愿意修改或删除不是他们编写的代码,或者不了解其用途的代码。因为他们担心会破坏某些地方而不知道其原因或者不知道该如何修复它。
这时,我们开始称这些代码块为代码“岩浆”,用来说明它刚出现时具有可变的本质,而一旦固化了就像玄武岩一样坚硬而难以去除。我们突然认识到发现了一个潜在的反模式。
在之后大约一年的时间里,我们又进行过几次数据挖掘探索和界面设计工作。我们非常频繁地遇到同样的这个模式,以至于我们整个部门都把它称为Lava Flow(见图5-10)。
图5-10 过时的技术和遗忘的扩展形成的Lava Flow在它的尾迹中留下固化的死代码
5.4.2 一般形式
在开始于研究活动最后以产品结束的系统中,常常可以发现Lava Flow反模式。它的特点是在代码中散布着过去的开发版本形成的类似岩浆的代码“流”,而现在它们固化成像玄武岩一样,不可移动、逐渐失去作用。没有人对它们还有多少印象,或者根本就没有印象了(见图5-11)。这是由于在以前(也许是侏罗纪)项目还处于研究模式下时,开发人员尝试不同方法来完成工作,尤其是为了赶着交付某种演示时,把合理的设计实践都置诸脑后,文档也被牺牲掉了,从而造成了这样的结果。

图5-11 Lava Flow源代码列表
造成的结果是严重破碎的代码、任意改变的类和与系统整体没有清晰关系的过程。实际上,这些流往往外观如同乱麻一般非常复杂,让人以为它们很重要,但没人能够真正解释它们到底做什么或者为什么会存在。有时,某个上了年纪的头发花白的开发隐士会想起某些细节,但更常见的是由于有疑问的代码“并没有真的造成什么破坏,可能实际上是很关键的,我们只是没时间来折腾它”,所有人都决定“让它保持原样”。
“架构是关于如何浪费空间的艺术。”
——Phillip Johnson(美国著名建筑师)
虽然解剖这些岩浆流,研究它们的人类学因素会很有趣,但在进度表上一般不会有足够的时间来绕这样的路。开发人员通常会采取权宜之计,巧妙地绕过它们。
但是,该反模式在概念验证或原型代码迅速发展成产品的创新设计工场中令人难以置信地常见。它是一个不好的设计,以下是几个关键原因:
● 对Lava Flow进行分析、检验和测试的成本很高昂。所有这些工作都是没有价值的,完全就是浪费。实际上,几乎不可能进行检验和测试。
● 读取Lava Flow代码到内存中的代价也很高,会浪费重要的资源,影响性能。
● 和许多反模式一样,你会失去面向对象设计的内在优势。在这种情况下,你失去了在不扩散Lava Flow块的情况下进行模块化和复用的能力。
5.4.3 症状和后果
● 系统中频繁出现无法解释其合理性的变量和代码碎片。
● 没有文档的、似乎重要的复杂函数、类或代码段,它们与系统架构之间没有清晰的关系。
● 非常松散的、“进化中的”系统架构。
● 整段注释掉代码而没有解释或文档。
● 大量“正在变化”或“待替换”的代码区。
● 未使用的(死)代码,简单地留在那里。
● 头文件中存在未使用的、无法说明的或过时的接口。
● 如果移除了现有的Lava Flow代码,由于代码在其他地方被复用,它仍然可能会扩散。
● 如果没有对导致Lava Flow的过程进行检查,而后续开发人员在分析原始流时太匆忙或太不情愿,结果在试图绕过原始流时继续产生新的流,可能会导致产生指数形式的增长。这会让问题更复杂。
● 随着流的复杂化和硬化,很快就会无法为代码编写文档,也无法充分理解它的架构以便加以改进。
5.4.4 典型原因
● 在未考虑配置管理的情况下把研发代码放入到产品中。
● 对未完成的代码进行未受控制的分发。为实现某种功能而实现多种试验方法。
● 单个开发人员(也称为独狼,lone wolf)编写代码。
● 缺乏配置管理或没有充分遵循过程管理策略。
● 缺乏架构或者采用非架构驱动的开发。这一点对临时性很高的开发团队来说尤其普遍。
● 反复性的开发过程。软件项目的目标常常是不清楚的,或者反复发生变化。要解决变化,项目必须返工、倒退和开发原型。为了应对演示的最终期限,常见的做法是用很少的时间地对代码做出匆忙的改变来解决眼前的问题。永远都不会清理代码,让对架构的考虑和文档化工作被无限期地推迟。
● 架构疤痕。有时,会在进行了一些开发后发现在需求分析时做出的架构承诺是不可行的。系统架构可能需要重新配置,但很少会去掉那些内联的错误。注释掉不需要的代码甚至都有可能是不可行的,尤其是在现代开发环境中,可能有数百个文件包含系统的代码。“谁会看所有这些文件?把它们链接进来就是了!”
5.4.5 已知例外
研发环境中的小规模、用后抛弃的原型非常适合使用Lava Flow反模式。在这种环境中,迅速开发是很重要的,其结果并不要求是可维持的。
5.4.6 重构方案
只有一种可靠的方法来防止Lava Flow反模式:保证在产品代码开发前建立健全的架构。该架构必须受到配置管理过程的支撑,这个管理过程要保证开发符合架构规定,而且可以容纳“任务蔓延”(变化的需求)。如果一开始进行架构考虑时采取欺骗的态度,最终会开发不属于目标架构的代码,从而产生冗余的代码或死代码。经过一段时间,死代码就会成为分析、测试和修改时的问题。
在出现了Lava Flow的地方,治疗过程会非常痛苦。重要的原则是在活跃的开发中避免架构变化。这一原则尤其适用于计算体系,也就是定义系统集成方案的软件接口。项目管理过程必须把开发推迟到定义了清晰的架构并让开发人员都了解它之后。定义架构可能需要进行数个系统发现活动。要通过系统发现来找到那些实际使用的、对系统而言不可缺少的构件。系统发现还会确定那些可以被安全删除的代码行。这项活动相当冗长,它可能会需要一个有经验的软件侦探的调查技能。
在消除可疑的死代码时,有可能会产生错误。发生这种事的时候,要避免在充分了解出错原因之前马上修正出现的症状的冲动。要研究问题的依赖关系,这可以帮助你更好地定义目标架构。
要避免Lava Flow,重要的是建立系统层次的软件接口。它应该是稳定的、定义良好的,而且具有清楚的文档。从长远的角度看,与费力地排除坚硬的Lava Flow代码块相比,在软件接口质量上进行先期投入的成本可能要低很多倍。
类似于源代码控制系统的根据可以帮助进行配置管理。大多数Unix环境都捆绑了源代码管理系统,提供了基本的能力来记录受配置控制的文件的更新历史。
5.4.7 示例
我们最近参与了一次数据挖掘探索,试图从最初的接口架构中确定出进化后的接口,该架构是我们原创的,正处于不断更新的过程中。对该系统进行挖掘的原因是因为开发人员使用我们的原始架构的独特方式让我们很着迷:本质上,他们用我们的通用应用间架构建立了一个准事件服务。
在研究他们的系统时,我们遇到了大段代码的阻碍。这些代码段似乎对我们期望发现的整体架构没有贡献。它们在某种程度上缺乏内聚性,而且只有很少的文档或者根本没有。当我们问当前的开发人员关于其中一些部分的问题时,得到的回答是:“那些吗?嗯,我们不再使用那个方法了。Reggie试过某个方法,但是我们提出了更好的办法。不过我想Reggie的其他代码可能会依赖那些内容,所以我们没有删除任何内容。”当我们更深入地研究时,才发现Reggie甚至已经不再参与开发,有一段时间没有来过了,于是那些代码段已经放在那里几个月了。
经过两天的代码检查,我们认识到组成系统的大部分代码与我们研究过的代码非常相似:本质上完全是Lava Flow。我们收集到的有关他们到底是如何构造架构的信息非常少;因此,几乎不可能进行挖掘。这时,我们基本上放弃了尝试挖掘代码,转而关注当前开发人员对“到底”做了什么的解释,希望能够以某种方式把他们的工作编制成接口扩展,让我们可以把它结合到接下来对我们的通用应用间框架的修改中。
我们的解决方法是分离出对他们开发的系统具有最深入理解的单个关键人物,然后和他共同编写IDL。表面上看,我们共同编写的IDL的目的是支持数周后的一次决定性演示。利用Fire Drill小型反模式,我们让系统开发人员为这次演示给他们的产品迅速建立了一个CORBA封装,从而验证了我们的IDL。很多人都熬了不少的夜,但是演示很成功。当然这个解决方法还有另一个作用:我们最终结束于用IDL表示的接口,而它正是我们一开始就想发现的。
5.4.8 相关解决方案
在当前的竞争性世界上,尽量减少研发和产品之间的时间差常常是大家都想达到的目标。在很多行业中,它对公司的存亡至关重要。在处于这种情况下时,有时会在定制的配置管理过程中发现避免Lava Flow的组合措施,这种配置管理过程在建立原型的阶段就设置了一些限制性控制,类似于把它“挂钩”到真实的、产品级的开发中,但没有完全限制研发的试验性本质。在可能的情况下,自动化可以扮演很重要的角色,但是关键之处在于定制一个一旦系统移动到产品环境中,就可以很容易扩展成完整配置管理系统的准配置管理过程。要解决的问题就是在两种成本之间取得平衡,一种是配置管理对创造性过程的阻碍所造成的成本,另一种则是在创造性过程产生了一些有用的、可销售的内容后迅速获得对开发的配置管理控制的成本。
可以通过周期性地把原型系统映射到更新的系统架构中来促进这种方法,系统中应该包括有限的、但是标准化的内联代码文档。
5.4.9 对其他视角和规模的适用性
架构视角在从最开始就防止Lava Flow中扮演了关键的角色。管理者也可以承担在早期发现Lava Flow或发现会导致Lava Flow的环境的职责。这些管理者还必须具有权威,可以在一发现Lava Flow时就踩下刹车,推迟其后的开发直到定义和推广了一个清楚的架构。
与大多数反模式一样,预防的成本总是低于纠正。所以先期投资于良好的架构和团队教育通常可以保证项目避免Lava Flow和大多数其他反模式。虽然这种起始成本不会马上显示出回报,但它的确是一种好投资。
小型反模式:Ambiguous Viewpoint(模糊视角)
反模式问题
面向对象分析和设计模型常常没有澄清该模型所表达的视角。默认情况下,OOAD模型采用了实现视角,而这个视角在很多时候很可能是最没有用的。混合的视角使得在接口和实现细节之间无法进行基本的隔离,而这种隔离本应是面向对象范型带来的首要益处。
重构方案
面向对象分析和设计有三个主要视角:业务视角、规范视角和实现视角。业务视角定义了信息和处理的用户模型。这是领域专家要维护和解释的模型(通常称为分析模型)。分析模型是信息系统最稳定的一些模型,是值得维持的。
如果模型不聚焦于所要求的观察角度,就没那么有用了。观察角度对信息进行了过滤。例如,对电话交换系统的类模型定义会根据下列观察角度所提供的焦点而不同:
q 电话用户,他关心打电话和获取条目化通话账单时的便利性。
q 电话接线员,他关心把用户连接到需要的号码。
q 电话记账部门,他们关心记账规则和记录用户打出的所有电话。
从上面三个观察角度出发会得到一些相同的类,但是不会很多。即使是那些相同的类,它们的方法也不完全相同。
规范视角关注软件接口。由于对象(作为抽象数据类型)的目的是把实现细节隐藏到接口之后,规范视角定义了对象系统中暴露出的抽象和行为。规范视角定义了系统中对象之间的软件边界。
实现视角定义了对象的内部细节。实现模型在实践中常被称为设计模型。必须随着对软件的开发和修改对设计模型不停地进行维护,才能让它成为软件的准确模型。由于过时的模型是没有用的,所以只有选定的设计模型,尤其是那些说明系统的某些复杂方面的设计模型才需要得到维护。





