程序设计 程序设计
首页 U 开头 本地查找local find 不用本地查找帧, 全屏 Google 搜索特定话题的网页 jump to footer translate with Babelfish by Roedy Green ©1996-2008 Canadian Mind Products  翻译:SuperMMX ©2003-2008 自由 SuperMMX
“The cardinal rule of writing unmaintainable code is to specify each fact in as many places as possible and in as many ways as possible. ”
“编写不可维护代码的主要原则就是:在尽可能多的地方,以尽可能多的方式来指明每一个事实。”
~ Roedy
  1. 编写可维护代码的关键就是只在一个地方指定应用程序的每个事实。要改变想法,只需要在一个地方修改,就能保证整个应用程序还能工作。因此,编写不可维护代码的关键就是:在尽可能多的地方,以尽可能多的各种方式一遍又一遍地指定同一个事实。令人高兴的是,象 Java 这样的语言,不辞辛苦地让编写这样不可维护的代码如此容易。例如,要改变一个广泛使用的变量的类型几乎是不可能的,因为所有的造型和转换都不能运行了。与这些类型相关联的临时变量也都不再正确。更进一步,如果这个变量要显示在屏幕上,那么就必须追踪所有相关的显示和数据输入代码,并且手动修改。Algol 类的语言,包括 C 和 Java,认为把数据存储在一个数组、散列表、纯文件和数据库中是完全不同的语法。而在象 Abundance 或者 某种程度上 Smalltalk 语言中,语法是一样的;只是声明变了。要利用 Java 这个愚蠢的地方。把知道可能增长从而内存容纳不下的数据放入一个数组中。这样,维护程序员稍后就需要惊人的工作量把数组转换成文件访问。同样可以把小文件放入数据库,当需要进行性能调整的时候,维护程序员就会享受把它们转换成数组访问的乐趣了。
  2. Java 造型(Cast)

    :Java 的造型机制是与生俱来的,可以放心使用而不必愧疚,因为这个语言本来就需要它。每次从 Collection 中取得一个对象,必须把它造型成原来的类型。所以变量的类型可能在很多个地方指定。如果类型后来改变了,那么所有的造型都必须修改使之符合。如果这个倒霉的维护程序员没有找到全部造型(或者改得太多),编译器可能会检测出来也可能不会。同样,如果一个变量的类型从 short 变成 int,所有相应的造型都需要从 (short) 改成 (int)。已经有一个正在进行中的动向,是要创造一个通用的造型操作符 (cast) 和一个通用的转换操作符 (convert),当变量类型变化的时候,就不需要额外的维护。一定不要让这个异端进入语言规范中。对 RFE 114691 和泛型投反对票,因为它们将会消除许多造型操作。
  3. 利用 Java 的冗余

    :Java 坚持必须两次指定变量类型。Java 程序员已经习惯了这样的冗余,如果你让这两种类型稍微有点不一样,他们就不会注意到,就象这个例子:
    // note subtle spelling change
    Bubblegumb = new Bubblegom();
    不幸的是,++ 操作符的流行,使得象下面这样的伪冗余代码很难轻易得手:
    // note subtle spelling change
    swimmer = swimner + 1;
  4. 绝不验证

    :绝不检查输入数据的正确性或者不一致。它表明你绝对相信公司的设备,并且你是一个完美的小组成员,相信项目中所有的合作者和系统管理员。始终返回合理的值,甚至输入数据有疑问或者错误。
  5. 要有礼貌,决不断言

    :避免 assert() 机制,因为它能把三天的调试工作变成十分钟。
  6. 避免封装

    :由于效率的重要性,避免封装。一个方法的调用者,需要所有能得到的外部线索来提醒它们,方法内部是如何工作的。
  7. 克隆&修改

    :以效率的名义,使用剪切/拷贝/克隆/修改。比使用很多可重用的小模块更快。在以写的代码行数多少评定进度的时候特别有用。
  8. 使用静态数组

    :如果库中的模块需要一个数组来保存一张图片,那就只定义一个静态数组。绝没人会有大于 512x512 的图片,所以一个固定大小的数组就可以了。要达到最高的精度,定义一个 double 数组。将这个超过 2M 的静态数组隐藏起来的额外好处,就是程序即使从来没有调用你的方法,也能超过客户机器的内存,象疯了一样颠簸。
  9. 虚假接口

    :写一个空接口叫做 "WrittenByMe" 或者其他什么,让所有的类都实现它。然后,给所有用到的 Java 自带类写一个包装类。想法就是保证程序中的每个对象都实现了这个接口。最后,编写方法,使得它们所有的参数和返回值都是 WrittenByMe。这就几乎不可能看出某个方法是干什么的,也引进了许多有趣的造型要求。为了以后的扩展性,让每个小组成员都有自己的私人接口(比如,WrittenByJoe);一个程序员正在编写的类都要实现自己的这个接口。然后就可以用这么大量而没有任何意义的接口来任意引用对象了。
  10. 庞大的监听器

    :决不为每个 Component 创建单独的监听器。项目中的每个按钮总是使用同一个监听器。只是简单地使用一大堆 if…else 来检测按下了哪个按钮。
  11. 大量使用一个好东西TM

    :疯狂使用包装和 oo (面向对象),例如:
    myPanel.add( getMyButton() );
    
    private JButton getMyButton()
       {
       return myButton;
       }
    一个可能看不出有多有趣,别着急,总有一天会的。
  12. 友好的友元

    :在 C++ 中尽可能频繁地使用友元声明。把创建类的指针指向创建出来的类,把这两种方法结合起来。那么就不需要浪费时间考虑接口了。另外应该使用 privateprotected 关键字来证明你写的类是经过很好封装的。
  13. 使用三维数组

    :多多益善。以旋转的方式在数组之间拷贝数据。也就是说,用 ArrayA 的行填充 ArrayB 的列。拷贝的时候偏移为 1,但不要给出明显的理由,就很不错。就是要让维护程序员着急。
  14. 混合与竞争

    :同时使用访问方法(accessor method,指 getter 和 setter)和公有变量(public vairable)。这样,就能改变一个对象的变量而省去了调用访问方法的开支,但仍把这个类声明为一个"Java Bean"。还有一个额外的好处,当维护程序员添加日志函数想找出谁在修改值的时候,会让他欲哭无泪。
  15. 包装,包装,包装

    :不论什么时候,在不得不使用不是你自己写的代码中的方法时,要使用至少一层包装把你的代码和那些代码隔离开来。毕竟,其他的作者可能在将来某个时候不顾所有的后果,把每个方法都重新命名。那时候你将会怎样? 当然,如果他做了这样的事,可以用一层包装来把你的代码和这些修改隔离起来,要不然让 VAJ 来处理全局重命名。无论如何,这都是一个很完美的借口,在他做这些蠢事之前,用一个间接的包装层预先拦住他。Java 的一个失误就是:如果不使用一些很简单的虚拟包装方法,本身什么都不做,只是调用具有相同或者非常相近名字的另外一个方法,就不可能解决一些简单问题。这就意味着,可能写出四层深的包装而没有做任何事,并且几乎没有人注意到。要隐藏地更深,在每一层对这些方法进行重命名,从同义字典中随机选一个同义词。这就造成一些现象,说明还做了一些事情。进一步,重命名可以帮助保证项目中术语的一致性。要保证没有人把层数削减到一个合理的数字,要在每一层中调一些自己的代码绕过包装器。
  16. 包装,包装,包装,更多的包装

    :保证所有的 API 函数都经过 6-8 次的包装,并定义在不同的源文件中。使用 #define 创建这些函数的快捷方式也行。
  17. 没有秘密!

    :把每个方法和变量都声明为 public。毕竟,某些人某些时候可能要使用。一旦一个方法声明为 public 以后,就不能很好地撤回了,能吗?这就使藏在表面下的一些工作以后很难改变。还有一个令人欣喜的好处是可以隐藏一个类到底要做什么。如果老板问你是不是疯了,就告诉他你是在遵循透明接口的经典原则。
  18. 爱经

    :这项技术增加的好处就是能让包的使用者或者文档编写者以及维护者精神错乱。创建出同一方法许多不同的重载变体,仅仅在最细小的地方有所不同。我认为是奥斯卡·王尔德(Oscar Wilde)观察到在《爱经》(Kama Sutra)中体位 47 和 115 是一模一样的,除了在 115 中那个妇女的手是交叉了。包的使用者不得不仔细阅读一长串方法来找出要使用哪个变体。这项技术也让文档膨胀,从而就保证文档更可能过时。如果老板问为什么这么做,解释说这仅仅是为了用户的方便。再一次,为了能达到最好的效果,克隆任何公共的逻辑,然后坐下等着看它们慢慢不再同步。
  19. 变换,为难

    :把一个叫做 drawRectangle(height, width) 的方法的参数顺序反过来,变成 drawRectangle(width, height),而对方法的名字没有任何改动。接着几次发布之后,再改回来。维护程序员仅仅很快地看一眼方法调用,是不能判断出这些方法是否进行了调整的。一般化的方法作为一个练习留给读者。
  20. 主题和变体

    :不要在一个单独方法中使用参数,而是能创建多少分离的方法就创建多少。例如 setAlignment(int alignment) 中,alignment 是一个枚举常量,要为左中右创建三个方法,setLeftAlignmentsetRightAlignment 以及 setCenterAlignment。当然,要达到最好的效果,必须要克隆公共的逻辑,就使它们很难保持同步。
  21. 静态很不错

    :让尽可能多的变量成为静态变量。如果在程序中对这个类不需要多于一个的实例,那么其他的人也不会需要。再一次,如果项目中的其他编码人员抱怨,告诉他们运行速度会有所提高。
  22. Cargill 的窘境

    :利用 Cargill 的窘境(我认为是他的):"任何设计问题都可以通过增加另外一层迂回来解决,除了有太多的迂回层次以外。"分解 OO 程序,直到几乎不可能找到一个方法能真正地更新程序状态。还可以更好,方法指针森林包含了整个系统所用到的每个方法指针,通过遍历方法指针森林,把这些事情作为回调来激活。为这些要激活的森林遍历加上副作用,遍历时释放先前通过深度拷贝创建的那些经过引用计数的对象,而这些深度拷贝并非真的那么深。
  23. 收藏

    :在代码中到处保留着不用的或者是过时的方法。毕竟,如果在 1976 年用过一次,谁知道以后是否会再用到呢?当然程序从那时起就已经修改过了,但是也有可能很容易地改回去。你可"不想重新发明整个轮子"吧(管理者喜欢这样说话)。如果不动这些方法和变量的注释,并让含义充分模糊,任何维护这些代码的人都不敢碰它们。
  24. 就是 Final

    :让所有的叶子类都为 final。毕竟,已经做完了这个项目——当然,没有其他人能通过扩展这些类来改善你的工作。它可能甚至是安全上的漏洞——要知道,java.lang.String 难道不是仅仅为了这个原因而声明为 final 的吗? 如果项目中其他的编码人员向你抱怨,告诉他们速度上能获得的提高。
  25. 避免接口

    :在用 Java 的时候,鄙弃接口。如果负责人抱怨的话,就告诉他们,Java 的接口强迫你在用相同手段实现相同接口的不同类之间"剪切粘贴"代码,他们就知道这将多么难维护。相反,象 Java AWT 的设计者那样做——在类中放许许多多的功能,只有继承他们的子类才能使用,用许许多多的"instanceof"检查。这样,如果有人想要重用你的代码,他不得不扩展你的类。如果他们想重用来自两个类中的代码——运气真坏呀,他们不能同时扩展这两个类!如果一个接口不可避免,就创建一个万能的接口,把它叫做"ImplementableIface"。来自学术环境的其他精英,会在实现这个接口的类名后面添上"Impl"。这能带来很大的好处,比如,那些实现了 Runnable 接口的类。
  26. 避免布局

    :绝不要使用布局。这样维护程序员要增加一个输入项,就必须手工调整屏幕上显示出来的所有其他东西的绝对坐标。如果老板强迫你使用布局,就只用一个巨大的 GridBagLayout,把网格的绝对坐标进行硬编码。
  27. 环境变量

    :如果不得不写一些给其他程序员使用的类,把环境检查(C++ 中是 getenv() / Java 中是 System.getProperty())的代码放到类的匿名静态初始化器中,通过这种方式来把参数传递到你的类中,而不是在构造函数中。好处就是类的二进制代码一装载,甚至是在进行实例化之前,初始化方法马上就会调用,所以它们通常会在 main() 程序之前运行。换句话说,在你的程序把这些参数读入之前,其他程序没办法修改它们——用户最好象你一样先设置好这些环境变量。
  28. 表驱动逻辑

    :避免任何形式的表驱动逻辑。它本身就足够透明,很快就会让最终用户校对,接着会抖动,甚至自己修改这些表格。(译者注:没看明白)
  29. 修改"母亲"的域

    :在 Java 中,所有作为参数传递的原始类型,实际上都是只读的,因为它们是传值的。被调用者可以修改这些参数,但是对于调用者的变量不起作用。相反,所有传递的对象都是可读写的。引用是传值的,就意味着实际上对象本身是传引用的。被调用者可以在对象中对域做任何想做的事情。绝对不要在文档中指明一个方法是否对传递过来的参数进行了修改。让方法名暗示出只是查看域,而实际上进行了修改。
  30. 全局变量的魔力

    :让错误消息处理过程设置一个全局变量,代替异常来进行错误处理。然后保证系统中每个长时间运行的循环,都检查这个全局标志,如果出现错误就退出。增加另外一个全局变量,当用户按下“重置”按钮时候发信号。当然,系统中所有主要的循环都要检查这第二个标志。隐藏一些不能按照要求退出的循环。
  31. 全局变量,怎么强调都不够

    :如果上帝不让使用全局变量,他就不会发明出来。与其让上帝失望,不如使用和设置尽可能多的全局变量。每个函数至少使用和设置两个,甚至没有理由这么做。以后,任何好的维护程序员很快就会发现,这实在是一个侦探工作练习。她(上帝)会对这个练习很满意,因为它把真正的维护程序员和业余的区分开了。
  32. 全部变量,再来一次,孩子们

    :全局变量把你从必须给函数指定参数中解救出来。要好好利用。从这些全局变量中选出一个或者多个,来指明对其他全局变量要做什么样的处理。维护程序员会傻傻地假设 C 函数不会有副作用。保证它们把结果以及内部状态信息存储在全局变量中。
  33. 副作用

    :在 C 中,函数应该是幂等的,(没有副作用),我希望这个提示已经足够了。
  34. 回退

    :在循环体内,假设循环动作是成功的,并且立即更新所有指针变量。如果后来在那次循环动作检测到异常,在紧跟循环体的条件表达式中,把指针的更新进行回退,造成一些副作用。
  35. 局部变量

    :绝不要使用局部变量。无论何时受到诱惑的时候,要把它放入一个实例变量或者静态变量中,并且无私地和类中的其他方法共享。以后其他方法中需要类似声明的时候,这会省下一些工作。C++ 程序员可以更进一步,把它们全部声明为全局变量。
  36. 缩减、重用、回收

    :如果不得不为回调函数定义一个结构来保存数据,始终把这个结构叫做 PRIVDATA。每个模块都定义自己的 PRIVDATA。在 VC++ 中有很大的好处,能混淆调试器,这样,如果有一个 PRIVDATA 变量需要在变量查看窗口中扩展开来,它不知道你要看的到底是哪个,所以它就随便选一个。
  37. 配置文件

    :这些配置文件通常是“关键字=值”的形式。这些值在装载的时候赋给 Java 变量。最明显的迷惑技巧,就是对于关键字和 Java 变量使用稍微有点不同的名字。对于那些在运行时永远不变的常量也使用配置文件。参数文件变量需要的代码量至少是单个简单变量的五倍。
  38. 臃肿的类

    :要确定尽可能用最迟钝的方法把类限制起来,务必在每个类里面包含一些无关紧要要的、不起眼的方法和属性。举个例子吧,一个阐明天体物理轨道几何学的类中,的确需要计算潮汐时间的方法以及构成 Crane 天气模型的一些属性。这不仅使类过度定义,并且使在一般的系统代码中寻找这些方法,就象在垃圾中找一根吉他弦一样难。
  39. 纵情于子类

    :面向对象编程生来就是用来编写不可维护代码的。如果有一个包含十个属性的类(变量/方法),考虑使用带有一个属性的基类,对它进行九层深的继承,这样每个后代都只带有一个属性,当到达最后的子类时,就有了全部十个属性。如果可能的话,把每个类声明放到不同的文件中。这就有些附加作用,让 INCLUDE 或者 USES 语句变得膨胀,并且迫使维护程序员在编辑器中打开更多的文件。要保证每个类都至少创建了一个实例。