代码整洁之道之函数

代码整洁之道之函数编码规范。

短小

函数的第一规则是要短小。第二条规则是还要更短小。

代码块和锁进

if语句,else语句,while语句等,其中的代码应该只有一行。该行大抵是一个函数调用。这样不仅能保持函数短小,而且因为块内调用的函数拥有较具说明性的名称,从而增加了文档上的价值。

只做一件事

函数的规则应该是只做一件事,做好这件事。只做这一件事

每个函数一个抽象层级

确保函数只做一件,函数中的语句都要在同一抽象层级上。

自顶向下读代码:向下规则

想要让代码拥有自顶向下的阅读顺序。我们想要让每个函数后面都跟着位于下一抽象层级的函数,这样一来,在查看函数列表时,就能循抽象层级向下阅读了。我把这叫做向下规则。

switch 语句

写出短小的switch语句很难,写出只做一件事的switch语句也很暗。switch语句天生要做N件事。但通常又无法避免switch语句。
利用多态来实现这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
public Money calculatePay (Employee e)
throws InvalidEmployeeType {
switch (e.type) {
case COMMISSIONED:
return calculateCommissionedPay (e);
case HOURLY:
return calculateHourlyPay (e);
case SALARIED:
return calculateSalariedPay (e);
default:
throw new InvalidEmployeeType (e.type);
}
}

该方式存在的问题:

  • 太长,当出现新的雇员类型时,还会变得更长。
  • 明显做了不止一件事
  • 它违反了单一权责原则(Single Responsibility Principle”, SRP)
  • 它违反了开放闭合原则(Open Closed Principle’, OCP),因为每当添加新类型时,就必须修改之

该问题的解决方案是将switch 语句埋到抽象工厂’底下,不让任
何人看到。该工厂使用 switch 语句为 Employee 的派生物创建适当的实体,而不同的函数,如calculatePay、isPayday 和deliverPay 等,则藉由 Employee 接口多态地接受派遣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class Employee {
public abstract boolean isPayday ();
public abstract Money calculatePay ();
public abstract void deliverPay (Money pay);
}
public interface EmployeeFactory {
public Employee makeEmployee (EmployeeRecord r) throws InvalidEmployeeType;
}

public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee (EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee (r)
case HOURLY:
return new HourlyEmployee (r);
case SALARIED:
return new SalariedEmploye (r);
default:
throw new InvalidEmployeeType (r.type);
}
}
}

使用描述性名称

使用描述性的名称可以很好的描述函数做的数,这就是为什么要函数短小,函数越短小,功能越集中,就越便于取个好名字。

函数参数

最理想的函数参数是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应该尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)。少的参数更容易让人理解,从测试角度来看也更方便。

一元函数

  • 操作该参数
  • 事件,有输入参数而无输出参数。(使用改参数修改系统状态)

标识参数

禁止传参标识参数,传递标识参数时就意味着该函数不止做一件事。

二元函数

二元参数要比一元参数难以理解,如:

1
writeField(name)

1
writeField(outputStream,name)

更清晰。另外也需要注意二元函数参数的位置。使用二元函数时应尽量将二元函数转换为一元函数,如: outputStream.writeField(name)。

三元函数

有三个参数的函数要比二元函数难懂得多。排序、琢磨、忽略的问题都会加倍体现。建
议你在写三元函数前一定要想清楚。

参数对象

如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。

例如,下面两个声明的差别:

1
2
Circle makeCircle (double x, double y, double radius);
Circle makeCircle (Point center, double radius);

从参数创建对象,从而减少参数数量,看起来像是在作弊,但实则并非如此。当一组参
数被共同传递,就像上例中的x和y 那样,往往就是该有自己名称的某个概念的一部分。

参数列表

同理,有可变参数的函数可能是一元、二元甚至三元。超过这个数量就可能要犯错了。

1
2
3
void monad (Integer... args);
void dyad (String name, Integer... args);.
void triad (String name, int count, Integer... args);

动词与关键字

给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。对于一元函数,
函数和参数应当形成一种非常良好的动词/名词对形式。例如: write(name)就相当令人认同。

不管这个“name”是什么,都要被“write”。更好的名称大概是 writeField(name),它告诉我们,“name”是一个“field”。最后那个例子展示了函数名称的关键字(keyword)形式.

无副作用

副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事。有时,它会
对自己类中的变量做出未能预期的改动。有时,它会把变量搞成向函数传递的参数或是系统全局变量。无论哪种情况,都是具有破坏性的.

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password)
{
User user = UserGateway.findByName (userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedBy
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password".equals (phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}

副作用就在于对 Session.initialize()的调用。checkPassword 函数,顾名思义,就
是用来检查密码的。该名称并未暗示它会初始化该次会话。所以,当某个误信了函数名的调用者想要检查用户有效性时,就得冒抹除现有会话数据的风险。

分隔指令与询问

函数要么做什么事,要么回答什么事,但二者不可得兼。函数应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。看看下面的例子:

1
public boolean set (String attribute, String value);

该函数设置某个指定属性,如果成功就返回 true, 如果不存在那个属性则返回 false。这样就导致了以下语句:

1
if (set ("username", "unclebob"))...

从读者的角度考虑一下吧。这是什么意思呢?它是在问 username 属性值是否之前已设置为 unclebob 吗? 或者它是在问 username 属性值是否成功设置为 unclebob 呢? 从这行调用很难判断其含义,因为 set 是动词还是形容词并不清楚。

作者本意,set 是个动词,但在 if 语句的上下文中,感觉它像是个形容词。该语句读起来像是说“如果 username 属性值之前已被设置为uncleob”,而不是“设置 username 属性值为unclebob,看看是否可行,然后……”。

要解决这个问题,可以将 set 函数重命名为
setAndCheckIfExists,但这对提高 if语句的可读性帮助不大。真正的解决方案是把指令与询
问分隔开来,防止混淆的发生:

1
2
3
if (attributeExists ("username")) {
setAttribute ("username", "unclebob");
}

别重复自己

重复可能是软件中一些邪恶的根源。

小结

每个系统都是使用某种领域特定语言搭建,而这种语言是程序员设计来描述那个系统的。函数是语言的动词,类是名词。这并非是退回到那种认为需求文档中的名词和动词就是系统中类和函数的最初设想的可怕的旧观念。其实这是个历史更久的真理。编程艺术是且一直就是语言设计的艺术。

节选《代码整洁之道》

Search by:GoogleBingBaidu