代码整洁之道 读书笔记 - 第3章 函数
短小
函数的第一规则是要短小。第二条规则是还要更短小。
函数20行封顶最佳。
if语句、else语句、while语句等,其中的代码块应该只有一行,而且,块内调用的函数拥有较具说明性的名称,还能起到文档的作用。
只做一件事
函数应该做一件事。做好这件事。只做这一件事。
每个函数一个抽象层级
自顶向下读代码:向下规则.
switch语句
只出现一次,用于创建多态对象,且隐藏在某个继承关系中,系统其他部分看不到。
使用描述性的名称
沃德原则:如果每个例程都让你感到深合己意,那就是整洁代码。
长而具有描述性的名称,要比短而令人费解的名称好,要比描述性的长注释好。
函数参数
理想的参数数量是越少越好,尽量避免三个以上的参数。
参数带有太多概念性,includeSetupPage()要比includeSetupPageInto(newPage-Content)易于理解。
从测试的角度看,要编写能确保参数的各种组合运行正常的测试用例。
1、一元函数的普遍形式
1)有输入参数也有输出参数
boolean fileExists("MyFile") InputStream fileOpen("MyFile")
2)有输入参数没有输出参数
void passwordAttemptFailedNtimes(int attempts)
3)尽量避免编写不遵循这些形式的一元函数
void includeSetupPageInto(StringBuffere pageText)
对于转换,使用输出参数而非返回值令人迷惑。
如果函数要对输入参数进行转换操作,转换结果就该体现为返回值。实际上,StringBuffer transform(StringBuffer in) 比 void transform(StringBuffer out) 好。
2、标识参数
向函数传入布尔值就是说明本函数不止做一件事。如果标识为true将会这样做,标识为false则会那样做。
将render(Boolean isSuite)函数一分为二:renderForSuite() 和 renderForSingleTest()
3、二元函数
1)两个参数的函数要比一元函数难懂
writeField(name) //一个参数的可读性更好 writeField(outoutStream, name)
2)有些时候两个参数正好
Point p = new Point(0, 0)
笛卡儿点天生拥有两个参数。如果看到new Point(0)会反而感到奇怪。
3)应该尽量利用一些机制将其转换成一元函数
outputStream.writeField(name) //把writeField方法写成outputStream的成员之一
4、三元函数
有三个参数的函数要比二元函数难懂得多,建议在写三元函数前一定要想清楚。
5、参数对象
如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了
Circle makeCircle(double x, double y, double radius); Circle makeCircle(Point center, double radius);
6、参数列表
7、动词与关键字
给函数取个好名字,能较好地解释函数的意图,以及参数的顺序和意图。
对于一元函数,函数和参数应当形成一种非常良好的动词/名词对形式
write(name) //不管这个“name”是什么,都要被“write” writeField(name) //更好的名称,它告诉我们,“name”是一个“field”
无副作用
副作用是一种谎言。函数承诺只做一件事,但还是会做其他被藏起来的事
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.getPhraseEncodedByPassword(); String phrase = cryptographer.decrypt(codedPhrase, password); if ("Valid Password".equals(phrase)) { Session.initialize(); return true; } } return false; } }
代码的副作用在于对Session.initialize()的调用。checkPassword函数,就是用来检查密码的。并未暗示它会初始化该次会话。当某个误信了函数名的调用者想要检查用户有效性时,就得冒抹除现有会话数据的风险。
这一副作用造出了一次时序性耦合。checkPassword如果在不合适的时候调用,会话数据就有可能沉默地丢失。
如果一定要时序性耦合,就应该在函数名称中说明。重命名函数为checkPasswordAndInitializeSession,但这还是违反了“只做一件事”的规则。
分隔指令与询问
函数要么做什么事,要么回答什么事,但二者不可兼得
public boolean set(String attribute, String value);
该函数设置某个指定属性,如果成功就返回true,如果不存在那个属性则返回false。这样就导致了以下语句
if (set("username", "unclebob")) ...
它是在问username属性值是否之前已设置为unclebob,或是在问username属性值是否成功设置为unclebob。
要解决这个问题可以将set函数重命名为setAndCheckIfExists,但真正的解决方案是把指令与询问分隔开来,防止混淆的发生
if (attributeExists("username")) { setAttribute("username", "unclebob"); ... }
使用异常替代返回错误码
1、抽离Try/Catch代码块
把Try/catch代码块的主体部分抽离出来,另外形成函数。
2、错误处理就是一件事
函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。
关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。
3、依赖磁铁
返回错误码通常暗示某处有个类或是枚举定义了所有错误码
public enum Error { OK, INVALID, NO_SUCH, LOCKED, OUT_OF_RESOURCES, WAITING_FOR_EVENT; }
这样的类就是一块依赖磁铁(dependency magnet);当Error枚举修改时,所有使用它的类都需要重新编译和部署。所以程序员宁愿复用旧的错误码,而不添加新的。
别重复自己
重复可能是软件中一切邪恶的根源。许多原则与实践规则都是为控制与消除重复而创建。
考德(Codd)的数据库范式都是为消灭数据重复而服务。
面向对象编程将代码集中到基类,从而避免了冗余。
面向方面编程(Aspect Oriented Programming)、面向组件编程(Component Oriented Programming)多少也都是消除重复的一种策略。
结构化编程
有些程序员遵循Edsger Dijkstra的结构化编程规则。Dijkstra认为,每个函数、函数中的每个代码块都应该有一个入口、一个出口。
遵循这些规则,意味着每个函数中只该有一个return语句,循环中不能有break或continue语句,而且永永远远不能有任何goto语句。
对于小函数,这些规则助益不大,而且偶尔出现return、break或continue语句没有坏处,甚至还比单入单出原则更具有表达力。