7、重复的危害
咱们以为,可靠地开发软件、并让咱们的开发更易于理解和维护的惟一途径,是遵循咱们称之为DRY的原则:程序员
系统中的每一项知识都必须具备单1、无歧义、权威的表示。数据库
DRY-Don't Repeat Yourself.编程
不要重复你本身。缓存
与此不一样的作法是在两个或更多的地方表达同一事物。若是你改变其中一处,你必须记得改变其余各处。或者,就像那些异型计算机,你的程序将由于自相矛盾而被迫屈服。这不是你是否能记住的问题,而是你什么时候忘记的问题。服务器
重复是怎样发生的编程语言
咱们所见到的大多数重复均可纳入下列范畴:函数
强加的重复性能
有时,重复彷佛是强加给咱们的。项目标准可能要求创建含有重复信息的文档,或是重复代码中的信息的文档。多个目标平台各自须要本身的编程语言、库以及开发环境,这会使咱们重复共有的定义和过程。编程语言自身要求某些重复信息的结构。咱们都在咱们以为无力避免重复的情形下工做过。然而也有一些方法,可用于把一项知识存放在一处,以遵照DRY原则,同时也让咱们的生活变得更容易一点。学习
信息的多种表示。在编码一级,咱们经常须要以不一样的形式表示同一信息。咱们也许在编写服务器应用,在客户和服务器端使用了不一样的语言,而且须要在两端都表示某种共有的结构。咱们或许须要一个类,其属性是某个数据库表的schema(模型、方案)的镜像。你也许在描写一本书,其中包括的程序片断,也正是你要编译并测试的程序。测试
发挥一点聪明才智,你一般可以消除重复的须要。答案经常是编写简单的过滤器或代码生成器。能够在每次构建软件时,使用简单的代码生成器,根据公共的元数据表示构建多种语言下的结构。能够根据在线数据库schema、或是最初用于构建schema的元数据,自动生成类定义。
代码中的文档。程序员被教导说,要给代码加上注释;好代码有许多注释。遗憾的是,没有人教他们,代码为何须要注释;糟糕的代码才须要许多注释。
DRY法则告诉咱们,要把低级的知识放在代码中,它属于那里;把注释留给其余的高级说明。不然,咱们就是在重复知识,而每一次改变都意味着既要改变代码,也要改变注释。注释将不可避免地变得过期,而不可信任的注释彻底没有注释更糟。
文档与代码。你撰写文档,而后编写代码。有些东西变了,你修订文档、更新代码。文档和代码都含有同一知识的表示。而咱们都知道,在最紧张的时候——最后期限在逼近,最重要的客户在喊叫——咱们每每会推迟文档的更新。
语言问题。许多语言会在源码中强加可观的重复。若是语言使模块的接口与实现分离,就经常会出现这样的状况。C和C++有头文件,在其中重复了被导出变量、函数和(C++)类的名称和信息。Object Pascel甚至会在同一文件里重复这些信息。若是你使用远地过程调用或CORBA,你将会在接口规范与实现它的代码之间重复接口信息。
无心的重复
有时,重复来自设计中的错误。
让咱们看一个来自配送行业的例子。假定咱们的分析揭示,一辆卡车有车型、牌照号、司机及其余一些属性。与此相似,发运路线的属性包括路线、卡车和司机。基于这一理解,咱们编写了一些类。
但若是Sally打电话请病假、咱们必须改换司机,事情又会怎么样呢?Truck和DeliverRoute都包含有司机。咱们改变哪个?显然这样的重复很糟糕。根据底层的商业模型对其进行规范化——卡车底层的属性集真的应包含司机?路线呢?又或许咱们须要第三种对象,把司机、卡车及路线结合在一块儿。无论最终解决方案是什么咱们,咱们都应避免这种不规范的数据。
当咱们拥有多个互相依赖的数据元素时,会出现一种不那么显而易见的不规范数据。让咱们看一个表示线段的类:
1 class Line{ 2 public: 3 Point start; 4 Point end; 5 double length; 6 };
第一眼看上去,这个类老是合理的。线段显然有起点和终点,并老是有长度(即便长度为零)。但这里有重复。长度是由起点和终点决定的:改变其中一个,长度就会变化。最好是让长度成为计算字段:
class line{ public: Point start; Point end; double length() {return start.distanceTo(end);} };
在之后的开发过程当中,你能够由于性能缘由而选择违反DRY原则。这常常发生在你须要缓存数据,以免重复昂贵的操做时。其诀窍是使影响局部化。对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++这样的面向对象语言的一个重要问题。在可能的状况下,应该老是用访问器(accessor)函数读写对象的属性。这将使将来增长功能(好比缓存)变得更容易。
无耐性的重复
每一个项目都有时间压力——这可以驱使咱们中间最优秀的人走捷径的力量。须要你与写过的一个例程类似的例程?你会受到诱惑,去拷贝原来的代码,并作出一些改动。须要一个表示最大点数的值?若是我改动头文件,整个项目就得从新构建。也许我应该在这里使用直接的数字,这里,还有这里,须要一个与Java Runtime中的某个类类似的类?源码在那里(你有使用许可),那么为何不拷贝它、并做出你所需的改动呢?
若是你以为受到诱惑,想想古老的格言:“欲速则不达”。你如今也许能够节省几秒钟,但之后却可能损失几小时。、
无耐性的重复是一种容易检测和处理的重复形式,但那须要你接受训练,并愿意为避免之后的痛苦而预先花一些时间。
开发者之间的重复
另外一方面,或许是最难检测和处理的重复发生在项目的不一样开发者之间。整个功能集均可能在无心中被重复,而这些重复可能几年里都不会被发现,从而致使各类维护问题。
在高层,能够经过清晰的设计、强有力的技术项目领导,以及在设计中进行获得了充分理解的责任划分,对这个问题加以处理。可是,在模块层,问题更加隐蔽。不能划入某个明显的责任区域的经常使用功能和数据可能会被实现许屡次。
咱们以为,处理这个问题的最佳方式是鼓励开发者相互进行主动的交流。设置论坛,用以讨论常见问题。让某个团队成员担任项目资料管理员,其工做是促进知识的交流。在源码树种指定一个中央区域,用于存放实用例程和脚本。必定要阅读他人的源码与文档,不论是非正式的,仍是进行代码复查。你不是在窥探——你是在向他们学习。并且要记住,访问时互惠的——不要由于别人钻研你的代码而苦恼
让复用变得容易
你所要作的是营造一种环境,在其中要找到并复用已有的东西,比本身编写更容易。若是不容易,你们就不会复用。而若是不进行复用,大家就会有重复知识的风险。