作者 | 老码农的自留地
来源 | 程序员的那些事
如何编写无法维护的代码
让自己稳拿铁饭碗
简介
永远不要(把自己遇到的问题)归因于(他人的)恶意,这恰恰说明了(你自己的)无能。—— 拿破仑
为了造福大众,在 Java 编程领域创造就业机会,兄弟我在此传授大师们的秘籍。这些大师写的代码极其难以维护,后继者就是想对它做最简单的修改都需要花上数年时间。而且,如果你能对照秘籍潜心修炼,你甚至可以给自己弄个铁饭碗,因为除了你之外,没人能维护你写的代码。再而且,如果你能练就秘籍中的全部招式,那么连你自己都无法维护你的代码了!
你不想练功过度走火入魔吧。那就不要让你的代码一眼看去就完全无法维护,只要它实质上是那样就行了。否则,你的代码就有被重写或重构的风险!
总则
Quidquid latine dictum sit, altum sonatur.
(随便用拉丁文写点啥,都会显得高大上。)
想挫败维护代码的程序员,你必须先明白他的思维方式。他接手了你的庞大程序,没有时间把它全部读一遍,更别说理解它了。他无非是想快速找到修改代码的位置、改代码、编译,然后就能交差,并希望他的修改不会出现意外的副作用。
他查看你的代码不过是管中窥豹,一次只能看到一小段而已。你要确保他永远看不到全貌。要尽量让他难以找到他想找的代码。但更重要的是,要让他不能有把握忽略任何东西。
程序员都被编程惯例洗脑了,还为此自鸣得意。每一次你处心积虑地违背编程惯例,都会迫使他必须用放大镜去仔细阅读你的每一行代码。
你可能会觉得每个语言特性都可以用来让代码难以维护,其实不然。你必须精心地误用它们才行。
命名
“当我使用一个单词的时候” Humpty Dumpty 曾经用一种轻蔑的口气说, “它就是我想表达的意思,不多也不少。“ – Lewis Carroll — 《爱丽丝魔镜之旅》, 第 6 章
编写无法维护代码的技巧的重中之重是变量和方法命名的艺术。如何命名是和编译器无关的。这就让你有巨大的自由度去利用它们迷惑维护代码的程序员。
妙用宝宝起名大全
买本宝宝起名大全,你就永远不缺变量名了。比如 Fred 就是个好名字,而且键盘输入它也省事。如果你就想找一些容易输入的变量名,可以试试 adsf 或者 aoeu之类。
1、单字母变量名
如果你给变量起名为 a,b,c,用简单的文本编辑器就没法搜索它们的引用。而且,没人能猜到它们的含义。
2、创造性的拼写错误
如果你必须使用描述性的变量和函数名,那就把它们都拼错。还可以把某些函数和变量名拼错,再把其它的拼对(例如 SetPintleOpening 和 SetPintalClosing)),我们就能有效地将 grep或 IDE 搜索技术玩弄于股掌之上。这招超级管用。还可以混淆不同语言(比如 colour — 英国英语,和 color — 美国英语)。
3、抽象
在命名函数和变量的时候,充分利用抽象单词,例如 it, everything, data, handle, stuff, do, routine, perform 和数字,像这样命名的好例子有 routineX48, PerformDataFunction, DoIt, HandleStuff 还有 do_args_method。
4、首字母大写的缩写
用首字母大写缩写(比如GNU 代表 GNU’s Not Unix) 使代码简洁难懂。真正的汉子(无论男女)从来不说明这种缩写的含义,他们生下来就懂。
5、辞典大轮换
为了打破沉闷的编程气氛,你可以用一本辞典来查找尽量多的同义词。例如 display, show, present。在注释里含糊其辞地暗示这些命名之间有细微的差别,其实根本没有。
不过,如果有两个命名相似的函数真的有重大差别,那倒是一定要确保它们用相同的单词来命名(例如,对于 “写入文件”, “在纸上书写” 和 “屏幕显示” 都用 print 来命名)。在任何情况下都不要屈服于编写明确的项目词汇表这种无理要求。你可以辩解说,这种要求是一种不专业的行为,它违反了结构化设计的信息隐藏原则。
6、首字母大写
随机地把单词中间某个音节的首字母大写。例如 ComputeReSult。
7、重用命名
在语言规则允许的地方,尽量把类、构造器、方法、成员变量、参数和局部变量都命名成一样。更高级的技巧是在{}块中重用局部变量。这样做的目的是迫使维护代码的程序员认真检查每个示例的作用域。特别是在Java代码中,可以把普通方法伪装成构造器。
8、使用非英语字母
在命名中偷偷使用不易察觉的非英语字母,例如:
typedef struct { int i; } ínt;
看上去没啥不对是吧?嘿嘿嘿…这里的第二个 ínt 的 í 实际上是东北欧字母,并不是英语中的 i 。在简单的文本编辑器里,想看出这一点点区别几乎是不可能的。
9、巧妙利用编译器对于命名长度的限制
如果编译器只区分命名的前几位,比如前8位,那么就把后面的字母写得不一样。比如,其实是同一个变量,有时候写成 var_unit_update ,有时候又写成 var_unit_setup,看起来是两个不同的函数调用。而在编译的时候,它们其实是同一个变量 var_unit。
10、下划线,真正的朋友
可以拿 _ 和 __ 作为标示符。
11、混合多语言
随机地混用两种语言(人类语言或计算机语言都行)。如果老板要求使用他指定的语言,你就告诉他你用自己的语言更有利于组织你的思路,万一这招不管用,就去控诉这是语言歧视,并威胁起诉老板要求巨额精神损失赔偿。
12、扩展 ASCII 字符
扩展 ASCII 字符用于变量命名是完全合法的,包括 ß, Ð, 和 ñ 等。在简单的文本编辑器里,除了拷贝/粘贴,基本上没法输入。
13、其它语言的命名
使用外语字典作为变量名的来源。例如,可以用德语单词 punkt 代替 point。除非维护代码的程序员也像你一样熟练掌握了德语. 不然他就只能尽情地在代码中享受异域风情了。
14、数学命名
用数学操作符的单词来命名变量。例如:
openParen = (slash asterix) / equals;
(左圆括号 = (斜杠 星号)/等号;)
15、令人眩晕的命名
用带有完全不相关的感情色彩的单词来命名变量。例如:
marypoppins = (superman starship) / god;
(欢乐满人间 = (超人 星河战队)/上帝;)
这一招可以让阅读代码的人陷入迷惑之中,因为他们在试图想清楚这些命名的逻辑时,会不自觉地联系到不同的感情场景里而无法自拔。
16、何时使用 i
永远不要把 i 用作最内层的循环变量。用什么命名都行,就是别用 i。把 i 用在其他地方就随便了,用作非整数变量尤其好。
17、惯例 — 明修栈道,暗度陈仓
忽视 Java 编码惯例,Sun 自己就是这样做的。幸运的是,你违反了它编译器也不会打小报告。这一招的目的是搞出一些在某些特殊情况下有细微差别的名字来。
如果你被强迫遵循驼峰法命名,你还是可以在某些模棱两可的情况下颠覆它。例如,inputFilename 和 inputfileName 两个命名都可以合法使用。在此基础上自己发明一套复杂到变态的命名惯例,然后就可以痛扁其他人,说他们违反了惯例。
18、小写的 l 看上去很像数字 1
用小写字母 l 标识 long 常数。例如 10l 更容易被误认为是 101 而不是 10L 。禁用所有能让人准确区分 uvw wW gq9 2z 5s il17|!j oO08 `’” ;,. m nn rn {[()]} 的字体。要做个有创造力的人。
19、把全局命名重用为私有
在 A 模块里声明一个全局数组,然后在 B 模块的头文件里在声明一个同名的私有数组,这样看起来你在 B 模块里引用的是那个全局数组,其实却不是。不要在注释里提到这个重复的情况。
20、误导性的命名
让每个方法都和它的名字蕴含的功能有一些差异。例如,一个叫 isValid(x) 的方法在判断完参数 x 的合法性之后,还顺带着把它转换成二进制并保存到数据库里。
伪装
当一个 bug 需要越长的时间才会暴露,它就越难被发现。- Roedy Green
编写无法维护代码的另一大秘诀就是伪装的艺术,即隐藏它或者让它看起来像其他东西。很多招式有赖于这样一个事实:编译器比肉眼或文本编辑器更有分辨能力。下面是一些伪装的最佳招式。
把代码伪装成注释,反之亦然
下面包括了一些被注释掉的代码,但是一眼看去却像是正常代码。
for(j=0; j<array_len; j =8)
{
total = array[j 0 ];
total = array[j 1 ];
total = array[j 2 ]; /* Main body of
total = array[j 3]; * loop is unrolled
total = array[j 4]; * for greater speed.
total = array[j 5]; */
total = array[j 6 ];
total = array[j 7 ];
}
如果不是用红色标出来,你能注意到这三行代码被注释掉了么?
用连接符隐藏变量
对于下面的定义:#define local_var xy_z
可以把 “xy_z” 打散到两行里:
#define local_var xy\
_z // local_var OK
这样全局搜索 xy_z 的操作在这个文件里就一无所获了。对于 C 预处理器来说,第一行最后的 “\” 表示继续拼接下一行的内容。
文档
任何傻瓜都能说真话,而要把谎编圆则需要相当的智慧。—— Samuel Butler (1835 – 1902)
不正确的文档往往比没有文档还糟糕。—— Bertrand Meyer
既然计算机是忽略注释和文档的,你就可以在里边堂而皇之地编织弥天大谎,让可怜的维护代码的程序员彻底迷失。
1、在注释中撒谎
实际上你不需要主动地撒谎,只要没有及时保持注释和代码更新的一致性就可以了。
2、只记录显而易见的东西
往代码里掺进去类似于
/* 给 i 加 1 */
这样的注释,但是永远不要记录包或者方法的整体设计这样的干货。
3、记录 How 而不是 Why
只解释一个程序功能的细节,而不是它要完成的任务是什么。这样的话,如果出现了一个bug,修复者就搞不清这里的代码应有的功能。
4、该写的别写
比如你在开发一套航班预定系统,那就要精心设计,让它在增加另一个航空公司的时候至少有 25 处代码需要修改。永远不要在文档里说明要修改的位置。后来的开发人员要想修改你的代码?门都没有,除非他们能把每一行代码都读懂。
5、计量单位
永远不要在文档中说明任何变量、输入、输出或参数的计量单位,如英尺、米、加仑等。计量单位对数豆子不是太重要,但在工程领域就相当重要了。同理,永远不要说明任何转换常量的计量单位,或者是它的取值如何获得。
要想让代码更乱的话,你还可以在注释里写上错误的计量单位,这是赤裸裸的欺骗,但是非常有效。如果你想做一个恶贯满盈的人,不妨自己发明一套计量单位,用自己或某个小人物的名字命名这套计量单位,但不要给出定义。万一有人挑刺儿,你就告诉他们,你这么做是为了把浮点数运算凑成整数运算而进行的转换。
6、坑
永远不要记录代码中的坑。如果你怀疑某个类里可能有bug,天知地知你知就好。如果你想到了重构或重写代码的思路,看在老天爷的份上,千万别写出来。切记电影《小鹿斑比》里那句台词:“如果你不能说好听的话,那就什么也不要说。”。
万一这段代码的原作者看到你的注释怎么办?万一老板看到了怎么办?万一客户看到了怎么办?搞不好最后你自己被解雇了。一句”这里需要修改“的匿名注释就好多了,尤其是当看不清这句注释指的是哪里需要修改的情况下。切记“难得糊涂”四个字,这样大家都不会感觉受到了批评。
7、说明变量
永远不要对变量声明加注释。有关变量使用的方式、边界值、合法值、小数点后的位数、计量单位、显示格式、数据录入规则等等,后继者完全可以自己从程序代码中去理解和整理嘛。如果老板强迫你写注释,就把方法体代码混进去,但绝对不要对变量声明写注释,即使是临时变量!
8、在注释里挑拨离间
为了阻挠任何雇佣外部维护承包商的倾向,可以在代码中散布针对其他同行软件公司的攻击和抹黑,特别是可能接替你工作的其中任何一家。例如:
/* 优化后的内层循环
这套技巧对于SSI软件服务公司的那帮蠢材来说太高深了,他们只会
用 <math.h> 里的笨例程,消耗50倍的内存和处理时间。
*/
class clever_SSInc
{
.. .
}
可能的话,除了注释之外,这些攻击抹黑的内容也要掺到代码里的重要语义部分,这样如果管理层想清理掉这些攻击性的言论然后发给外部承包商去维护,就会破坏代码结构。
程序设计
编写无法维护代码的基本规则就是:在尽可能多的地方,以尽可能多的方式表述每一个事实。- Roedy Green
编写可维护代码的关键因素是只在一个地方表述应用里的一个事实。如果你的想法变了,你也只在一个地方修改,这样就能保证整个程序正常工作。所以,编写无法维护代码的关键因素就是反复地表述同一个事实,在尽可能多的地方,以尽可能多的方式进行。
令人高兴的是,像Java这样的语言让编写这种无法维护代码变得非常容易。例如,改变一个被引用很多的变量的类型几乎是不可能的,因为所有造型和转换功能都会出错,而且关联的临时变量的类型也不合适了。
如果变量值要在屏幕上显示,那么所有相关的显示和数据录入代码都必须一一找到并手工进行修改。类似的还有很多,比如由C和Java组成的Algol语言系列,Abundance甚至Smalltalk对于数组等结构的处理,都是大有可为的。
1、Java 造型
Java 的造型机制是上帝的礼物。你可以问心无愧地使用它,因为 Java 语言本身就需要它。每次你从一个 Collection 里获取一个对象,你都必须把它造型为原始类型。这样这个变量的类型就必须在无数地方表述。如果后来类型变了,所有的造型都要修改才能匹配。
如果倒霉的维护代码的程序员没有找全(或者修改太多),编译器能不能检测到也不好说。类似的,如果变量类型从short 变成 int,所有匹配的造型也都要从(short) 改成 (int)。
2、利用 Java 的冗余
Java 要求你给每个变量的类型写两次表述。Java 程序员已经习惯了这种冗余,他们不会注意到你的两次表述有细微的差别,例如
Bubblegum b = new Bubblegom;
不幸的是 操作符的盛行让下面这种伪冗余代码得手的难度变大了:
swimmer = swimner 1;
3、永远不做校验
永远不要对输入数据做任何的正确性或差异性检查。这样能表现你对公司设备的绝对信任,以及你是一位信任所有项目伙伴和系统管理员的团队合作者。总是返回合理的值,即使数据输入有问题或者错误。
4、有礼貌,无断言
避免使用 assert 机制,因为它可能把三天的debug盛宴变成10分钟的快餐。
5、避免封装
为了提高效率,不要使用封装。方法的调用者需要所有能得到的外部信息,以便了解方法的内部是如何工作的。
6、复制粘贴修改
以效率的名义,使用 复制 粘贴 修改。这样比写成小型可复用模块效率高得多。在用代码行数衡量你的进度的小作坊里,这招尤其管用。
7、使用静态数组
如果一个库里的模块需要一个数组来存放图片,就定义一个静态数组。没人会有比 512 x 512 更大的图片,所以固定大小的数组就可以了。为了最佳精度,就把它定义成 double 类型的数组。
8、傻瓜接口
编写一个名为 “WrittenByMe” 之类的空接口,然后让你的所有类都实现它。然后给所有你用到的Java 内置类编写包装类。这里的思想是确保你程序里的每个对象都实现这个接口。最后,编写所有的方法,让它们的参数和返回类型都是这个 WrittenByMe。
这样就几乎不可能搞清楚某个方法的功能是什么,并且所有类型都需要好玩的造型方法。更出格的玩法是,让每个团队成员编写它们自己的接口(例如 WrittenByJoe),程序员用到的任何类都要实现他自己的接口。这样你就可以在大量无意义接口中随便找一个来引用对象了。
9、巨型监听器
永远不要为每个组件创建分开的监听器。对所有按钮总是用同一个监听器,只要用大量的 if…else 来判断是哪一个按钮被点击就行了。
10、好事成堆TM
狂野地使用封装和 OO 思想。例如
myPanel.add( getMyButton );
private JButton getMyButton
{
return myButton;
}
这段很可能看起来不怎么好笑。别担心,只是时候未到而已。
11、友好的“朋友”
在 C 里尽量多使用 friend 声明。再把创建类的指针传递给已创建类。现在你不用浪费时间去考虑接口了。另外,你应该用上关键字 private 和 protected 来表明你的类封装得很好。
12、使用三维数组
大量使用它们。用扭曲的方式在数组之间移动数据,比如,用 arrayA 里的行去填充 arrayB 的列。这么做的时候,不管三七二十一再加上 1 的偏移值,这样很灵。让维护代码的程序员抓狂去吧。
13、混合与匹配
存取方法和公共变量神马的都要给他用上。这样的话,你无需调用存取器的开销就可以修改一个对象的变量,还能宣称这个类是个”Java Bean”。对于那些试图添加日志函数来找出改变值的源头的维护代码的程序员,用这一招来迷惑他尤其有效。
14、没有秘密!
把每个方法和变量都声明为 public。毕竟某个人某天可能会需要用到它。一旦方法被声明为 public 了,就很难缩回去。对不?这样任何它覆盖到的代码都很难修改了。它还有个令人愉快的副作用,就是让你看不清类的作用是什么。如果老板质问你是不是疯了,你就告诉他你遵循的是经典的透明接口原则。
15、全堆一块
把你所有的没用的和过时的方法和变量都留在代码里。毕竟说起来,既然你在1976年用过一次,谁知道你啥时候会需要再用到呢?当然程序是改了,但它也可能会改回来嘛,你”不想要重新发明轮子”(领导们都会喜欢这样的口气)。如果你还原封不动地留着这些方法和变量的注释,而且注释写得又高深莫测,甭管维护代码的是谁,恐怕都不敢对它轻举妄动。
16、就是 Final
把你所有的叶子类都声明为 final。毕竟说起来,你在项目里的活儿都干完了,显然不会有其他人会通过扩展你的类来改进你的代码。这种情况甚至可能有安全漏洞。java.lang.String 被定义成 final 也许就是这个原因吧?如果项目组其他程序员有意见,告诉他们这样做能够提高运行速度。
17、避免布局
永远不要用到布局。当维护代码的程序员想增加一个字段,他必须手工调整屏幕上显示所有内容的绝对坐标值。如果老板强迫你使用布局,那就写一个巨型的 GridBagLayout 并在里面用绝对坐标进行硬编码。
18、全局变量,怎么强调都不过分
如果上帝不愿意我们使用全局变量,他就不会发明出这个东西。不要让上帝失望,尽量多使用全局变量。每个函数最起码都要使用和设置其中的两个,即使没有理由也要这么做。毕竟,任何优秀的维护代码的程序员都会很快搞清楚这是一种侦探工作测试,有利于让他们从笨蛋中脱颖而出。
19、再一次说说全局变量
全局变量让你可以省去在函数里描述参数的麻烦。充分利用这一点。在全局变量中选那么几个来表示对其他全局变量进行操作的类型。
20、局部变量
永远不要用局部变量。在你感觉想要用的时候,把它改成一个实例或者静态变量,并无私地和其他方法分享它。这样做的好处是,你以后在其他方法里写类似声明的时候会节省时间。C 程序员可以百尺竿头更进一步,把所有变量都弄成全局的。
21、配置文件
配置文件通常是以 关键字 = 值 的形式出现。在加载时这些值被放入 Java 变量中。最明显的迷惑技术就是把有细微差别的名字用于关键字和Java 变量.甚至可以在配置文件里定义运行时根本不会改变的常量。参数文件变量和简单变量比,维护它的代码量起码是后者的5倍。
22、子类
对于编写无法维护代码的任务来说,面向对象编程的思想简直是天赐之宝。如果你有一个类,里边有 10 个属性(成员/方法),可以考虑写一个基类,里面只有一个属性,然后产生 9 层的子类,每层增加一个属性。等你访问到最终的子类时,你才能得到全部 10 个属性。如果可能,把每个类的声明都放在不同的文件里。
编码迷局
1、迷惑 C
从互联网上的各种混乱C 语言竞赛中学习,追随大师们的脚步。
2、追求极致
总是追求用最迷惑的方式来做普通的任务。例如,要用数组来把整数转换为相应的字符串,可以这么做:
char *p;
switch (n)
{
case 1:
p = "one";
if (0)
case 2:
p = "two";
if (0)
case 3:
p = "three";
printf("%s", p);
break;
}
3、一致性的小淘气
当你需要一个字符常量的时候,可以用多种不同格式:‘ ‘, 32, 0×20, 040。在 C 或 Java里 10 和 010 是不同的数(0 开头的表示 16 进制),你也可以充分利用这个特性。
4、造型
把所有数据都以 void * 形式传递,然后再造型为合适的结构。不用结构而是通过位移字节数来造型也很好玩。
5、嵌套 Switch
Switch 里边还有 Switch,这种嵌套方式是人类大脑难以破解的。
6、利用隐式转化
牢记编程语言中所有的隐式转化细节。充分利用它们。数组的索引要用浮点变量,循环计数器用字符,对数字执行字符串函数调用。不管怎么说,所有这些操作都是合法的,它们无非是让源代码更简洁而已。
任何尝试理解它们的维护者都会对你感激不尽,因为他们必须阅读和学习整个关于隐式数据类型转化的章节,而这个章节很可能是他们来维护你的代码之前完全忽略了的。
7、分号!
在所有语法允许的地方都加上分号,例如:
if(a);
else;
{
int d;
d = c;
}
;
8、使用八进制数
把八进制数混到十进制数列表里,就像这样:
array = new int
{
111,
120,
013,
121,
};
9、嵌套
尽可能深地嵌套。优秀的程序员能在一行代码里写 10 层 ,在一个方法里写 20 层 {}。
10、C数组
C编译器会把 myArray[i] 转换成 *(myArray i),它等同于 *(i myArray) 也等同于 i[myArray]。高手都知道怎么用好这个招。可以用下面的函数来产生索引,这样就把代码搞乱了:
int myfunc(int q, int p) { return p%q; }
...
myfunc(6291, 8)[Array];
遗憾的是,这一招只能在本地C类里用,Java 还不行。
11、放长线钓大鱼
一行代码里堆的东西越多越好。这样可以省下临时变量的开销,去掉换行和空格还可以缩短源文件大小。
记住,要去掉运算符两边的空格。优秀的程序员总是能突破某些编辑器对于 255 个字符行宽的限制。
12、异常
我这里要向你传授一个编程领域里鲜为人知的秘诀。异常是个讨厌的东西。良好的代码永远不会出错,所以异常实际上是不必要的。不要把时间浪费在这上面。子类异常是给那些知道自己代码会出错的低能儿用的。
在整个应用里,你只用在 main 里放一个try/catch,里边直接调用 System.exit 就行了。在每个方法头要贴上标准的抛出集合定义,到底会不会抛出异常你就不用管了。
13、使用异常的时机
在非异常条件下才要使用异常。比如终止循环就可以用 ArrayIndexOutOfBoundsException。还可以从异常里的方法返回标准的结果。
15、测试
在程序里留些 bug,让后来的维护代码的程序员能做点有意思的事。精心设计的 bug 是无迹可寻的,而且谁也不知道它啥时候会冒出来。要做到这一点,最简单的办法的就是不要测试代码。
16、永不测试
永远不要测试负责处理错误、当机或操作系故障的任何代码。反正这些代码永远也不会执行,只会拖累你的测试。还有,你怎么可能测试处理磁盘错误、文件读取错误、操作系统崩溃这些类型的事件呢?
为啥你要用特别不稳定的计算机或者用测试脚手架来模拟这样的环境?现代化的硬件永远不会崩溃,谁还愿意写一些仅仅用于测试的代码?这一点也不好玩。如果用户抱怨,你就怪到操作系统或者硬件头上。他们永远不会知道真相的。
17、永远不要做性能测试
嘿,如果软件运行不够快,只要告诉客户买个更快的机器就行了。如果你真的做了性能测试,你可能会发现一个瓶颈,这会导致修改算法,然后导致整个产品要重新设计。谁想要这种结果?而且,在客户那边发现性能问题意味着你可以免费到外地旅游。你只要备好护照和最新照片就行了。
18、永远不要写任何测试用例
永远不要做代码覆盖率或路径覆盖率测试。自动化测试是给那些窝囊废用的。搞清楚哪些特性占到你的例程使用率的90%,然后把90%的测试用在这些路径上。
毕竟说起来,这种方法可能只测试到了大约你代码的60%,这样你就节省了40%的测试工作。这能帮助你赶上项目后端的进度。等到有人发现所有这些漂亮的“市场特性”不能正常工作的时候,你早就跑路了。一些有名的大软件公司就是这样测试代码的,所以你也应该这样做。
,