优秀的程序员如何清晰表达代码的意图

jopen 9年前

当读者用所喜爱的IDE来工作,并用到了其中各种吸引人的附加功能(语法检查、自动完成、静态分析和其他功能)时,会不会感叹缺少一个尚未被发明的 特定功能?对,笔者指的就是意图检查器。对此大家都很了解。当我们在思考时,就会需要这样的功能:“我希望它能按照我的意思而不是按照我敲的东西来编 程!”或许在奋力编写一个棘手的算法时,就会想要这个功能。可能在调用了这个功能后,就发现了一个愚蠢的敲错一个字符这样的bug。不管在什么情况下,所 面临的就是将意图转化成实现的复杂性。

另一方面,大家以前都问过像这样的问题:“这段代码是做什么的?”或者更极端地问:“这个开发人员当时是怎么想的?”测试所做的所有事情,就是要验 证实现与显性或隐性的开发意图之间的匹配性:显性表现在代码应该要完成一些目标;隐性表现在代码在完成上述目标时,也应该具有特定的可用性、强壮性等这些 特征,而不管这些是否被特别地考虑过[ 重要的是要考虑到,上述意图会发生在软件开发过程的许多层面上。开发人员将用户所想要的,转化为前者所认为后者所想要的,哪些功能应该被添加以解决后者的 需要,如何将这些特性映射到现有或期望的应用上,如何将这些纳入系统的架构和设计之中,以及如何编写代码。本章所讨论的许多观点,都可以外推到其他层面 上。]。

意图都被放到哪里去了

意图是一个捉摸不定的东西。在更广阔的社会里,存在着“意图式生活”(intentional living)这个词。在实践意图式生活时,会试图把所做的每一个行为当作是刻意的,而不是当作习惯性的或偶然的,同时要考虑到行为发生的地点和在这些行 为的背景下会出现的后果。意图的明确性也常常会与极简主义的那些方法联系起来。上述道理虽然显而易见,但是要实践这种生活,需要用更多的纪律和努力。

软件需要同样的专注力。笔者在参与开发了多个有关人身安全方面的项目和产品之后,对于所写代码的后果变得很敏感。这些后果可以在某种程度上扩展到整 个软件。如果电子邮件程序将邮件发送给了错误的收件人,那么就会违反保密性、破坏信任,有时还会带来重大的金融和政治影响。一个字处理程序的恼人崩溃似乎 微不足道,特别是在没有造成数据丢失的情况下,但如果这种情况出现几百万次,那么它就会演变成大量的刺激,丧失生产力。

极简主义也适用于软件。代码编写得越少,要维护的内容也就越少。要维护的内容越少,必须要理解的内容也就越少。必须要理解的内容越少,犯错误的机会也就越少。更少的代码成就更少的bug。

笔者有意囊括了超乎安全性、金钱和生产力之外的因素。渐渐地,软件被集成到了周围一切的事物中,包括被集成到伴随日常生活的各种设备中。这种无处不 在使得软件对我们的生活质量和精神状态所施加的影响不断增大。所以上文中包含了像“信任”和“刺激”这样描述影响的字眼。产品的意图包含了非功能性的方 面。那些取得巨大成功的公司,不仅撷取了用户的头脑,也俘获了他们的内心[ 若想深入了解这个话题,可以参考Wiley出版社1995年出版的Alan Cooper的著作《交互设计精髓》(About Face)及其后续版本。]。

将意图与实现分离

实现仅仅是完成意图的众多方式中的一种而已。如果一个人能够对实现与意图之间的边界有一个清楚的认识,那么他在编写和测试软件时就能有一个更好的心 智模式(mental model)。实现与意图极其相像的情况屡见不鲜。例如,一个“奖励获胜玩家”的意图可以实现为“查询获胜用户,遍历他们,然后为每人的成绩清单中添加一 个徽章”。实现的语言与意图的表述紧密对应,但这两者并不相同。

明确地划分意图与实现之间的界限,能有助于将测试工作的规模与软件相匹配。在不掺杂实现元素的前提下,对意图测试得越多,测试耦合到代码的情况就越 少。耦合得少了,就不用被迫随着实现中的变化更新或重写测试了。测试改变得越少,花费在测试上的精力也就越少,而这会增加测试保持正确的可能性。所有这一 切都会使得在验证、维护和扩展软件上的花费更少,在长远来看更是如此。

也可以将代码的意图与功能相分离。此处所说的分离是将代码原本的工作用意与它实际的行为分隔开。当需要测试实现时,应该对代码本应做的事情进行测 试。而对代码所编写出的那些行为进行测试时,若该代码编写得不正确,那么这种测试就会造成一个安全的假象。一个运行通过的测试会告诉我们一些有关代码质量 和代码与目的之间契合度的信息。而一个不应运行成功的测试若运行通过了,那么该测试就会在上述信息上对我们撒谎。

当编写代码时,要使用编程语言和框架中的特性来最清晰地表达意图。在Java语言中将变量声明为final或private,在C++中声明为 const,在Perl中声明为my,或者在JavaScript中声明为var,都是表达了有关该变量用途的意图。在像Perl和JavaScript 这样带有弱参数需求的动态语言中,在Perl中传递哈希参数[PBP]和在JavaScript中传递对象参数[JTGP]时,参数值本身的命名能用于在 代码内部更加清晰地记录意图。

一个能引发思考的简单例子

让我们看看一个使用Java语言的例子。代码清单2-1显示了一个简单的Java类,该类带有几条线索,展示了在构造该类时所表现出来的意图。考虑 ScoreWatcher类,它是一个跟踪体育比赛分数系统的一部分。它封装了从一个新闻源(a news feed)获得比赛分数的功能。

代码清单2-1 一个简单的Java类展示了带有意图的构造

class ScoreWatcher {    private final NewsFeed feed;    private int pollFrequency; // Seconds    public ScoreWatcher(NewsFeed feed, int pollFrequency) {      this.feed = feed;      this.pollFrequency = pollFrequency;    }    public void setPollFrequency(int pollFrequency) {      this.pollFrequency = pollFrequency;    }    public int getPollFrequency() {      return pollFrequency;    }    public NewsFeed getFeed() {      return feed;    }    ...  }

首先,看一下该类所定义的那些属性(attribute)。编写该类的作者把feed定义为final[ 在Java语言中,final关键字本身就表明了,由它所声明的变量一旦初始化,其值就不能再改变了。]的属性,却没有把pollFrequency也定 义为final。这告诉了我们什么?它表达了这样一个意图:feed应该只能在类构建时被赋值一次,但pollFrequency能够在该对象的整个生命 周期中被修改。接下来,在代码中所看到的pollFrequency同时具备getter和setter,而feed仅有一个getter,又强化了这一 点。

但这仅仅让我们了解了实现上的意图。上面的做法可能会支持哪个功能性的意图呢?可以根据代码的上下文做出一个合理的结论,即对于每一个能够使用的新 闻源,应该只恰好分配一个类来封装它。还可以继续推论,或许对于每一个要被监测的比赛分数,也应该恰好只存在一个用来初始化ScoreWatcher的 NewsFeed。还可以继续推测,如果存在多个新闻源,那么多个源的管理可能会隐藏在一个新闻源的接口后。这一点需要验证,但是在目前的情况下看起来是 合理的。

然而,或许是由于Java语言在表达能力上的限制,上述假设有一个弱点。即便不知道NewsFeed类的构造情况,我们也能推测出:即使feed这个引用本身不能被改变,但还是有可能通过它来修改它所引用的对象。在C++语言中,可以这样声明属性:

const NewsFeed * const feed;

这个声明不仅表达了指针不能被改变,而且还表达了不能使用指针来改变它所指向的对象。这在C++语言中提供了一个额外的上下文不变性 (contextual immutability)的标记,而这一点在Java语言中并不存在。在Java语言中,想让一个类的所有实例都不可变(immutable)还是比较 容易的。但是想让一个特定的引用所引用的对象不可变,就需要花费相当多的努力了,或许需要创建一个处理不可变性的代理来封装该对象实例。

然而,这些又是如何改变测试的呢?类的构造——实现——清楚地规定了赋给那个类的feed在该类的整个声明周期中不应改变。这是意图吗?让我们看看如代码清单2-2所示的验证这个假设的测试。

代码清单2-2 验证代码清单2-1中的新闻源不会改变的测试

class TestScoreWatcher {    @Test    public void testScoreWatcher_SameFeed() {      // Set up      NewsFeed expectedFeed = new NewsFeed();      int expectedPollFrequency = 70;        // Execute SUT[ SUT是Software Under Test(被测软件)的缩写。[xTP]]      ScoreWatcher sut = new ScoreWatcher(expectedFeed,      expectedPollFrequency);        // Verify results      Assert.assertNotNull(sut); // Guard assertion      Assert.assertSame(expectedFeed, sut.getFeed());        // Garbage collected tear down    }  }

在JUnit中,assertSame断言验证的是,期望的引用和实际的引用都指向同样的对象。回到有关该类的意图的推测上,假设引用到同样的 feed很重要这一点是合理的,但是同样的NewsFeed这一点是不是在这种情况下有些超出规格所规定的范围?例如,要是代码的实现为了选择加强初始新 闻源的不变性,从getter将其返回之前就克隆其属性,从而确保任何变化都不会影响ScoreWatcher的NewsFeed的内部状态,那该怎么 办?在这种情况下,测试构造器的参数是相同的这一点就不正确了。这种设计的意图,更有可能需要验证feed的深度相等性[ 验证两个对象的深度相等性,即验证这两个对象内部所保存的各个数据的值都一一相等。——译者注](deep equality)。

本文来自《优质代码:软件测试的原则、实践与模式》


本书专门从软件开发人员和技术人员关注的代码质量的角度来讲软件测试的原理、实践和模式。作者有20多年软件开发经验,10多年软件测试技术的教授 经验。书中积累了来自大量高水准软件工程师的多年经验。无论你是在写一个新系统,还是试图驾驭一个遗留系统,本书都会让你高效地开发高质量的代码。

测试驱动、测试先行和尽早测试这些开发实践,正在帮助成千上万的软件开发组织改善其软件。在本书中,作者立足于所有读者已经熟知的测试驱动开发知识,帮助读者实现前所未有的优质代码。

为了帮助读者更加全面、有效和轻松地测试任何软件系统,本书使用真实的代码示例介绍了测试的模式、原则和20多个技术细节,并通过两个完整的案例分 析,即测试一个全新的Java应用程序和一个未被测试的“遗留”JavaScript jQuery插件,将本书讲述的所有内容整合在了一起。此外,作者还展示了一个概念框架,帮助读者将精力重点放在改善贯穿整个软件生命周期的可测试性上, 并给读者提供了简化代码构造的全系列测试的实操指南。

无论是最常见的场景还是多线程,本书都会帮读者学会如何针对每一种情景选择最好的测试技术;无论是为一个新的创业公司开发前沿代码,还是维护一个很难驾驭的老旧系统,本书都会帮读者交付其真正需要的优质代码。

简化所有代码的单元测试,并改善集成测试和系统测试。

  • 详述意图和实现,促进更加可靠和可扩展的测试。
  • 克服对编写测试的机制的混淆和误解。
  • 测试“副作用”、行为特征和上下文约束。
  • 了解软件设计与可测试性之间微妙的交互,并对其进行利用,而非受困其中。
  • 揭示能够指导关键测试决策的一些核心原则。
  • 探讨以下内容的测试:getter/setter、字符串处理、封装、覆写变化、可见性、单例模式、错误条件等。
  • 确定性地重现并测试一些复杂的竞态条件。
  • ID:ptpressitbooks

    来自:http://www.jianshu.com/p/c291cb2019e0