软件构造-MIT Readings 阅读总结

测试:

  • 测试优先编程——在写代码前先写好测试用例,尽早发现bug。
  • 利用分区与分区边界来选择测试用例。
  • 白盒测试与声明复盖率。
  • 单元测试——将测试模块隔离开来。
  • 自动化回归测试杜绝新的bug产生。

还记得好软件具备的三个属性吗?试着将它们和测试联系起来:

  • 远离bug 测试的意义在于发现程序中的bug,而“测试优先编程”的价值在于尽可能早的发现这些bug。
  • 易读性 测试并不会使代码审查变得容易,但是我们也要注意正确书写测试注释。
  • 可改动性 我们针对改动后的程序进行测试时只需要依赖规格说明中的行为描述。(这里的测试针对的是正确性而不是鲁棒性)。另外,当我们完成修改后,自动化回归测试能够帮助我们杜绝新的bug产生。

代码评审:

代码评审是一种广泛应用的软件质量提升方法。它可以检测出代码中的各种问题,但是作为一个初学课程,这篇阅读材料只提及了下面几个好代码通用的原则:

  • 不要重复你的代码(DRY)
  • 仅在需要的地方做注释
  • 快速失败/报错
  • 避免使用幻数
  • 一个变量有且仅有一个目的
  • 使用好的命名
  • 避免使用全局变量
  • 返回结果而非打印它
  • 使用空白符提升可读性

下面把代码评审和我们的三个目标联系起来:

  • 远离bug. 通常来说,代码评审使用人的审查来发现bug。DRY使得你只用在一处地方修复bug,避免bug的遗漏。注释使得原作者的假设很清晰,避免了别的程序员在更改代码的时候引入新的bug。快速报错/失败使得bug能够尽早发现,避免程序一直错更多。避免使用全局变量使得修改bug更容易,因为特定的变量只能在特定的区域修改。
  • 易读性. 对于隐晦或者让人困惑的bug,代码评审可能是唯一的发现方法,因为阅读者需要尝试理解代码。使用明智的注释、避免幻数、变量目的单一化、选择好的命名、使用空白字符都可以提升代码的易读性。
  • 可更改性. DRY的代码更具有可更改性,因为代码只需要在一处进行更改。返回结果而不是打印它使得代码更可能被用作新的用途。

规格说明:

一个规格说明就好像是实现者和使用者之间的防火墙。它使得分别开发成为可能:使用者可以在不理解源代码的情况下使用模块,实现者可以在不知道模块如何被使用的情况下实现模块。

规格说明和我们三大目标之间的联系:

  • 远离bug. 一个好的规格说明会清晰明确的要求实现者和使用者遵守相关的制约。而Bug经常是因为实现者和使用者对于接口的理解冲突导致的,规格说明会明显的减小这种可能性。在模块中使用一些能够交由机器检查的特性,例如静态检查、异常等而不是注释会进一步降低bug的可能性。
  • 易读性. 一个简洁准确的规格说明会比源代码本身更易读易懂。
  • 可改动性. 规格说明在实现者和使用者之间建立了一个“契约”——只要这两方遵守这份“契约”,他们可以对自己的代码进行任何改变。

设计规格说明:

规格说明在使用者和实现者之间起着一道防火墙的作用——对于人和代码之间也是一样。正如上篇阅读谈到的规格说明,这使得独立开发成为可能:使用者可以在不阅读模块源码的情况下将源码应用到各个地方,使用者可以不在意模块被使用的环境(只要他们都遵循规格说明的要求)。

在实际使用中,声明性的规格说明是最重要的。前置条件(弱化规格说明)使得使用者更困难(确保输入合法),但是合理的使用会使得实现者能够做出一些假设,从而选择更合适的实现方案。

和我们的三个目标联系起来:

  • 远离bug. 如果没有规格说明,即使是最小的更改都有可能使得整个程序崩溃,改动起来也是很麻烦的。一个结构良好、逻辑明确的规格说明会最小化使用者和实现者之间的误解,并帮助我们进行静态检查、测试、代码评审等等。
  • 易于理解. 一个好的规格说明会让使用者不必去阅读源码也能正确安全地使用模块。例如,你可能永远不会去阅读Python dict.update ,但是通过阅读对应的声明性规格说明你就能很好的应用它。
  • 可改动性. 一个合理的“弱”规格说明会给实现者一定的自由,而一个“强”的规格说明会给使用者一定的自由。我们甚至可以改变规格说明本身:只要我们是加强了它而不是削弱了它(减弱前置条件或者增强后置条件)。

可变性与不变性:

在这篇阅读中,我们看到了利用可变性带来的性能优势和方便,但是它也会产生很多风险,使得代码必须考虑全局的行为,极大的增加了规格说明设计的复杂性和代码编写、测试的难度。

确保你已经理解了不可变对象(例如String)和不可变索引(例如 final 变量)的区别。画快照图能够帮助你理解这些概念:其中对象用圆圈表示,如果是不可变对象,圆圈有两层;索引用一个箭头表示,如果索引是不可变的,用双箭头表示。

本文最重要的一个设计原则就是不变性 :尽量使用不可变类型和不可变索引。

接下来我们还是将本文的知识点和我们的三个目标联系起来:

  • 远离bug.不可变对象不会因为别名的使用导致bug,而不可变索引永远指向同一个对象,也会减少bug的发生。
  • 易于理解. 因为不可变对象和索引总是意味着不变的东西,所以它们对于读者来说会更易懂——不用一边读代码一边考虑这个时候对象或索引发生了哪些改动。
  • 可改动性. 如果一个对象或者索引不会在运行时发生改变,那么依赖于这些对象的代码就不用在其他代码更改后进行审查。

避免调试:

在这篇阅读中,我们介绍了几种最小化调试代价的方法:

  • 避免调试
    • 使用静态类型检查、动态检查、不可变类型和不可变索引让bug无法产生。
  • 限制bug范围
    • 通过断言检查、快速失败让bug的影响不扩散。
    • 通过增量式开发和单元测试让bug尽量只存在于刚刚修改的代码中。
    • 最小化变量作用域使得搜寻范围减小。

最后还是将这次阅读的内容和我们的三个目标联系起来:

  • 远离bug. 本阅读的内容就是如何避免和限制bug。
  • 易于理解. 静态类型检查、final以及断言都是额外的“注释”——它们体现了你对程序状态的假设。而缩小作用域使得读者可以更好的理解变量是如何使用的,因为他们需要浏览的代码范围变小了。
  • 可改动. 断言检查和静态检查都是能够自动检查的“假设”,所以如果未来有一个程序员错误改动了代码,那么违背假设的错误就能马上检测到。

抽象数据类型:

  • 抽象数据类型(ADT)是通过它们对应的操作区分的。
  • 操作可以分类为创建者、生产者、观察者、改造者。
  • ADT的标识由它的操作集合和规格说明组成。
  • 一个好的ADT应该是简单,逻辑明确并且表示独立的。
  • 对于ADT的测试应该对每一个操作进行测试,并同时利用到创建者、生产者、观察者、改造者。

将本次阅读的内容和我们的三个目标联系起来:

  • 远离bug. 一个好的ADT会在使用者和实现者之间建立“契约”,使用者知道应该如何使用,而实现者有足够的自由决定具体实现。
  • 易于理解. 一个好的ADT会将其内部的代码和信息隐藏起来,而使用者只需要理解它的规格说明和操作即可。
  • 可改动. 表示独立使得实现者可以在不通知使用者的情况下对ADT内部进行改动。

抽象函数与表示不变量:

  • 不变量是指对于一个对象,它有一种能够在整个生命周期保证为真的属性。
  • 一个好的ADT会确保它的不变量为真。不变量是由创建者和生产者创建,被观察者和改造者保持。
  • 表示不变量明确了什么是合法的表示值,并且这些表示应该在运行时调用checkRep()检查。
  • 抽象函数将具体的表示映射到抽象值上。
  • 表示暴露会威胁到表示独立性和表示不变量。

下面将这篇阅读的知识点与我们的三个目标联系起来:

  • 远离bug. 一个好的ADT会确保它的不变量为真,因此它们不会被使用者代码中的bug所影响。同时,通过显式的声明和动态检查不变量,我们可以尽早的发现bug,而不是让错误的行为继续下去。
  • 易于理解. 表示不变量和抽象函数详细的表述了抽象类型中表示的意义,以及它们是如何联系到抽象值的。
  • 可改动. 抽象数据类型分离了抽象域和表示域,这使得实现者可以改动具体实现而不影响使用者的代码。

接口与枚举:

抽象数据类型是由它支持的操作集合所定义的,而Java中的结构能够帮助我们形式化这种思想。
这能够使我们的代码:

  • 远离bug. 一个ADT是由它的操作集合定义的,而接口就是做了这件事情。当使用者使用接口类型时,静态检查能够确保它们只使用了接口规定的方法。如果实现类写出了/暴露了其他方法——或者更糟糕,暴露了内部表示——,使用者也不会依赖于这些操作。当我们实现一个接口时,编译器会确保所有的方法标识都得到实现。
  • 易于理解. 使用者和维护者都知道在哪里寻找ADT的规格说明。因为接口没有实例成员或者实例方法的函数体,所以它能更容易的将具体实现从规格说明中分离开。
  • 可改动. 我们可以轻松地为已有的接口添加新的实现类。如果我们认为静态工厂方法比类构造方法更合适,使用者将只会看到这个接口。这意味着我们可以调整接口中工厂方法的实现类而不用改变使用者的代码。

Java的枚举类型能够定义一种只有少部分不可变值的ADT。和以前使用特殊的整数或者字符串相比,枚举类型能够帮助我们的代码:

  • 远离bug. 静态检查能够确保使用者没有使用到规定集合外的值,或者是不同枚举类型的值。
  • 易于理解. 将常量命名为枚举类型名字而非幻数(或其他字面量)能够更清晰的做自我注释。
  • 可改动. 无

调试:

在这篇阅读中,我们学习了如何系统的进行调试:

  • 构建测试用例复现bug,并将其添加到测试套件中
  • 使用科学的方法发现bug:
    • 调试提出假设
    • 利用探针(print、assert、debugger)来观察程序的行为并测试假设的前置条件是否满足
  • 彻底而非草率的修复bug

对于我们课程的三个目标,这篇阅读主要针对的是远离bug:我们试着剔除bug,并利用回归测试防止bug重新出现。

相等:

  • 相等应该满足等价关系(自反、对称、传递)。
  • 相等和哈希必须互相一致,以便让使用哈希表的数据结构(例如 HashSet 和 HashMap)正常工作。
  • 抽象函数是不可变类型相等的比较基础。
  • 索引是可变类型相等的比较基础。这也是确保相等一致性和保护哈希表不变量的唯一方法。

相等是实现抽象数据类型中的一部分。现在我们将本文的知识点与我们的三个目标联系起来:

  • 远离bug. 正确的实现相等和哈希对于聚合类型的使用很重要(例如集合和映射),这也是写测试时很需要的。因为每一个对象都会继承Object中的实现,实现不可变类型时一定要覆盖它们。
  • 易于理解.使用者和其他程序员在阅读规格说明后会期望我们的ADT实现合理的相等操作。
  • 可改动. 为不可变类型正确实现的相等操作会把索引相等和抽象值相等分离,也对使用者隐藏对象是否进行了共享。为可变类型选择行为相等而非观察相等帮助我们避开了隐秘的bug。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章