测试和恢复性的争论:面向对象vs.函数式编程

Michael Feathers最近的博文在博客社区引发了一场异常激烈的论战。Feathers发表言论说一些面向对象编程语言的内嵌特性有助于测试的进行,并且使用面向对象编程语言编写的代码更容易恢复。

他举了这样一个例子,class X有一个叫作badMethod的方法,这个方法处理一些“痛苦”的工作,比如调用并更新产品数据库、或者处理一些甚至关系到底层硬件的事务:

public class X {    public void method() {       ...       badMethod();       ...    }    ...}

理想的设计是,系统可以允许独立测试一般的类和类组。但如果这个例子没有实现这样的设计,“badMethod是个非final,可覆写的方法”的事实就有利于为获得“测试足够的机动性”提供所需的灵活性,因为它允许“覆写功能并为我们创建一个楔子来让测试变得更简单”:

public class TestableX extends X { void badMethod() { /* do nothing */ } }

Feathers称之为一个seam(接缝),“一个你无须修改便能使用一个功能替代另一个功能的地方”。他相信OO语言提供的缓绑定技术使得其本身比函数式语言的恢复更为友好。

一些评论者,包括Feathers本人在内,都强调了大多数语言都能提供seam的事实:预处理器、继承/多态性和委派、宏和函数指针、高阶函数、动态函数、一等函数、模块边界或monads。。。。。。其中一些人认为,真正关系到可测试性的是底层设计而非编程语言的选择。比如John,他断言,无论使用何种语言,“代码的结构需要首先考虑到简化单元测试。”另一位博客Andrew,强调说如果“代码的结构没有向所需的测试看齐的话”,那么实现将不得不向顺应测试的方向,做相应的修改。因此,他也评论说“关于‘seam’的想法确确实实是瞄准了为实现可测试性而进行恰当设计的底层问题”,也就是说,适当地布置seam。

针对这些争论,Feathers强调说,尽管大部分语言都拥有seam,但关键在于哪种语言用起来更为顺手,尤其是在代码未能以方便测试的方式而设计的情况下:

我同意“针对测试来设计”是真正的重点所在,但我也知道无论我们做什么,总会有一些系统没有以这样的方式来设计。也正是因为这个原因,我非常在乎可恢复性。

[…]

我知道设计seams是可能的,但那不是问题所在。真正的问题在于在它们没有被加入进设计的情况下,而适当布置它们到底有多简单。

[…]

当然,seams也不总是与你所想要实现的测试粒度相一致,毋庸置疑的是,在对seam具有良好支持的语言中,要实现与测试粒度相一致会简单得多,因为seam已经存在那里,也因为它创建新的seam更为简单。

根据Feathers所述,尽管在函数式语言中可以采用其他模型来达到同样的目的,但“这是沉重的”,但Haskell例外。在Haskell中,“大部分你想在测试中避免的代码,都可以采用monad来实现”。

尽管Feathers着重指出,他知道人们会辩论说“纯函数式可以满足任何单元测试的需求”,但仍然有许多评论者强力辩论说他没有考虑到函数式语言的细节,以及函数式语言所能提供的机会。Erikd表达了这样一种感觉,他觉得Feathers是在将Java构造器和惯用方法运用到函数式代码中去。

首先,他看上去是在使用Ocaml文法编写Java代码,然后又抱怨说Ocaml不够像Java。他的结论一点都不惊人。Ocaml不是为了编写类似Java这样面向对象的代码而设计的,就是这么简单。

其次,他声称使用函数式语言比Java困难。虽然使用Ocaml文法编写Java代码可能确实很难,但是编写一般的Ocaml代码或函数式代码就不会那么困难了。

很多函数式语言的拥护者强调说,在函数式编程中,是没有副作用的,并且据Greg M.所说,这点可以预防写出需要重构的代码,而且可以将测试变得更为简单:

函数式语言可以让你将代码结构在顶层就将所有讨厌的事务分离开来,并且保持代码的纯逻辑。

[…]

当你的单元拥有完全独立的保障时,单元测试可以变得如此简单!或者说,最差也能保证清楚和明显的依赖性。

Robert Goldman也发表辩论说“常规的面向对象编程语言中过度采用的状态对测试来说很不利”,因为人们需要“创建巨大的互相关联的对象实体,才能为测试提供平台。而且,检验预期的副作用的过程可能会导致额外的复杂性”。相反,“在类似于Haskell这样的纯函数式框架中,所有这些问题都被封装在Monad中”。正如Greg Monads提出的那样,它可以允许编写“一段(凭空)创建IO命令流的代码,以及另一段代码来利用这个IO流,并且决定如何执行这些命令”。

与Greg来自同一个阵营的Ericd坚持认为,在函数式编程中没有内部状态,所以也就没有状态变化的处理。如果要测试“一个没有状态转变的模型或系统”,人们根本不需要Feathers谈到的那种测试:

剩下所需的唯一测试是收集一组输入来测试所有边界条件,将这些输入传递给待测函数,然后验证其输出就可。

[...]

如果组件可以被分离测试(也就是纯函数)并且测试结果表明函数是正确的话,那么这些纯函数的组合理所当然也是正确的。

Feathers对此回答说,他“非常理解纯函数式,并且也知道拥有好的设计的代码自然不该有这些问题”。他强调,并不是所有的代码都有好的设计,而且,“Haskell的确是迫使你将副作用隔离开来的函数式编程语言的一种,然而其他一些语言,比如OCaml或Scala,“它们看起来无法避免人们将代码搞得乱七八糟”。

无论如何,很多不同意Feathers看法的争论者认为,将函数式代码搞得乱七八糟的唯一方式就是在使用函数式语言时采用非函数式用法。Goldman断言,拥有副作用的程序“被公认为是像ML、Ocaml和Common Lisp这样的混合性语言的非函数式部分”,显然是要避免使用的。Greg同样支持这个观点,他表示,除非人们非要和函数式语言作对,以非函数式用法的方式来编写代码,那自然也就没办法“得到你本可以从权威的OO代码中‘得到’的IoC和分离关注点。”这也是为什么Erikd坚持认为,有OO技术背景的人想要使用函数式语言编写高质量代码的话,就必须抛弃“旧习和思考方式”,尽可能长时间地忘却“面向对象和专断的编程特性”。

上一篇:C++学习(八)(C语言部分)之 图形库


下一篇:Java IO系列之一:IO