在第11.1.2 节,我们讨论跟踪代码中的依赖关系时,使用的C# 方法,类似于上两个示例中的F# 函数,演示函数式编程使得更容易识别函数做什么,访问什么数据。这不仅在写代码时非常有用,而且在测试时也极其有用。
在第11.1 节,我们写过一个命令式方法,打印出由多字组成的名字,但是,它有副作用,会从作为参数传递进来的可变列表中删除元素。只要我们以后不再使用这个列表,就不会引起任何问题。对这个方法的任何单元测试以检查打印输出,都会成功。
使这个方法棘手的是,如果我们把它与其他同样正确的方法连一起使用时,可能会得到意想不到的结果,因此,想彻底测试命令代码很难。原则上,我们应该测试的是,每个方法只做它应该做的,且仅此。不幸的是,“且仅此”部分真的难以测试,因为任何一段代码都可以访问并修改共享的可变状态的任何一部分。
而在函数式编程中,我们不会修改任何共享状态,因此,我们只需要验证,对于所有给定的输入,函数返回正确的结果。这也说明,当我们把两个测试过的函数放在一起使用时,只要测试这个组合有相应的结果:并不需要验证其中的函数不会以微妙的方式破坏了对方的数据。清单11.11 显示的这种测试,看起来完全没有意义,但是想象一下,如果我们使用List<T>,而不是不可变的F# 列表,会是什么样子。
清单11.11 测试两个有副作用的函数(F#)
[<Fact>]
let partitionThenLongest() =
lettest = ["Seattle"; "New York"; "Grantchester"]
letexpected = ["New York"], ["Seattle"; "Grantchester"]
letactualPartition = partitionMultiWord(test) | [1]
letactualLongest = getLongest(test) |
Assert.Equal(expected,actualPartition) | [2]
Assert.Equal("Grantchester",actualLongest) |
可以发现,单元测试顺序地运行两个函数[1],但只使用部分结果,验证结果是合我们的期望值[2]。这样,函数调用是独立的,如果它们不包含任何副作用,我们可以自由地改变调用的顺序。在函数世界中,这个单元测试根本不需要:我们已经为每个单独的函数写过单元测试,而这个测试并没有验证任何额外的行为。
然而,如果我们想要使用可变的List<T> 类型,写类似的代码,这个测试可能会捕获到我们在第11.1 节发现过的错误。如果partitionMultiWord 函数修改了值 test 所引用的列表,比如,删除所有单字的名字,那么,第二次调用的结果就不可能是测试预期的“Grantchester”。这是关于函数式代码重要的一般观点:如果我们很好地测试了所有的基本部分,再测试组合的代码,那么,我们就不需要在新的配置中,测试这些基本部分是否仍然有正确的行为。
到目前为止,我们已经讨论了函数式程序的重构和测试。我们发现,一等函数能够减少代码重复,不可变的数据结构有助于我们了解代码在做什么,而且,减少了测试两段代码可能会互相干扰的需要。
本章的剩余部分将会讨论如果代码被执行时,如何能够利用这一优势,使代码更高效。首先,我们需要了解何时有一定的灵活性,F# 和C# 如何决定何时执行代码。