1.无法从集合的角度思考
从命令式编程过度到函数式和声明式编程,会立刻要求你思考将数据集合当作原语来操作,而不是作为标量值。无论你在关系型数据库(并且不是作为一个对象仓库)中使用 SQL,还是设计规模会随多处理器线性变化的程序,亦或是你写的代码必须要在拥有 SIMD 能力的芯片(比如现代显卡和电子游戏机)上执行,都需要这种过渡。
特征
只有在带有声明式或函数式编程特性(程序猿应该知道这些特性)的平台上看到这些特征时,下面列出的才算数。
在 for 或 foreach 的循环里对集合中的每个元素执行原子操作。
写的 Map 或 Reduce 函数里包含自定义的循环,在循环里逐一重复执行数据集。
从服务器抓取大量数据集,并在客户端上计算和,而不是在查询里使用聚集函数。
函数作用于一个集合中的每个元素,并在函数开头通过执行一次新的数据库查询来抓取一个关联记录。
写的业务逻辑函数,例如更新一个用户界面或执行文件 I/O,很不幸地为了某种折衷而伴随有副作用。
在实体类打开专属的数据库连接或文件操作符,并且在每个对象的生命周期里都保持连接状态。
补救措施
非常有趣,想象着一个发牌的人通过手指在牌里翻转把一副牌切成两堆交叉洗牌,就能让大脑联想到集合,以及如何成批地操作集合。激发人联想的其他想象还有:
高速公路上的车流通过一系列收费站(并行处理)。
泉水汇聚成溪流,溪流又汇聚成小河,最后再汇聚成江河(并行分解/聚集函数)。
一个报纸印刷机(协同程序、流水线)。
夹克上的拉链头把拉链齿拉上(简单的联结)。
转移 RNA 加上氨基酸,并在一个核蛋白中加入信使 RNA,就变成了蛋白质(多阶段函数驱动联结,详见 animation)。
在一棵橘子树中,数以亿计的细胞里同时发生着上述的过程,不断地将空气、水和阳光转换成橘子汁(大型分布式集群上的 Map/Reduce)。
如果你正在写一个处理集合的程序,思考一下所有的附加数据和记录,你的函数需要操作它们的每一个元素。并且在 Reduce 函数应用到每对数据上之前,使用 Map 函数把它们成对地联结在一起。
2.缺乏批判性思维
除非你能批判自己的思维并从中找出缺陷,否则你会错过那些可以在敲代码之前就能解决的问题。如果你也无法评判自己曾经写过的代码,那你只能在不断摸索中以龟速学习。这个问题同时来源于思考怠惰和以自我为中心,因此,这个问题的特征似乎也来自两个不同的方向。
特征
自制“业务规则引擎”。
静态工具类很冗余且庞大,或者多学科的函数库只用一个命名空间。
把各种应用糅合在一起,或给当前的应用附加不相关的特性来避免启动新项目的开销。
程序架构开始需要建立 epicycle 模型。(译者: epicycle 模型是天文学上使用的模型,用来解释天体在运动过程中出现的偏差等异常行为。)
为了很不相关的数据向表中添加字段(比如:在通讯录的表中放置“# cars owned”字段)。(译者:通讯录的内容要记录是否有车干嘛,确实扯远了。)
前后矛盾的命名规范。
处于“拿着锤子看什么都是钉子”的心态,或者改变对问题的定义,这样所有问题都能用某个特定的技术来解决。
编写程序降低问题的复杂度。
从病理上冗余地防御式编程(“企业级代码”)。
用 XML 重新发明 LISP。
补救措施
从Paul 和 Elder 写的《批判性思维 | Critical Thinking》这样的书入手,控制自我意识,在向朋友或同事发表自己的想法以此寻求评论时,练习抵制为自己辩护的冲动。
一旦你习惯了别人来检验你的想法,你就会开始自我审视并练习想象这些想法的结果。另外,你也需要培养起区别轻重缓急的能力(能直觉知道对这种规模的问题,需要花费多少精力比较合适)、用实践验证假设的习惯(这样你就不会高估问题的大小)和面对失败的健康心态(就算艾萨克.牛顿的地心引力说是错的,但我们依然爱他并需要他去尝试)。
最后,你必须自律。意识到计划里有缺陷不会让你更高效,除非你有足够的意志力去改正缺陷,并重建手中正在进行的工作。
3.弹球式编程
如果你把面板倾斜得刚刚好,把曲柄拉回到刚好的距离,并且以正确的顺序击中那些凸起的按钮,那么程序就会像弹球一样运行无误:随着指令的执行流程,从条件语句返回,跳过未选中的指令,转向下一次的状态转换。
特征
用一个 try-catch 代码块包围 Main() 的整个函数体,并在 Catch 分句中重置整个程序(像弹球地沟,掉下去以后重新开始游戏)。
在强类型的语言中,用字符串或整型来存储那些拥有(可以用)更合适封装类型的值。
把复杂数据打包成带分隔符的字符串,然后在使用它的每个函数里解析一遍。
对输入有歧义的函数,不会用断言(assertion)或方法协定(method contract)。
使用 Sleep() 来等待另一个线程完成任务。
对非枚举类型的值使用 switch 语句,而且分支语句中没有“Otherwise”分句。
用 Automethods 或 Reflection 来调用在非法的用户输入中提到的方法。
在函数里通过设置全局变量来返回多个值。
类里有一个方法和几个字段,通过设置字段来为方法传递参数。
不用事务来更新多行数据库内容。
孤注一掷(比如,试图不用事务和 ROLLBACK 来恢复数据库的状态)。
补救措施
把程序的输入想象成水。它即将流过每一个缝隙,灌满每一个容器。那么你要想一想,如果它流过的地方并没有明确创建任何东西去呈接它的话,会造成什么后果。
你要让自己熟悉平台的机制,这有助于写出健壮且易扩展的程序。共有三种基础机制:
当某种意外发生时,能在产生任何破坏之前停止程序,然后帮助你识别出是哪里出错了(类型体系、断言、异常等)。
将程序的执行导向处理意外最佳的代码块( try-catch 模块、多重分发、基于事件驱动编程等)。
暂停线程直到一切就绪(WaitUntil 命令、互斥锁和信号量、同步锁等)。
还有第四条,单元测试,你可以在设计阶段使用。
使用这些机制应该成为你的第二天性,就像在句子里用逗号和句号一样。为了做到这些,每次浏览一遍上面介绍的机制(括号里提到的那些),并重构你的旧程序,把提到的这些机制塞到任何能塞的地方,就算最后发现这么做并不合适(尤其是在它们看似不合适的时候,至少那时你也开始明白其中的缘由)。
4.不熟悉安全原则
如果要说下述特征并不很严重,但它们几乎是大部分程序都存在的整体质量问题。意思是说,这些特征不会让你成为一名很糟糕的程序猿,只是意味着你不应该从事网络程序或安全系统的工作,直到你已经在这方面做了一些功课。
特征
以明文形式存储可利用信息(名字、卡号、密码等)。
用低效的加密术存储可利用信息(将密码编译在程序中的对称加密算法;简单密码;任何“解码环( decoder-ring )”、自创加密算法、专有的或未验证的加密算法)。
在接受网络连接或解释来自非置信源的输入信息之前,程序或设备没有限制它们的权限。
不进行边界检查或输入合法性验证,尤其是在使用非托管类的语言时。
把不合法或非转义的输入串接到字符串上来构建SQL查询。
调用用户输入中指定的程序。
试图通过搜索已知漏洞的签名(signature)来阻止漏洞被利用。
用不加盐的哈希值(unsalted hash)存储信用卡卡号或密码。
补救措施
下面只涵盖了基本原则,但遵照这些原则会避免绝大多数臭名昭著的错误,那些错误可以让整个系统大打折扣。对于任何处理或存储有价值信息的系统,无论是向你还是其用户,或是控制一个贵重资源的系统,它们通常都有一个安全专家来审查系统的设计与实现。
从审查程序开始,找出用数组或其他配置内存的容器来存储输入的代码,确保这部分代码检查了输入的大小不会超出分配给它的内存大小。没有其他类型的 bug 能比缓冲区溢出更能导致可利用的安全漏洞。从某个层面来说,在写网络通信程序或任何安全第一的场合下,你应该认真考虑使用某种内存托管型的编程语言。
下一步,审查数据库查询操作。审查那些将未修改输入串接到 SQL 查询内容中的查询操作,并且,如果平台支持的话就切换为使用参数化查询,如果不支持就对输入进行过滤或转义。这么做是为了防止 SQL 注入攻击。
在你清除了这两类最臭名昭著的安全 bug 之后,你应该继续将所有的的程序输入视为完全不可靠,或是有潜在恶意。按有效的验证规则来定义程序的输入很重要,而且除非输入能通过验证,否则程序应该拒绝它,这样你就能够通过修复验证方法并使其更加明确来修复可利用的漏洞,而不是通过扫描已知漏洞的签名来修复漏洞。
进一步说,你应该总是在开始设计程序之前,思考程序需要执行的操作以及这些操作需要从 host 获得什么样的权限,因为这个时候是想出怎么样能尽可能使用最少权限的最佳时机。这条建议背后的原则是,如果在你的代码中找到一个可利用的 bug ,限制这个bug可能对系统其他部分造成的损害。换言之:在你学会不信任输入之后,你也应该学会不要信任自己写的程序。
最后你要学会的是数据加密基础,从《Kerckhoff’s principle》开始。这一点亦可表达为“安全第一”,从中还衍伸出了一些有趣之处。
原则一,永远不要信任一个密码或其他加密原语,除非它已经被公开发表,并且已经由更高级别的安全社区对其进行了全面的分析和测试。从密码学的发展来看,模糊晦涩的加密法、专有的加密法或是新出现的加密法都毫无安全可言。即使是可信的加密原语,其实现中也会存在缺陷,因此,对于你不能确定其已经得到全面审查的加密算法(包括自己实现的版本),要避免使用。所有的新型加密系统都要经过一系列的详细审查,这个过程可能长达十年之久,或更长,而你只要关注那些最后经受住了审查并且所有已知错误都已修复的加密系统。
原则二,如果密钥容易破解或存储失当,那这和完全不加密一样糟糕。如果程序要对数据加密,但不需要解密或很少需要解密,那就考虑只把对称加密密钥对的公钥给它,并让解密阶段和私钥分开运行,用户必须每次输入一个好的口令来确保密钥的安全。
越是处于危险之中,你需要做的功课越多,并且必须在程序的设计阶段投入更多精力。这都是因为一旦你的程序部署下去,就会有成堆、有时候可能是成千上万的不速之客试图去破坏它的安全性。
绝大部分可追溯到代码问题的安全故障都归因于一些很愚蠢的错误,其中大部分错误可以通过筛选输入、谨慎使用资源、利用常识、想清楚再写代码等方式来避免。
5. 代码一塌糊涂
特征
不遵循一贯的命名规范。
不使用缩进,或缩进不一致。
不使用空格,例如在方法之间不加空格(或表达式里不加空格,看“ANDY=NO”)。
有一大堆被注释掉的代码。
补救措施
程序猿在匆忙之下(或特殊情况下)犯了上述所有毛病的话,会在之后返回来清理,但一个糟糕的程序猿真的就只是粗心大意。有时,利用可通过快捷键来修复缩进和空格(“美观的格式”)的 IDE 是很帮助的,但我发现程序猿总是把代码搞得一团糟,极大地违背 Visual Studio 对适当缩进的坚持。(http://www.thinksite.cn/)