前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【五分钟阅读系列】程序员修炼之道——7:重复的危害

【五分钟阅读系列】程序员修炼之道——7:重复的危害

作者头像
放牛的星星
发布2020-07-10 17:25:36
1.1K0
发布2020-07-10 17:25:36
举报
文章被收录于专栏:壹种念头

  给予计算机两项自相矛盾的知识,是James T. Kirk舰长(出自Star Trek,“星际迷航”——译注)喜欢用来使四处劫掠的人工智能生命失效的方法。遗憾的是,同样的原则也能有效地使你的代码失效。

  作为程序员,我们收集、组织、维护和利用知识。我们在规范中记载知识、在运行的代码中使其活跃起来并将其用于提供测试过程中所需的检查。

  遗憾的是,知识并不稳定。它变化——常常很快。你对需求的理解可能会随着与客户的会谈而发生变化。政府改变规章制度,有些商业逻辑过时了。测试也许表明所选择的算法无法工作。所有这些不稳定都意味着我们要把很大一部分时间花在维护上,重新组织和表达我们的系统中的知识。

  大多数人都以为维护是在应用发布时开始的,维护就意味着修正bug和增强特性。我们认为这些人错了。程序员须持续不断地维护。我们的理解逐日变化。当我们设计或编码时,出现了新的需求。环境或许变了。不管原因是什么,维护都不是时有时无的活动,而是整个开发过程中的例行事务。

  当我们进行维护时,我们必须找到并改变事物的表示——那些嵌在应用中的知识胶囊。问题是,在我们开发的规范、过程和程序中很容易重复表述知识,而当我们这样做时,我们是在向维护的噩梦发出邀请——在应用发布之前就会开始的噩梦。

  我们觉得,可靠地开发软件、并让我们的开发更易于理解和维护的惟一途径,是遵循我们称之为DRY的原则:

  系统中的每一项知识都必须具有单一、无歧义、权威的表示。

  我们为何称其为DRY?

提示11: DRY – Don’t Repeat Yourself 不要重复你自己

  与此不同的做法是在两个或更多地方表达同一事物。如果你改变其中一处,你必须记得改变其他各处。或者,就像那些异形计算机,你的程序将因为自相矛盾而被迫屈服。这不是你是否能记住的问题,而是你何时忘记的问题。

  你会发现DRY原则在全书中一再出现,并且常常出现在与编码无关的语境中。我们觉得,这是注重实效的程序员的工具箱里最重要的工具之一。

  在这一节我们将概述重复的问题,并提出对此加以处理的一般策略。

重复是怎样发生的

  我们所见到的大多数重复都可归入下列范畴:

  • 强加的重复(imposed duplication)。开发者觉得他们无可选择——环境似乎要求重复。
  • 无意的重复(inadvertent duplication)。开发者没有意识到他们在重复信息。
  • 无耐性的重复(impatient duplication)。开发者偷懒,他们重复,因为那样似乎更容易。
  • 开发者之间的重复(interdeveloper duplication)。同一团队(或不同团队)的几个人重复了同样的信息。

让我们更详细地看一看这四个以“i ”开头的重复。

强加的重复

  有时,重复似乎是强加给我们的。项目标准可能要求建立含有重复信息的文档,或是重复代码中的信息的文档。多个目标平台各自需要自己的编程语言、库以及开发环境,这会使我们重复共有的定义和过程。编程语言自身要求某些重复信息的结构。我们都在我们觉得无力避免重复的情形下工作过。然而也有一些方法,可用于把一项知识存放在一处,以遵守DRY原则,同时也让我们的生活更容易一点。这里有一些这样的技术:

信息的多种表示。在编码一级,我们常常需要以不同的形式表示同一信息。我们也许在编写客户-服务器应用,在客户和服务器端使用了不同的语言,并且需要在两端都表示某种共有的结构。我们或许需要一个类,其属性是某个数据库表的schema(模型、方案)的镜像。你也许在撰写一本书,其中包括的程序片段,也正是你要编译并测试的程序。

  发挥一点聪明才智,你通常能够消除重复的需要。答案常常是编写简单的过滤器或代码生成器。可以在每次构建(build)软件时,使用简单的代码生成器,根据公共的元数据表示构建多种语言下的结构(示例参见图3.4,106页)。可以根据在线数据库schema、或是最初用于构建schema的元数据,自动生成类定义。本书中摘录的代码,由预处理器在我们每次对文本进行格式化时插入。诀窍是让该过程成为主动的,这不能是一次性转换,否则我们就会退回到重复数据的情况。

代码中的文档。程序员被教导说,要给代码加上注释:好代码有许多注释。遗憾的是,没有人教给他们,代码为什么需要注释:糟糕的代码才需要许多注释。

  DRY法则告诉我们,要把低级的知识放在代码中,它属于那里;把注释保留给其他的高级说明。否则,我们就是在重复知识,而每一次改变都意味着既要改变代码,也要改变注释。注释将不可避免地变得过时,而不可信任的注释比完全没有注释更糟(关于注释的更多信息,参见全都是写,248页)。

文档与代码。你撰写文档,然后编写代码。有些东西变了,你修订文档、更新代码。文档和代码都含有同一知识的表示。而我们都知道,在最紧张的时候——最后期限在逼近,重要的客户在喊叫——我们往往会推迟文档的更新。

  Dave曾经参与过一个国际电报交换机项目的开发。很容易理解,客户要求提供详尽的测试规范,并要求软件在每次交付时都通过所有测试。为了确保测试准确地反映规范,开发团队用程序方式、根据文档本身生成这些测试。当客户修订他们的规范时,测试套件会自动改变。有一次团队向客户证明了,该过程很健全,生成验收测试在典型情况下只需要几秒种。

语言问题。许多语言会在源码中强加可观的重复。如果语言使模块的接口与其实现分离,就常常会出现这样的情况。C与C++有头文件,在其中重复了被导出变量、函数和(C++的)类的名称和类型信息。Object Pascal甚至会在同一文件里重复这些信息。如果你使用远地过程调用或CORBA[URL 29],你将会在接口规范与实现它的代码之间重复接口信息。

  没有什么简单的技术可用于克服语言的这些需求。尽管有些开发环境通过自动生成头文件、隐藏了对头文件的需要,而Object Pascal允许你缩写重复的函数声明,你通常仍受制于给予你的东西。至少对于大多数与语言有关的问题,与实现不一致的头文件将会产生某种形式的编译或链接错误。你仍会弄错事情,但至少,你将在很早的时候就得到通知。

  再思考一下头文件和实现文件中的注释。绝对没有理由在这两种文件之间重复函数或类头注释(header comment)。应该用头文件记载接口问题,用实现文件记载代码的使用者无须了解的实际细节。

无意的重复

  有时,重复来自设计中的错误。

  让我们看一个来自配送行业的例子。假定我们的分析揭示,一辆卡车有车型、牌照号、司机及其他一些属性。与此类似,发运路线的属性包括路线、卡车和司机。基于这一理解,我们编写了一些类。

  但如果Sally打电话请病假、我们必须改换司机,事情又会怎样呢?Truck和DeliverRoute都包含有司机。我们改变哪一个?显然这样的重复很糟糕。根据底层的商业模型对其进行规范化(normalize)——卡车的底层属性集真的应包含司机?路线呢?又或许我们需要第三种对象,把司机、卡车及路线结合在一起。不管最终的解决方案是什么,我们都应避免这种不规范的数据。

  当我们拥有多个互相依赖的数据元素时,会出现一种不那么显而易见的不规范数据。让我们看一个表示线段的类:

代码语言:javascript
复制
class Line {
     public:

      Point  start;


      Point  end;


      double length;


    };

  第一眼看上去,这个类似乎是合理的。线段显然有起点和终点,并总是有长度(即使长度为零)。但这里有重复。长度是由起点和终点决定的:改变其中一个,长度就会变化。最好是让长度成为计算字段:

代码语言:javascript
复制
class Line {


     public:


      Point  start;


      Point  end;


      double length() { return stanceTo(end); }


    };

  在以后的开发过程中,你可以因为性能原因而选择违反DRY原则。这经常会发生在你需要缓存数据,以避免重复昂贵的操作时。其诀窍是使影响局部化。对DRY原则的违反没有暴露给外界:只有类中的方法需要注意“保持行为良好”。

代码语言:javascript
复制
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  = stanceTo(end);


          changed = false;
        }


        return length;


      }


    };

  这个例子还说明了像Java和C++这样的面向对象语言的一个重要问题。在可能的情况下,应该总是用访问器(accessor)函数读写对象的属性。这将使未来增加功能(比如缓存)变得更容易。

无耐性的重复

  每个项目都有时间压力——这是能够驱使我们中间最优秀的人走捷径的力量。需要与你写过的一个例程相似的例程?你会受到诱惑,去拷贝原来的代码,并做出一些改动。需要一个表示最大点数的值?如果我改动头文件,整个项目就得重新构建。也许我应该在这里使用直接的数字(literal number),这里,还有这里,需要一个与Java runtime中的某个类相似的类?源码在那里(你有使用许可),那么为什么不拷贝它、并做出你所需的改动呢?

  如果你觉得受到诱惑,想一想古老的格言:“欲速则不达”。你现在也许可以节省几秒钟,但以后却可能损失几小时。想一想围绕着Y2K惨败的种种问题。其中许多问题是由开发者的懒惰造成的:他们没有参数化日期字段的尺寸,或是实现集中的日期服务库。

  无耐性的重复是一种容易检测和处理的重复形式,但那需要你接受训练,并愿意为避免以后的痛苦而预先花一些时间。

开发者之间的重复

  另一方面,或许是最难检测和处理的重复发生在项目的不同开发者之间。整个功能集都可能在无意中被重复,而这些重复可能几年里都不会被发现,从而导致各种维护问题。我们亲耳听说过,美国某个州在对政府的计算机系统进行Y2K问题检查时,审计者发现有超出10,000个程序,每一个都有自己的社会保障号验证代码。

  在高层,可以通过清晰的设计、强有力的技术项目领导(参见288页“注重实效的团队”一节中的内容)、以及在设计中进行得到了充分理解的责任划分,对这个问题加以处理。但是,在模块层,问题更加隐蔽。不能划入某个明显的责任区域的常用功能和数据可能会被实现许多次。

  我们觉得,处理这个问题的最佳方式是鼓励开发者相互进行主动的交流。设置论坛,用以讨论常见问题(在过去的一些项目中,我们设置了私有的Usenet新闻组,用于让开发者交换意见,进行提问。这提供了一种不受打扰的交流方式——甚至跨越多个站点——同时又保留了所有言论的永久历史)。让某个团队成员担任项目资料管理员,其工作是促进知识的交流。在源码树中指定一个中央区域,用于存放实用例程和脚本。一定要阅读他人的源码与文档,不管是非正式的,还是进行代码复查。你不是在窥探——你是在向他们学习。而且要记住,访问是互惠的——不要因为别人钻研(乱钻?)你的代码而苦恼。

提示12: Make It Easy to Reuse 让复用变得容易

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-05-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 壹种念头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档