一、糟糕程序员的迹象1. 无法对代码进行推理
对代码进行推理意味着能跟随代码的执行路径(“在脑子里运行程序”),同时清楚地知道代码执行的目标。
特征
程序里有“巫毒代码( voodoo code )”;存在对程序目标毫无益处的代码,但却仍然勤勉地维护它们(例如,初始化从来不用的变量、调用和目标毫不相关的函数、生成用不着的输出,等等)。(译者:巫毒代码应该就是隐藏危险的代码,不知道什么时候就会给程序造成危害,就像“巫毒术”。)
多次执行幂等函数(例如:多次调用 save() 函数“只是为了确保无误”)。(译者:幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。)
通过重写错误代码的结果来修复程序 bug。
“溜溜球式代码( Yo-Yo code )”就是将一个值转换成另一种不同的格式,然后再转换回到最初的格式(例如:将一个小数转换成一个字符串,然后再转回成小数;或是填充一个字符串,然后再裁剪它)。
“推土机式代码( Bulldozer code )”将大块代码分解成多个子程序,看起来像是重构,但不可能在其他环境下重用(耦合度太高)。
补救措施
程序猿可以通过实践来克服这个缺点,如果 IDE 自带的调试器能单步调试,就把它作为助手使用。比如说在 Visual Studio 里,这就意味着要在问题区域的起始处打上断点,然后按下‘ F11 ’单步调试,查看变量的值(变化前后都要查看),直到你明白了代码正在做什么。如果你的目标环境不具备这种特性,那就找一个拥有这种特性的环境去实践。
这么做的目的是,让你做到不再需要调试器就能在脑子里跟随代码的流程,而且有足够的耐心去思考代码正在对整个程序的状态做什么。这么做的好处就是能够识别出冗余且无用的代码,而且不需要从头执行整个路径就能在当前代码中找出 bug。
2.难以理解语言的编程模型
面向对象编程( Object Oriented Programming )就是一种语言模型,正如函数式编程( Functional programming )或声明式编程( Declarative programming )一样。它们每一个都和过程式或命令式编程有着显著不同,就像过程式编程明显不同于汇编或基于 GOTO 的编程。此外,虽然有很多语言都跟随同一个主流编程模型(如面向对象的编程),但它们都只介绍自己的改进,例如递推式构造列表( list comprehensions )、泛型( generics )、鸭式分类( duck-typing )等等。
译者:duck-typing 是动态语言的一种程序设计风格,用以实践方法多态。Duck-typing 并不关注对象的实际类型,而是关注其表现。概念提出者 James Whitcomb Riley 这样描述这个风格:当看到一只鸟走起来像鸭子,游起泳来像鸭子,叫起来也像鸭子,那这只鸟就可以看出是鸭子。
特征
使用任何所需的语法来摆脱模型的束缚,接着用他们熟悉的语言风格来完成程序的剩余部分。
(面向对象编程)试图在未实例化的类中调用非静态的函数或变量,并且无法理解为什么这样不能编译。
(面向对象编程)写了大量“ xxxxxManager ”这样的类,类中包含所有控制对象字段的方法,而这些对象本身几乎没有定义方法。
(关联式编程)把关联式数据库当作对象仓库,在客户代码中执行所有的联结( joins )和关系约束( relation enforcement )。
(函数式编程)为了处理不同类型的输入或运算符,对同一个算法创建多个版本实现,而不是向一个泛型实现传入高级函数。
(函数式编程)非要在能自动缓存的平台上手动缓存确定性函数的结果(比如 SQL 和 HasKell)。(译者:确定性函数就是在输入特定的值集合时,调用函数得到相同的结果。HasKell 是一种纯函数式编程语言。)
从别人的程序里剪切粘贴代码来处理 I/O 和 Monads。(译者:Monads 是函数式编程中一种代表计算指令的结构,详见Monad。)
(声明式编程)在命令式代码中设置单一值,而不是使用数据绑定( data-binding )。
补救措施
如果你的技能不足,是因为别人教得不好或是自己没学好,那编译器自身就是一位备选老师。学习一个新的编程模型,最有效的办法莫过于创建一个新工程,不管都有哪些新的构造方法,强迫自己去使用它们,无论在工程中的使用是否明智。你也需要练习用自己最熟悉且通俗易懂的措辞来解释模型特性,然后递归地创建自己的新词汇表,直到你对模型理解入微。举个例子:
阶段一:“OOP 就是方法的集合”
阶段二:“OOP 里的方法就是函数,它们运行在自带全局变量的小程序中”
阶段三:“全局变量被称为字段,其中有些是私有字段,在小程序外不可见”
阶段四:“拥有私有和公有元素是为了隐藏实现细节,暴露干净整洁的接口,这就叫封装”
阶段五:“封装意味着实现细节不会破坏业务逻辑”
对所有编程语言来说阶段五看起来都一样,因为所有语言在阶段五都试图让程序猿能表达出程序的意图,而不需要将其隐藏在如何实现的细节之中。拿函数式编程再举个例子:
阶段一:“函数式编程做的所有事情就是将确定性函数链接在一起”
阶段二:“当函数是确定的,编译器就能够预测什么时候可以缓存结果或跳过求值,甚至在什么时候提前中止求值是安全的”
阶段三:“为了支持惰性求值( Lzay Evaluation )和部分求值( Partial Evaluation ),编译器要求函数定义如何转换一个单一参数,甚至有时要将其转换成另一个函数。这就叫函数柯里化( Currying )”
阶段四:“有的时候编译器可以替我们进行函数柯里化( Currying )”
阶段五:“让编译器搞清楚普通细节,我就可以通过描述我想要什么来写程序,而不是告诉它怎么给我结果”
3.缺乏研究技巧/长期缺乏对平台特性的了解
如今,现代语言和框架都带有非常了不起的内置命令和特性,一些主要的框架(像 Java 、 . Net 、 Cocoa)由于本身结构庞大,任何一个程序猿(甚至是一个很优秀的程序猿)都要花费好几年时间去学习。但是,一个优秀的程序猿在自己开始构造所需函数之前,会先搜索有没有满足需求的内置函数。而杰出的程序猿们则能够分解并识别出任务中的抽象问题,接着在实际开始设计程序之前,去搜索适用的现有框架、模式、模型和语言。
特征
如果在应该掌握新平台很久以后,这些特征还继续出现,那它们就暗示着存在问题。
重新发明或做一些费劲繁杂的工作来实现某种功能,而不使用语言内置的基础机制,如事件-处理机制(events-and-handlers)或正则表达式。
重新发明框架的内置类和函数(比如定时器、数据集合、排序和搜索算法)。
在帮助论坛上发布这样的信息“把代码发到我的邮箱,谢谢”。
用很多条指令来实现“冗余代码”,实际上可以简单得多(比如:把一个小数转换成格式化字符串来取整,然后再把这个字符串转回成小数)。
坚持使用过时的技术,即便在那些情况下使用新技术更佳(比如:还在写命名委托函数,而不用lambda表达式)。
有一个很刻板的“舒适区( comfort zone )”,不顾一切地使用原语来解决复杂问题。
译者:“ comfort zone ”就是使人感到安全、舒服或在其掌控之下的形式或状态。
也会偶尔复制代码,复制的频率和框架大小成比例,因此,按自己的程度来判断吧。手写链表的人也许知道自己正在做什么,但手写 StrCpy() 的人可能就不知道了。
补救措施
一个程序猿如果不放慢速度,就不可能学到这类知识。而且很有可能,这个人一直都在火急火燎地用任何需要的手段让每个函数都工作起来。他需要在手边放一本平台的技术参考手册,并且能够花最小的代价浏览它,这就是说要么在桌上的键盘右边放一本打印稿,要么还有一个屏专门用来打开浏览器。为了开始培养这种习惯,他应该重构旧代码,目标是减少十分之一以上的指令数量。
4.无法理解指针
如果你不能理解指针,那你能写的程序类型就非常有限,因为指针的概念创造出了很多复杂的数据结构和有效的 APIs。托管类语言使用引用来代替指针,两者很像,但引用增加了自动解引用功能并禁止指针运算,从而消除特定类型的 bug。无论如何,它们还是非常相似,不能掌握这个概念就会导致数据结构的设计很差劲,并且出现一些由于不理解方法调用中值传递和引用传递的区别而导致的问题。
特征
不会实现链表;从链表或树中插入/删除节点时,写的代码总是丢失数据。
凭经验为长度可变的集合分配大数组,并且维护一个单独的集合大小计数器,而不是使用动态数据结构。
无法找出或修复由指针运算错误导致的 bug。
对于作为参数传递给函数的指针,修改其指向的值,并且没有预料到指针指向的对象会在函数外被改变。
复制指针,通过复制的指针改变其指向的值,然后假设原来的指针仍指向旧值。
在应该将指针的解引用值序列化时,却把指针序列化到磁盘或网络上。
通过比较指针值来对指针数组排序。
补救措施
“我有一个叫 Joe 的朋友待在宾馆的某个房间里,而我不知道他的房间号。但我知道他的熟人 Frank 待在哪个房间”,因此我跑去敲门问他‘Joe 在哪个房间?’,Frank 表示他也不知道,但他知道 Joe 的同事 Theodore 在哪个房间,并给了我 Theodore 的房间号。因此我又跑到Theodore的房间问 Joe 在哪,Theodore 告诉我 Joe 在414房间。实际上,Joe 就是在那个房间。”
对于指针,可以用很多种不同的隐喻来描述,而数据结构则可以描述成多种比喻。上面是对链表的简单类比,而且任何人都能发明自己的版本,即使他们不是程序猿。提到指针大家都能理解,因此,你的描述不会比现有的描述还更全面。当程序猿试图想象计算机的内存里正在发生什么,并把这个想象和他们对普通变量的理解融合时,虽然这两者很相似,但这个时候就会无法理解。也许将代码解释成一个简单的故事有利于推理当前的状况,直到发现其中的区别,直到程序猿可以像面对标量值和数组一样直观地想象指针和数据结构。
5.难以看透递归
递归的思想很容易理解,但程序猿们经常在自己脑子里想象一次递归操作的结果时遇到困难,或想不通一个简单函数是怎么计算出复杂结果的。这些不解使得要设计一个递归函数变得难上加难,因为当你要对初始条件或递归调用的参数进行测试时,你想象不出“当前走到哪一步了”。
特征
对问题设计极其复杂的迭代算法,但其实可以通过递归解决(比如:遍历一个文件系统树),尤其是在不用保证内存和性能的情况下。
递归函数在递归调用前后都会检查相同的初始条件。
递归函数没有测试初始条件。
递归子程序连接到一个全局变量或支持输出的变量上,或者累计这些变量的和。
对于递归调用中要传递什么参数表现出明显的困惑,或是不理解传递未修改参数的递归调用。
认为迭代的次数会被作为参数传递。
补救措施
先体会一下,准备好迎接某种堆栈溢出吧。首先,在代码里只写一个初始条件检测并只调用一次递归,递归中使用同一个被传递的未修改参数。即使你觉得写得不够好也要停下来,无论如何,让代码运行一下。它抛出了一个堆栈溢出的异常,那么现在返回去继续写,在递归调用中传递参数的已修改拷贝。产生了更多的堆栈溢出错误?输出过度?那就接着反复修改代码再运行,从修改初始条件测试转向修改递归调用,直到你开始凭直觉就知道函数怎么转换它的输入参数。忍住冲动,使用的初始条件测试或递归调用不要超过一次,除非你真的知道自己在做什么。
你的目标是勇于进行递归调用,即使在这条想象中的递归路径上,你没有完全搞清楚“自己在哪里”。那么,等你需要为一个真正的项目去写一个函数时,你会从写单元测试开始,并且运用上面提到的相同技术来一步步推进。
6.不信任代码特征
写这样的函数:IsNull() 和 IsNotNull(), 或 IsTrue(bool) 和 IsFalse(bool)。
检查一个布尔变量会不会出现除了 true 或 false 以外的值。
补救措施
别人是按代码行数付钱给你吗?这些旧习惯是不是你从一个拥有弱类型体系的语言中延续下来的?如果两种都不是,那这种情况就类似于“无法推理代码”,但是似乎不是推理能力受损,而是无法信任和适应编程语言。有些特征更像是经不起逻辑分析的“comfort code”,但程序猿非要强迫自己这么写。唯一的补救措施就是,多花时间熟悉编程语言。