秋梦无痕

一场秋雨无梦痕,春夜清风冻煞人。冬来冷水寒似铁,夏至京北蟑满城。

Avatar

重复之邪

重复之邪
――《实用程序员:从学徒到大师》
给计算机两条自相矛盾的知识,这是James T. Kirk船长【译者注:美国科幻电视连续剧Star Trek中人物】偏爱的一种让疯狂的人工智能装置失效的方式。不幸的是,同样的原理可以有效地击倒你的程序。
作为程序员,我们收集,组织,维护和利用知识。我们用文档把知识融于规格说明当中,使它在运行的程序中活跃起来,在测试中用它来提供所需的检验。
不幸的是,知识不是恒定的。它总在变化——常常是骤然的。随着一次和用户的会谈,你对于一条需求的理解就变了。政府改变了一条规章制度,某商业逻辑随之就过时了。测试可能表明所选择的算法不敷实用。所有这些不稳定性意味着,我们的时间大部分化在了维护工作,以及重新组织和重新表达系统中的知识上。
大多数人想当然地认为维护始于应用程序发布之时,那种维护意味着除错和增强功能。我们认为这些人错了。程序员总是不停地在维护。我们的理解日复一新;我们还在设计或者编程的时候新的需求就到了;可能环境总是处于变化之中;不管是何种缘由,维护不是一种断续的活动,而是整个开发过程中的每日例行公事。
在我们执行维护的时候,我们得找到和改变事物的表达——那些包含于系统中的知识汇聚块。问题在于实在太容易在我们开发的规范,过程和程序中复制知识了,而且一旦我们这样做,我们就引入了管理梦魇——这梦魇早在应用发布产品之前很久就开始了。
我们觉得,可靠地开发软件,以及使我们的开发更易于理解和维护的唯一方式,就是遵循我们称之为DRY的法则:
在一个系统里每一条知识都必须有一个单一的,无二义的,权威的表达。

为什么我们要称之为DRY呢?

诀窍 11:Don’t Repeat Yourself. (不要重复你自己。)

反之就是在两个或两个以上的地方表达同一事物。如果你改变一处,你就得记得去更改其它,否则,就像那个外星计算机一样,你的程序将由于自相矛盾而崩溃。你是否能记得不会是一个问题,问题在于何时你会忘记。
贯穿全书你将发现DRY法则会时不时出现,甚至会出现在和编程无关的上下文中。我们觉得这是实用程序员工具箱中最重要的工具之一。

在本节中我们将略述重复之难题,并提出一些应付的一般策略。

重复是如何产生的?

我们所见的大部分重复可归于下列种类:
· Imposed Duplication (强加的重复):开发人员感到他们别无选择——系统要求重复;
· Inadverent Duplication (无心的重复):开发者未意识到他们在重复信息;
· Impatient Duplication (偷懒的重复):开发人员因偷懒而复制,因为这看起来容易些;
· Interdeveloper Duplication (开发者间重复):一个(或多个)组里的多个人复制同一条信息。

让我们更详细地看看这四个I的重复。

强加的重复

有时候,重复看起来是强加于我们的。项目标准可能要求文档包含重复的信息,或者复制代码中的信息。多目标平台每一个都要求其各自的编程语言,程序库和开发环境,这就要我们复制共享的定义及过程。编程语言本身要求某种重复信息的结构。我们都曾在我们感到无力避免重复的情形下工作。尽管如此常常还是有办法保持每一条信息在一个地方,尊奉DRY的法则,同时还能让我们的生活轻松一点。
下面是一些技巧:

信息的多种表示
在代码级别,我们通常需要以不同的形式表示同一信息。可能我们在写一个用户-服务器应用,在服务器和客户端分别使用不同的语言,而需要在两者中都表达某个共享的结构。也许你在写一本书,引用了某个随后你要编译和测试的程序片断。
略施小技一般你就能够消除重复的需要。答案通常是写一个简单的代码过滤器或者生成器。不同语言的结构可以使用一个简单的代码生成器在每次创建软件的时候从一个通用的元数据表示中生成。类定义可以从在线数据库模型(Schema)中自动生成,或者从最先用于生成模型的元数据中产生。本书摘录的代码是由一个预处理器在我们每次格式化文本的时候插入的。关键在于让该过程成为主动的:这不能是“一次过”转换,否则我们又回到复制数据的情形。

代码中的文档
程序员被告知要注释他们的代码:好的代码有大量注释。不幸的是,他们从未被告知为何代码需要注释:坏的代码需要大量注释。
DRY法则告诉我们要把最低级的知识保留在代码里,那里是它们的归宿,而把文档留给其它高级的解释。否则我们就是在重复知识,而每一次修改就意味着同时修改代码和注释。注释将不可避免地过时,而不可信的注释还不如根本没有注释。(更多的有关注释的信息参见“一切皆著述”一节。)

文档与代码
你先写文档,后写代码。事情有变,你则先改进文档再更新代码。文档和代码都包含了同一知识的表达。而且我们都知道,在紧急的时刻,比如大限已隐约可见,或者重要客户在大声要求,我们都倾向于推迟文档的更新。
有一次大卫致力于一台国际电传交换机。相当可以理解地,其用户要求一个详尽无遗的测试规范,并且要求每次交付软件都要通过所有的测试。为了保证测试能够反映规范,开发小组编程从文档本身自动生成测试。当用户改进其规范的时候,测试集合自动更新了。一旦开发小组说服用户该过程是可靠的,一般数秒钟就可生成验收测试。

语言问题
很多语言在源码中强加很多可观的重复。通常这出现在该语言从一个模块的实现分离出其界面的时候。C和C++有头文件,复制了外部可见的变量,函数和(C++下)类的名字和类型信息。Object Pascal语言甚至在同一文件中重复这一信息。如果你在使用远程过程调用或者CORBA,你将在界面规范和实现代码中重复界面信息。
没有简单的技术能够克服语言的要求。虽然一些开发环境会通过自动生成来隐藏对头文件的需求,而Object Pascal语言允许你简写被重复的函数声明,一般你就只能圄于已有的条件了。至少在大多数基于语言的问题上,一个和实现不一致的头文件会产生某种形式的编译或者链接错误。你还是会出错,但是至少你在相当早的阶段就得到知会。

考虑一下在头文件和实现文件中的注释。在两个文件之间复制函数或者类的标题注释是没有任何意义的。在头文件中注释界面问题,而在实现文件中注释那些你的代码的用户不必知道的真相。

无心的重复

有时,重复是由设计中的错误所致。
让我们看一个配送业的例子。假设说我们的分析揭示,在众多属性中,一辆货车有一个类型,一个许可证号,以及一个司机。类似的,一条递送路线是一条路线,一辆货车和一个司机的组合。基于这些理解,我们编写了一些类的代码。
但是当Sally打电话来请病假而我们不得不换司机的时候会发生什么?Truck类和Delivery Route类都包含一个司机,我们更改哪个?显然这一重复是糟糕的。根据支撑事务逻辑来正交化设计——一辆货车真的需要一个司机作为其构成属性集的一部分么?一条路线呢?或者需要第三个对象把货车,司机和路线结合在一起?不管最终解决方案如何,避免这类非正交的数据。
如果存在互相依赖的数据元素的时候,数据间的非正交性就不显得那么明显了。让我们看一个表达线段的类:

class Line {
public:
Point start;
Point end;
double length;
};

乍一看去,这个类似乎相当有理。一条线段显然有首端和末端,也总会有一个长度(即使长度为零)。但是这样就重复了。长度是由首末端定义的:改变其中一点则长度就得随之而变。最好把长度变成一个计算域:

class Line {
public:
Point start;
Point end;
double length() { return start.distanceTo(end); }
};

随后在开发的过程当中,你可能会为了性能的原因而选择违反DRY法则。这常常会发生,因为你需要缓冲数据以避免昂贵的操作。其中关窍在于把影响局限于本地。违例不会被暴露到外部世界去:只有类内的方法才需要关心如何保持操作无误。

class Line {
private:
bool changed;
double length;
Point start;
Point end;

public:
void setStart(Point p) { start = p; changed = true; }
void setEnd(Point p) { end = p; changed = true; }

Point getStart(void) { return start; }
Point getEnd(void) { return end; }

double getLength() {
if (changed) {
length = start.distanceTo(end);
changed = false;
}
return length;
}
};

这个例子亦例证了对于面向对象的语言例如Java和C++中的一个重要问题。只要有可能,就总是使用存取函数来读写对象的属性。这使得将来在增加例如缓冲这样的功能时更加轻松。

偷懒的重复

每个项目都有时间压力——一种能驱使我们当中最好的都抄捷径的力量。需要一个类似于你写过的程序的函数?你会被诱使去复制原件然后稍加改动。需要一个值来表达最大的点数?如果改变了头文件,整个项目就要重新编译。也许我应该在这儿,这儿,还有这儿各用一个实际数字?需要一个类似于Java运行库中的类?源码都提供了,干吗不干脆拷贝一份然后进行所需的修改(先不管许可条款)?
如果你感受到了这一诱惑,请记住那句常见的警句:欲速则不达。你可能当下能节省若干秒钟,却要承受将来数以小时计的损失。想一想围绕着千年虫的问题就知道了。许多这样的问题就是由于开发者懒得把数据域的大小参数化或者未能实现数据服务的中心库而造成的。

偷懒重复是易于识别和处理的形式,但是需要纪律和主动的努力去预支时间以挽救将来的痛苦。

开发者间重复

另一方面,一个项目中不同开发者间的重复大概是最难以检测和处理的重复了。一整套功能集都可能被无意中重复了,而该重复可能无人注意达数年之久,从而导致管理上的棘手难题。我们从第一手资料知道,一个美国州政府的计算机系统被审查其Y2K规范问题, 该审计清查了超过1,0000个程序,每个都包含一个确认社会安全号码的程序的自身版本。
在高级层次上,处理这类问题可以通过有一个清晰的设计,一个强有力的项目技术领袖(见“实用主义团队”一节),以及设计中职责的明确分工来实现。然而,在模块水平上,问题则更加隐蔽。那些不明显属于所划分职责范围的但是又经常需要的功能或者数据就会被多次实现。
我们觉得解决该问题的方法就是鼓励开发者间活跃的经常性的交流。建立一个论坛以讨论一般性问题。(在过去的项目里,我们曾建立起私有的Usenet新闻组让开发人员交流想法和提出问题。这提供了交流的一种非打扰性方式——甚至可以跨越多处地点——同时还能保留一切言说的永久性历史。)指定一个小组成员作为项目的“图书管理员”,他的工作就是促进知识的交流。在源码树中腾出一块集放地,用于存放工具程序和脚本。还要强调阅读别人的代码和文档,不管是非正式的还是在代码互评中。你不是在剽窃——你是在向他们学习!而且要记住,这个过程是互惠的——你也不必因为其它人在阅读你的代码而感到到苦恼。

诀窍12:使其易于重用。

你要做的就是培养一个环境,在其中找到和重用已有的事物要比自己动手写容易。如果不容易的话,人们就不会去做!如果你不能重用,你就在冒重复知识的危险。

相关章节
· 正交性
· 文本处理
· 代码生成器
· 重构
· 实用主义团队
· 无处不在的自动过程
· 一切皆著述

评论已关闭