第3章 - 函数

函数是任何主题中组织的第一线。

尽可能简短!!!

函数的第一条规则是它们应该简短。第二条规则是它们应该比那更简短。

代码块和缩进

这意味着在 if 语句、else 语句、while 语句等中的代码块应该只有一行长。可能这行应该是一个函数调用。这不仅保持了封闭函数的小体积,而且增加了文档价值,因为代码块中调用的函数可以有一个非常好的描述性名称。

这也意味着函数不应该大到可以容纳嵌套结构。因此,函数的缩进级别不应该超过一或二。这当然使函数易于阅读和理解。

只做一件事

函数应该做一件事。它们应该做好。它们应该只做这件事。

函数内的段落

如果你有一个函数分成了多个段落,比如声明、初始化等不分,这是函数做了多于一件事的明显症状。做一件事的函数不能合理地被分成多个段落。

每个函数一个抽象层次

为了确保我们的函数正在做“一件事”,我们需要确保我们函数内的语句都在同一个抽象层次上。

从上到下阅读代码:递减规则

我们希望代码像自顶向下的叙述一样阅读。我们希望每个函数后面都是下一个抽象层次的函数,这样我们可以阅读程序,随着我们阅读函数列表,一次下降一个抽象层次。

换句话说,我们希望能够像一组TO段落一样阅读程序,每个段落都描述当前的抽象层次,并引用下一个抽象层次的后续TO段落。

  1. - 为了包括设置和拆除,我们包括设置,然后我们包括测试页面内容,然后我们包括拆除。
  2. - 为了包括设置,如果这是一个套件,我们包括套件设置,然后我们包括常规设置。
  3. - 为了包括套件设置,我们在父级层次结构中搜索“SuiteSetUp”页面,并添加一个包含该页面路径的包含语句。
  4. - 为了搜索父级...

事实证明,程序员很难学会遵循这个规则,编写保持单一抽象层次的函数。但学会这个技巧也非常重要。这是保持函数简短并确保它们做“一件事”的关键。使代码像一组自顶向下的TO段落一样阅读是保持抽象层次一致的有效技术。

Switch语句

很难做出一个小的 switch 语句。即使是只有两个案例的 switch 语句也比我理想中的单个块或函数要大。同样,也很难让 switch 语句做一件事。由于它们的性质,switch 语句总是做N件事。不幸的是,我们不能总是避免 switch 语句,但我们可以确保每个 switch 语句都被埋在一个低级类中,并且永远不会重复。我们当然通过多态性来实现这一点。

使用描述性名称

当你工作在干净的代码上时,每个例程的结果几乎就是你期望的那样

实现这一原则的一半是为做一件事的小函数选择好名称。函数越小、越专注,选择描述性名称就越容易。

不要害怕让名称长。一个长的描述性名称比一个短的神秘名称要好。一个长的描述性名称比一个长的描述性注释要好。使用一个允许多个单词在函数名称中轻松阅读的命名约定,然后利用这些多个单词给函数一个说明它做什么的名称。

选择描述性名称将澄清你脑海中模块的设计,并帮助你改进它。寻找一个好的名称通常会导致代码的有利重构。

函数参数

函数的理想参数数量是零(无参数)。其次是一个(一元的),其次是两个(二元的)。三个参数(三元的)应该尽可能避免。超过三个(多元的)需要非常特别的证明 - 然后无论如何也不应该使用。

从测试的角度来看,参数甚至更难。想象一下编写所有测试用例以确保所有参数的各种组合都能正常工作的困难。如果没有参数,这是微不足道的。如果有一个参数,那就不太困难。有两个参数时问题变得更具挑战性。有超过两个参数时,测试每种适当值的所有组合可能会令人望而生畏。

输出参数比输入参数更难理解。当我们阅读一个函数时,我们习惯于信息通过参数进入函数,并通过返回值输出。我们通常不希望信息通过参数输出。因此,输出参数经常让我们不得不再看一眼。

常见的一元形式

有两个非常常见的原因将单个参数传递给函数。你可能在询问有关该参数的问题,比如 boolean fileExists(“MyFile”)。或者你可能在操作该参数,将其转换为其他东西并返回它。例如,InputStream fileOpen(“MyFile”) 将文件名String转换为 InputStream 返回值。这两种用途是读者在看到函数时所期望的。你应该选择名称来明确区分,并始终在一致的上下文中使用这两种形式。

标志参数

标志参数是丑陋的。将布尔值传递给函数是一个真正可怕的实践。它立即复杂化了方法的签名,大声宣布这个函数做了不止一件事。如果标志是true,它做一件事,如果标志是false,它做另一件事!

二元函数

带有两个参数的函数比一元函数更难理解。例如,writeField(name)writeField(output-Stream, name) 更容易理解。

当然,有时两个参数是合适的。例如,Point p = new Point(0,0); 是完全合理的。笛卡尔点自然需要两个参数。

即使是像 assertEquals(expected, actual)这样的明显二元函数也是有问题的。你有多少次把实际的放在预期的应该在的地方?两个参数没有自然顺序。预期,实际的顺序是一种需要练习才能学会的约定。

二元不是邪恶的,你肯定必须编写它们。然而,你应该意识到它们有代价,并且应该利用可能的机制将它们转换为一元。例如,你可以使 writeField 方法成为 outputStream 的成员,这样你就可以说 outputStream.writeField(name)。或者你可能使 outputStream 成为当前类的成员变量,这样你就不必传递它。或者你可以提取一个新类,比如 FieldWriter,在它的构造函数中接受 outputStream,并有一个写方法。

三元组

接受三个参数的函数比二元函数更难理解。排序、暂停和忽略的问题比二元函数多得多。在你创建三元组之前,我建议你非常仔细地思考。

参数对象

比较:

  1. Circle makeCircle(double x, double y, double radius);

  1. Circle makeCircle(Point center, double radius);

动词和关键字

为函数选择好的名称可以在很大程度上解释函数的意图以及参数的顺序和意图。在一元的情况下,函数和参数应该形成一个非常好的动词/名词对。例如,write(name)非常生动。不管这个“name”是什么,它都被“写”了。一个更好的名称可能是writeField(name),它告诉我们“name”这个东西是一个“field”。

最后一个是函数名称的关键字形式的例子。使用这种形式,我们将参数的名称编码到函数名称中。例如,assertEquals可能更好地写成assertExpectedEqualsActual(expected, actual)。这大大缓解了必须记住参数顺序的问题。

输出参数

通常应该避免输出参数。如果你的函数必须改变某些状态,请让它改变其拥有对象的状态。

命令查询分离

函数应该要么做某事,要么回答某事,但不要同时做两件事。你的函数应该改变对象的状态,或者它应该返回有关该对象的一些信息。同时做两件事通常会导致混淆。

优先使用异常而不是返回错误代码

从命令函数返回错误代码是命令查询分离的微妙违反。

不要重复自己(DRY)

重复可能是软件中所有邪恶的根源。许多原则和实践被创造出来是为了控制或消除它。

结构化编程

一些程序员遵循 Edsger Dijkstra 的结构化编程规则。Dijkstra 说,每个函数以及函数内的每个块,都应该有一个入口和一个出口。遵循这些规则意味着函数中应该只有一个返回语句,循环中没有 breakcontinue 语句,永远不要有任何 goto 语句。

虽然我们对结构化编程的目标和纪律表示同情,但当函数非常小的时候,这些规则几乎没有什么好处。只有在较大的函数中,这些规则才提供显著的好处。

所以如果你保持你的函数小,那么偶尔的多个 returnbreakcontinue 语句不会造成伤害,有时甚至比单入口、单出口规则更具表现力。另一方面,goto 只在大函数中才有意义,所以应该避免。