🐳 📖

《代码大全》阅读笔记之二:创建高质量的代码

📆 2021-04-30

来源:《代码大全》

🏷 编程阅读

🖍 关于软件构建过程中创建高质量代码的若干建议

🏂 正文 👇

《代码大全》是一本全面介绍软件开发过程中构建相关部分的,集学术界和业界专家的优秀编程实践的,由拥有丰富软件项目经验的业内大佬写给其他一线程序员的实用性编程著作。本书的第一部分主要概述了软件开发周期中,开始构建前的几个准备步骤,以及良好完成这些准备步骤的标志,为真正开始软件构建铺平道路。这一部分,主要是讲如何通过构建前的设计,编写良好的类与子程序,防御式编码以及通过伪代码这种系统性的设计具体代码的工具来创建高质量的代码。正如作者在第一部分所说:使用高质量的实践方法是那些能创造高质量软件的程序员的共性。这些高质量的实践方法在项目的初期,中期,末期都强调质量。所以位于项目中期的构建过程,使用高质量的实践方法来创造出高质量的代码就显得格外重要了。

即使在软件的架构设计或者说高层设计步骤中已经做过了设计工作,在构建前往往也需要进行设计。去构思,创造或发明一套方案,把一份计算机软件的规格说明书要求转变为可实际运行的软件。在设计过程中也会面临大量的挑战:

  • 设计是一个险恶的(wicked)问题,是那种往往只有通过解决或者部分解决才能被明确的问题;
  • 设计是一个了无章法的,需要在反复试错的过程中趋于完善;
  • 设计是在受限的情况下,确定取舍和调整顺序的过程;
  • 它也是一个自然而然形成的不确定的启发式过程;

软件的首要使命是管理复杂度。一个复杂度可控的软件才能更容易地被修改和维护。当项目确由技术因素导致失败时,其原因通常就是失控的复杂度。可以通过两种方法来管理复杂度:一,把任何人在同一时间需要处理的本质性复杂度的量减少到最少;二,不要让偶然性的复杂度无谓地快速增长。理想的设计应该具有以下的特征:

  • 最小复杂度;
  • 易于维护;
  • 松散耦合;
  • 可扩展;
  • 可重用;
  • 高扇入;
  • 低扇出;
  • 可移植;
  • 精简;
  • 有层次;
  • 使用标准的技术;

要在一个软件系统的若干不同细节层次上进行设计。例如:软件系统 --> 子系统和包 --> 包中的类 --> 类中的数据和子程序 --> 子程序。设计过程中可用的一些启发式方法有:

  • 如果是面向对象的设计,那么可以找出在现实世界中可以映射的对象;
  • 要形成一致的,并且在同一层次的抽象,就需要封装实现的细节;
  • 如果继承有利于简化编程并且是正确的选择,那么就可以用继承;
  • 注意信息隐藏,将复杂度和变化源控制起来;
  • 找出容易改变的区域,把不稳定的区域隔离,并限制起来;
  • 保持类与类之间或者子程序与子程序之间的松散耦合;

启发式方法可以让我们期待完成后的设计应该是什么样子,在设计过程中我们可以采取的一些可以获得良好结果的步骤:

  • 迭代,在不同的设计方案中,在不同的做法和不同层次的视角的审视过程中,会有所收获,这有助于改善整体的设计;
  • 分而治之,增量式地改进设计;
  • 自上而下和自下而上的设计相结合;
  • 通过建立试验性原型来小范围,低成本地验证设计;
  • 和其他人合作设计;
  • 设计文档的正规化和所需的细节层次与项目,项目成员等因素相关,总的来说,设计越详细,越充分,越有助于之后的编码;
  • 运用不同的方法记录你的设计成果;

请把设计看成是一个险恶的,杂乱的和启发式的过程,不要停留在你所想到的第一套解决方案,而是去寻求合作,探求简洁性,在需要的时候做出原型,迭代,并进一步迭代,这样做之后,你将对自己的设计成果感到满意。

类是面向对象编程的基础。要想理解面向对象编程,首先要理解抽象数据类型(ADT)。它是指一些数据以及对这些数据所进行的操作集合。这样的集合可以隐藏实现细节;改动也不会影响到整个程序;可以让你像现实世界中那样操作实体,而不用在底层实现上操作它;为程序的其他部分提供了更好的抽象层。创建高质量的类需要创建一个好的接口,提供一致的好的抽象;实现良好的封装,强制阻止其他部分看到类中的细节。具体设计和实现类的时候需要关注:

  • 类中“包含”的数据;
  • 类继承的问题;
  • 成员函数和数据成员;
  • 构造函数等;

我们可能有非常多的原因去创建一个类,不只是对现实世界中物体建模,还包括:降低复杂度,隔离复杂度,隐藏实现细节,代码重用,聚合相关操作等。

高质量的子程序也是高质量代码的重要组成部分。子程序是为实现一个特定的目的而编写的一个可被调用的方法(method)或过程(procedure)。子程序是用以节约空间和提高性能的最重要手段。创建一个子程序有很多正当的理由:

  • 降低复杂度;
  • 引入中间,易懂的抽象;
  • 避免代码重复;
  • 隐藏顺序;
  • 隐藏指针操作;
  • 提高可移植性;
  • 简化复杂的布尔判断;
  • 改善性能等;

好的子程序需要实现功能的内聚性,确保一个子程序仅执行一项操作。好的子程序也需要一个好的名字,指导原则包括:

  • 需要描述子程序所做的所有事情;
  • 避免使用无意义的,模糊或表述不清的动词;
  • 不要仅通过数字来形成不同的子程序的名字;
  • 子程序的命名重点是尽可能含义清晰,所以其长短要视该名字是否清晰易懂而定;
  • 给过程起名时使用语气强烈的动词加宾语的形式;
  • 对于一些有相对动作的操作过程,准确使用对仗词;
  • 为了项目的一致性,为常用操作确立命名规则;

对于子程序的合适的长度来说,如果要编写一段超过 200 行(空行和注释不算)代码的子程序,就要当心了,因为有可能会增加维护成本和提高出错率,同时也可能会遇到可读性的问题。相对于对长度强加限制,要更加关注与复杂度相关的因素。对于子程序的参数来说,要按照一定的顺序来排列,例如,可能是按照“输入 - 修改 - 输出”这样的顺序;在接口中对参数的假定加以说明;把参数个数限制在大约 7 个以内;确保实参与形参想匹配。要考虑函数与过程之间的区别。对于使用宏来说,如果不是万不得已,不要使用宏来代替子程序。

要树立防御式编程的思想:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。更一般地说,其核心想法是要承认程序都会有问题,都需要被修改。所以需要我们在编程的时候,考虑到程序的可修改性和可维护性。对于程序可能接收到垃圾输入的情况,一般有三种处理方法:一,检查所有来源于外部的数据的值;二,检查子程序所有输入参数的值;三,决定如何处理错误的数据。断言是一种可以在开发期间使用的,让程序在运行时自检的技术。可以用它来检查代码中的 bug。我们可以将断言看做是可执行的注解,它能更主动地对程序中的假定做出说明。对于高建壮性的代码,应该先使用断言再处理错误。断言用于处理代码中不应该发生的错误,那么如何处理那些预料中的错误呢?有以下一些方法:

  • 返回中立值;
  • 换用下一个正确的数据;
  • 返回与前一次相同的数据;
  • 换用最接近的合法值;
  • 把警告信息记录到日志文件中;
  • 返回一个错误码;
  • 调用错误处理子程序或对象;
  • 但错误发生时显示出错消息;
  • 用最妥当的方式在局部处理错误;
  • 关闭程序;

要根据出现错误的软件类型来确定最恰当的处理错误方式,有时可能会侧重于正确性,有时可能会侧重于健壮性。应该在整个程序采用一致的方式处理非法的参数。对于抛出异常这种把代码中的错误或异常情况传递给调用方代码的手段要审慎而明智地使用,否则可能会提高代码的复杂度。具体来说有以下一些建议:

  • 用异常通知程序的其他部分,发生了不可忽略的错误;
  • 只有真正例外的情况下才抛出异常;
  • 不能用抛出异常来推卸责任;
  • 在恰当的抽象层次抛出异常;
  • 在异常消息中加入关于异常发生的全部消息;
  • 避免使用空的 catch 语句;
  • 了解所用函数库可能抛出的异常;
  • 考虑创建一个集中的异常报告机制;
  • 把项目中对异常的使用标准化;
  • 考虑异常的替换方案,例如:在局部处理异常;使用错误码来传递错误;在日志文件中记录调试信息;关闭系统等;

创建代码的“隔离带”,将可能发生错误的代码隔离出来,使局部包容由错误造成的损害。以防御式编程为目的而进行隔离的一种方法,是把某些接口选定为“安全”区域的边界,对穿越安全区域边界的数据进行合法性校验,并当数据非法时做出敏锐反应。如果是在类的层次上采用这种方法,类的公有方法可以假设数据是不安全的,它们负责检查数据并进行清理,这样类的私有方法就可以假定数据都是安全的了。隔栏的使用使断言和错误处理有了清晰的区分。隔栏外部的程序应使用错误处理技术,而隔栏内部的程序里就应使用断言技术。如果隔栏内部的某子程序检测到了错误的数据,那么这应该是程序里的错误而不是数据里的错误。区别对待开发版和产品版,不要自动将产品版的限制强加在开发版之上,同时在开发版中使用辅助调试的代码并尽早引入。应该以这么一种方式来处理异常情况:在开发阶段让它尽量显现出来,而在产品代码运行时让它能够自我恢复,这称为“进攻式编程”。最后要确定在产品代码中该保留多少防御式代码,同时要考虑好什么地方需要进行防御,然后因地制宜地调整你进行防御式编程的优先级。

伪代码编程是创建单独类及其子程序的一种系统性方法。创建一个类一般而言是一个迭代过程。先对一个类做总体设计,列出这个类内部的特定子程序,创建这些子程序;然后从整体上复审这个类的构建结果。作为详细设计的工具,“伪代码”是指某种用来描述算法,子程序,类或完整程序的工作逻辑的,非形式的,类似于英语的记法。它需要:

  • 用类似英语的语句来精确描述特定的操作;
  • 避免使用目标编程语言中的语法元素;
  • 去描述解决问题的方法的意图,而不是去写如何在目标语言中实现这个语法;
  • 要不断的细化精化伪代码,直到看起来很容易直接写出代码为止;

伪代码写好后,还可以直接变成代码中的注释。通过伪代码编程过程创建子程序需要:一,先设计这个子程序;二,用高层次的伪代码来写程序,可以由一段头部注释开始。描述这段程序应该做些什么,首先简要地用一句话来写出该程序的目的。然后就可以为这个子程序编写高层次的伪代码,持续地精化和分解伪代码,直到你满意为止;三,在伪代码的基础上编写子程序代码:写出子程序的声明;编写首尾语句;将伪代码转换成高层次的注释;填充代码;检查代码。用伪代码编程的方法可以让你脱离那种先东拼西凑,然后通过编译运行来看代码是否能工作的怪圈。此外还存在一些其他的可以替代伪代码编程的方案:测试先行开发;重构;契约式设计和试图通过东拼西凑来写出能工作的代码等。

以上就是第二部分的主要内容。这部分给我感触最深,收获最大的是对于构建设计的复杂性的探讨和使用伪代码编程逐步编写具体代码的系统性方法这两点。构建设计自身本质的复杂性,特异性和不确定性,使得并不存在设计上的银弹;而通过伪代码逐步实现代码设计的系统性方法,可以让我们避免东拼西凑的试探性编程,让具体的编程过程更加合理和可控,值得在之后的编程实践中多加练习,使自己成为更加专业可靠的程序员。

👐 The End 🎉

上一篇 《代码大全》阅读笔记之三:变量🏠下一篇《代码大全》阅读笔记之一:打好基础 

👇 💬