首页 > 代码库 > 《程序员修炼之道》笔记(三)
《程序员修炼之道》笔记(三)
第四章 注重偏执的实效
“你不可能写出完美的软件”,我们要把这句话视为生活的公理,并接受它、拥抱它。
但同时,有一些方法可以尽量把这个事实转变为有利条件
作者用开车来类比写程序:每个人都知道只有他们自己是地球上的好司机,于是我们防卫性地开车,小心谨慎以避免麻烦发生,预判意料之外的事,尽量不让自己陷入无法解救自己的境地。编码也类似,我们不断地与他人的代码结合——可能不符合我们的高标准的代码——并处理可能有效也可能无效的输入。所以,我们要防卫性地编程。使用断言检测坏数据,检查一致性并在数据库的列上施加约束。
但注重实效的程序员更进一步,他们连自己也不信任。知道没人能编写完美的代码,包括自己,所以要针对自己的错误进行防卫性地编码。
采用防卫性编程的方式,可以帮助我们应对不完美的系统、荒谬的时间标度、可笑的工具、还有不可能实现的需求,——在这样一个世界中,让我们安全“驾驶”。当每个人都确实要对你不利时,偏执就是一个好主意。
1. 按合约设计
a) 没有什么比常识和坦率更让人感到惊讶,确保坦率的最佳方案之一就是合约。
b) 合约既规定你的权利与责任,也规定对方的权利与责任。此外,还有关于任何一方没有遵守合约后果的约定。
c) 除了在人与人之间使用,合约也可以帮助软件模块进行交互。
d) DBC 按合约设计(Design By Contract)
1) 用文档记载并约定软件模块的权利与责任,以确保程序的正确性。用文档记载这样的说明,并进行校验,是按合约设计的核心所在。而关于什么是正确的程序,作者的观点是:不多不少,做它声明要做的事情的程序。
2) 软件模块在开始做某件事之前,对输入状态有某种期望,在执行结束后,会产出特定的陈述。这就涉及到前条件、后条件和类不变项。
前条件(Precondition)为了调用例程,必须为真的条件;这是例程要求的输入。在其前条件被违反时,例程不应该被调用。传递好数据是调用者的责任
后条件(Postcondition) 例程保证会做的事情,例程完成时给出的结果。例程有后条件这一事实意味着他会结束,不允许有无限循环
类不变项(Class Invariant)。类确保从调用者的视角来看,该条件总是为真。在例程的内部处理过程中,不变项不一定会保持,但在例程退出、控制返回到调用者时,不变项必须为真,对这一点的理解觉得可以为:类的行为中需要遵守的约定,比如操作一个List,我们可以规定插入新的项时,它不能是已经存在的。这一规定作为一个不变项将应该总是被遵守。而为了更好得保证这一点,可以用Dictionary类型替换List。
3) 例程与任何潜在的调用者之间的合约可解读为:如果调用者满足了例程的所有前条件,例程应该保证在其完成时,所有后条件和不变项将为真。
如果任何一方没有履行合约的条款,(先前约定的)某种补偿措施就会启用,例如异常或终止程序。不管发生什么,不要误以为没能履行合约是bug,它不是某种绝不应该发生的事情,这也就是为什么前条件不应被用于完成像用户输入验证这样的任务的原因。
编写“懒惰”的代码,对在开始之前接受的东西要严格,而允诺返回的东西要尽可能少。
4) 继承和多态作为面向对象语言的基石,是合约可以真正闪耀的领域。例如,继承有Liskov替换原则:子类必须要能通过基类的接口使用,而使用者无须知道其区别。这便是里氏转换的内容了。
5) 使用DBC的好处是它迫使需求与保证的问题走到前台来,在设计时简单地列举输入域的范围是什么、边界条件是什么、例程允许交付什么(或者不允许交付什么)。如果没有想清楚这些,就是在靠巧合编程,这是许多项目开始、结束、失败的地方。此外,虽然用文档记载这些假定已经很管用,但让编译器帮忙检查合约会有更好的效果。使用断言可以对此进行部分的模拟。
2. 死程序不说谎
a) 尽早检测问题,尽早崩溃。有许多时候,让你的程序崩溃是最佳选择。死程序带来的危害通常比有疾患的程序要小得多。
b) 但也有时候,不能简单退出,因为在退出前需要释放申请的资源、需要写日志、处理事务、与其它进程交互等。
3. 断言式编程
a) 在自责中有一种满足感。当我们责备自己时,会觉得在没人有权责备我们。
b) 如果它不可能发生,用断言确保它不会发生。
c) 断言可能在编译时被关闭,所以不要把必须执行的代码放在assert中。
d) 虽然断言会影响性能,但最好保持运行。不要以为有测试就够了,测试不能保证覆盖全部情况,而且真实环境非常复杂,就算开发时已经没问题,但就像一个人又一次成功走过了钢丝,不一定以后就不再需要防护器材了。写程序时最缺的就是走钢丝的心态,也许就是因为这种心态,很多程序员guru们在原始的语言、简陋的编译器、有限的计算资源下,写出的却是非常严密高效的软件,例如高德纳的Tex排版软件,从发版至今的几十年间,一共只出现过15个bug。
e) 断言对性能会有一定影响,但除了对性能影响很大的断言,最好在编译时打开其余的断言。
4 何时使用异常
a) 检查没有可能的错误,特别是意料之外的错误,是一种良好的实践,但是,在实践中这可能会把我们引向相当丑陋的代码,为了编码这种情况,可以使用异常。
b) 异常很少应作为程序的正常流程使用,它应保留给意外事件。
c) 异常表示即时的、非局部的控制转移,这是一种级联的goto,那些把异常用作正常处理的一部分的程序,将遭受到经典的意大利面条式代码的所有可读性和可维护性问题的折磨。这些程序破坏了封装,通过异常处理,例程和它们的调用者被更紧密地耦合在一起。
5 怎样配平资源
a) 资源(内存、事务、线程、文件、定时器)的使用要遵循一种可预测的模式,你分配资源,使用它,然后解除其分配。也就是说,对于资源的使用,要有始有终。分配某项资源的例程或对象应该负责解除该资源的分配。
b) 嵌套的分配:对于一次需要不只一个资源的例程,有另外两个建议
1) 以与资源分配的次序相反的次序解除资源的分配,这样,如果一个资源含有对另一个资源的引用,你就不会造成资源被遗弃
2) 在代码的不同地方分配同一组资源时,总是以相同的次序分配它们,这将降低发生死锁的可能性
c) 资源的分配的解除的对称,类似类的构造器和析构器。面向对象语言中,将资源封装在类中,可自动配平。而在包含异常处理的代码中,可以在finally中释放资源。
欢迎关注我的个人公众号【菜鸟程序员成长记】
《程序员修炼之道》笔记(三)