目录
函数
函数在不同语言里有不同的叫法,或者是函数,或者是方法。大体就是一段被引用的程序片段。
举例:
public List<int []> getThem(){
List<int []> list1 = new ArrayList<int []>();
for (int x: theList)
if (x[0] == 4)
list1.add(x);
return list1;
}
这个getThem就是一个函数。
在《代码整洁之道》的第三章就是讲如何写好函数,本节内容为本人学习的记录。欢迎讨论指点,谢谢!
1. 短小的函数
短小是函数至关重要的属性,短小函数的好处:
- 阅读一个函数时不需要反复上下滚动鼠标滑轮,如果阅读的函数超过了一个屏幕函数里面的一些变量或者代码逻辑就要占据大脑记忆位,否则你就要上下来回滚动已确保你能继续阅读这个函数。
举例:
public List<int []> getThem(){
List<int []> list1 = new ArrayList<int []>();
for (int x: theList){
if (x[0] == 4){
list1.add(x);
}
else if (x[0] == 5){
list1.add(x + x);
}
else if (x[0] == 6){
list1.add(x + x + x);
}
else if (x[0] == 7){
list1.add(x + x + x + x);
}
}
return list1;
}
这是一个经典的例子,我们可以看到这个函数里面是for加if的嵌套,但是如果这种东西多了的话,就会很影响这个函数的观感。
举例:
public List<int []> getThem(){
List<int []> list1 = new ArrayList<int []>();
for (int x: theList){
chooseNumByListValue(x);
}
return list1;
}
如果改成这样的话,我们就可以直观的看出,这个循环里面装了什么东西,以及我们可以简单的跳到这个函数chooseNumByListValue
里面看具体是什么东西。当然if
这种结构也可以改变成别的写法,但是我们这里讨论的是我们写的函数要尽可能的简短!
像是if
,else
,while
语句,里面装的代码块应该尽可能的用一个函数来代替。这也让我联想到我一直被教育的一句话:在代码里面,流程和实现分离开
。
2. 只做一件事的函数
我们一定有见过一种函数,把很多实现的流程都装在了一起。
比如说:
public List<int []> playGame(){
//获取玩家登录的账号密码
//判断是否能够登录
//对玩家进行操作进行记录判断
//记录玩家游戏结果
}
在这个函数里面,我们讨论一件事,这个函数要做什么,以及它实际上做了什么。
playGame这个函数名告诉我们:
它应该是一个玩游戏的函数。那么它应该只涉及玩游戏这一件事。
实际上呢,它管的太多了:
- 账号密码有它的事。
- 是否能登录有它的事
- 记录玩家游戏结果有它的事
我们一件函数只做一件事,目的是为了后续可维护性更强,以及我们阅读代码的时候思绪不会乱飞舞。
如果这个函数做的事情太多,我们应该把它抽离出来。分成好几个函数来完成这些事情。
3. 每个函数一个抽象层级
对函数里面的代码块要有一个基本的判断,他们是不是处于同一层级的。
举个例子:
//TODO:获取一个html页面
//TODO:进行string类型转int类型的操作
//TODO:进行一个二进制补位操作
这里的例子是为了给一个具体的案例,展示这几个TODO
的内容实在是不能扯到同一个层级上。
当他们出现在同一个函数里面的时候就会出现一个问题,这个函数里面的粒度差距有一丢丢大。(当然这个例子只是为了说明而举例的,实际上应该不会存在把!)
4 switch语句
在代码片段里面总是避免不了switch
语句(或者if
),这些事情不可避免的会出现要做很多事情,这个时候我们要尽可能的将事情去归为同一间事情。
举个例子:
public Money calculatePay(Employee e){
switch(e.type){
case CashierPay:
return cashierPay(e);
case VirtualPay:
return virtualPay(e);
default :
return errorPay(e);
}
}
这是一个模拟支付的选择函数,在这个函数里面会根据传入的Employee
的不同,选择做不同的事情,这样就很明显的。
这个switch
并不只做一件事情,违反了单一职责原则
,同时,这里的switch
如果后续有新的Employee
增加,那么这个函数就要进行修改,违反了开放封闭原则
。
我们可以把它修改一下,让这个switch
去做一件事情,比如,都返回一个对象,然后通过这个对象去调用方法:
public Money calculatePay(Employee e){
switch(e.type){
case CashierPay:
return new CashierPay(e);
case VirtualPay:
return new VirtualPay(e);
default :
return new ErrorPay(e);
}
}
这样修改之后这个函数做的事情就变了,它的事情就是生成一个对象,根据不同的条件去生成。
后续有要生成新的对象,可以采用继承的方式,新的方法也可以让对应的类继承一个接口,接口里面可以写入抽象方法,再让具体的类去实现。
这样的写法会产生编码时候更多的代价,但是伴随的好处就是代码阅读起来很优雅!具体生产环境依据实际需要来。
5. 使用描述性的名称
在函数的命名,宁愿使用长而复杂的名称,也不要使用短而令人费解的名称。
举个例子:
public List<int []> getThem(){
...
}
还是如同上面使用的例子,这里只讨论函数命名,我们可以通过getThem
这个命名知道,这个函数将会返回一些东西,但是却不知道具体的是什么。如果命名成getUserNames
是不是更加具体了,这里并不是说这个函数的作用是什么,单单只讨论函数命名带来的好处。
具体的多长多复杂的名称,你可以命名之后给别人看看,看看周边人的评价更为友好。毕竟,这并没有一个准确的说法。而且阅读你代码的更多可能是和你一起工作的人。
6. 函数参数
理想的函数参数数量是0,越少的参数就会让人阅读时的工作量越少。
输入参数和输出参数都是如此,能越少就越好。
尽量使用返回值传递信息的方式,少用使用参数传递信息而不用返回值的方式,也就是函数里面的引用传递,通过引用直接赋值,然后省去返回值的过程。
6.1 一元函数的普遍形式
一元函数也就是只有一个输入参数,也就是只有一个形参。
举个例子:
public List<int []> getThem(String name){
...
}
使用一元函数的情景大体有两种,
- 一种是询问关于传入参数的问题,例如:
isTrueName(String name)
询问这个名字是否正确,返回一个布尔值。回答提问的问题 - 一种是根据参数进行操作。例如:
setFile(File file)
,根据传入的文件进行操作。
除此之外,从有无返回值看一元函数,还有这两种划分。
- 有返回值的一元函数。
- 没有返回值的一员函数。也被叫做事件。
6.2 不要使用标志参数
标志参数是在说在传参的时候传入一个布尔值。
从标志参数我们也可以看出,这个函数将会根据传入的标志参数的不同做出不同的反应。
6.3 双参数函数
双参数要注意的问题:
- 一是两个名字是否都能令人理解。
- 二是两者的顺序是否能够让人理解。
前者终归于命名问题,后者就是要根据实际问题看了,
例如:
public double getLength(int y,int x){
...
}
这里的x
,y
顺序是有种潜意识遵循的顺序,x
在前,y
在后。
也可以在将双参数函数转换成单参数函数,例如:
public void setResultByName(Result result, String name){
...
}
这个函数想表达的意思是通过name给result赋值。
我们可以在Result这个对象里面写一个setResultByName
方法,然后只需要调用 result.setResultByName(String name)
从而将双参数转换成为单参数。
当然具体情况具体分析~
6.4. 三参数函数
和双参数同理也是要注意排序和命名的问题。复杂度也变高了。
6.5 多个参数对象的转换
当传参超过了三个的时候,就要考虑是不是一些参数可以封装在一起,从而降低传参的个数。
举个例子:
public getCircle(double x, double y, double radius){
...
}
这里是通过传入的参数从而获得一个圆的意思。而我们要考虑的是,这里是不是有参数可以封装在一起,从而减少传参的个数。
public getCircle(Point center, double radius){
...
}
6.6 参数列表
当向函数传入可变参数的时候,如果传入的可变参数都是平等的话,或许可以换一个存放对象存储它们,然后将存储的对象进行传递。
上述只是一种转换方式,具体情况具体分析。
6.7 动词和关键字
在取函数名的时候可以采用动名词的形式,例如
public Circle getCircle(Point center, double radius){
...
}
get
是动词获取的意思,Circle
是循环,圆周的意思,这样我们就能比较清晰的看出这个函数的用途。
关键字是在函数命名中包含形参。这样也可以方便使用时避免传入参数的位置不当。
public void setProductByIdAndName(String id, String name){
...
}
7. 无副作用
这个是针对函数只做一件事的补充,函数有时出现一种情况,在正常调用这个函数的时候没出现问题,
当出现特殊情况会出现特殊的处理。
这种特殊的处理有时候并不会通过函数名表达出来,但是却在函数中出现。这就是函数的副作用。
我们应该尽可能的避免这种情况,如果避免不了应该尽可能的在函数名称中体现出来。
举个例子:
public boolean isRightUser(User user, String password){
if (user.password.equals(password)){
user.isJudged = ture;
return true;
}
return false;
}
在这个函数名字体现是要对用户进行判断返回布尔值的操作。实际上在函数里面偷偷对传入的对象进行了赋值操作(实际上应该是传不出去的,五毛钱辣条!)。
输出参数
我们也不建议在使用输出参数,在函数中通过传入引用类型的参数在函数中对他进行赋值,然后不采用返回值的形式将信息悄悄返回。
8 分隔指令与询问
这里实际上说的是函数命名不友好导致阅读到该函数使用时出现的模糊概念。
举个例子:
if (set("username","unRead")){
...
}
这里讨论set这个函数带来的模糊感,
- 一,在
if
里面我们可以先确定他会返回一个布尔值。 - 二,它应该会根据两个参数然后传出一个布尔值。
- 三,要点进去看一下。
这就是一个模糊感,我们函数的命名一般都是根据作用来的。
- 要么是函数回答了什么事,
isRightSetUserName
。 - 要么是这个函数做了什么事,
setUserName
。
所以例子中的set
函数可以进行修改一下。
if (isRightSetUserName("username")){
setUserName("unRead");
}
9. 用异常代替错误码
这里异常是指在Java中有的一种在代码块里面遇到错误向外抛出错误信息不进行代码块下面的语句的错误提示方式。
错误码是指if
或者switch
匹配到错误信息时进行错误消息打印中if判断==
右边的信息,或者case
右边的信息。
这里用异常代替错误码是在Java中讨论的,因为可以通过try
,catch
的方式减少其他语句的执行。
9.1 抽离try/catch代码块
将try/catch代码块抽离出来,单独形成一个函数,这样只用在正常代码逻辑里面需要进行错误判断处理的时候调用该函数即可。
9.2 错误处理就是一件事
在try/catch
抽离组成的函数里面,就应该只进行错误处理这一件事。所以这个函数不应该包含其他功能!
9.3 错误码的依赖
当使用错误码的时候,程序中多多少少都会引用到,如果对他进行修改,就会对使用到错误码的代码造成压力。
10. 别重复自己
如果有重复的代码片段,我们可以将它们组合成函数,从而降低代码的长度,提高代码的美感。
11. 结构化编程
我们在大函数(长长的函数)里面应该保证一个函数里面应该只有一个return
,在循环里面不用出现break
和continue
语句以及goto
。
12. 如何写出这样的函数
可以先尽可能的遵循规则写代码,我们要确保业务能够按时完成,然后后续可以对自己的代码进行修改,体会那种自己的代码一点一点变简洁美观的快乐!
本人学习的记录,以便下次需要的时候进行查看。也欢迎留下您宝贵的见解和建议。