代码整洁之道-读书笔记之函数

1.短小

搞定了函数的命名之后,看一下函数内容的建议和规范,第一原则:简短

问:函数应该有多么短小呢?

答:if语句、else语句、while语句等,其中的代码块应该只有一行

函数的缩进层级不应该多余一层或者两层,这样的函数易于阅读和理解

2.只做一件事

一函数理论上只做一件事情,只做一个抽象层次的事情,通俗的说就是看看当前函数是否还可以拆分出一个函数,如果可以说明就不是做一件事

3.每个函数一个抽象层级

保证一个函数一个抽象层级,也是确保函数只做一件事的依据。

阅读代码的习惯:自顶向下阅读

4.switch语句

switch语句的本意就是完成多件事情,下面看一段switch的代码

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); 
}

大家看到这个有人认为代码比较清爽,也比较简洁。

其实这里存在几个

1.当出现新的员工类型的时候,这里需要添加新的case和新的工资计算的方法

2.很明显这个方法做了多件事情

3.违反了单一原则

4.违反了开闭原则

在这里我给出上面的问题一个通用的解法:工厂+多态+封装

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); 
}

5使用描述性的名称

1.不要害怕长名称,长而具有描述性的名称,要比短而令人费解的名称好,要比描述性的长注释好

2.不要害怕花时间取名字

3.命令方式要保持一致,使用与模块名一脉相承的短语、名称、动词

6.函数参数

函数参数的数量:0>1>2>3 应该避免3个以及以上

随着参数数量的增加,单测的组合就越多,函数的就更无法保持只做一件事的标准

6.1一元函数

1.单纯操作参数,进行操作 User getUser(long userId);

2.操作参数,进行转换,并且返回值void void appendString(StringBuilder sb)(慎用)

3.操作参数,进行转换,将转换后的数据进行返回StringBuilder appendString(StringBuilder sb)

6.2标识参数

参数是boolean类型的方法

例如:operate(boolean flag)

这种方法会让用户产出模糊的想法,到底是做还是不做,这种的一般拆分成

canOperate(),noOperate();

6.3 二元函数

含有两个参数的函数,比一个参数的难懂一些,但是有的时候也是可以使用两个参数比如copyArray(String[] source, String[] target)

6.4 三元函数

含有三个参数的函数,可读性就更差了,创建三个参数的函数的时候,一定要思考情况在进行创建

6.5 参数对象

如果一个函数的参数数量过多,建议封装成一个对象进行传递

例如

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

6.6 参数列表

有时,我们想要向函数传入数量可变的参数。例如,String.format方法:String.format("%s worked %.2f hours.", name, hours);

如果可变参数像上例中那样被同等对待,就和类型为List的单个参数没什么两样。这样一来,String.formate 实则是二元函数。下列String.format的声明也很明显是二元的:

public String format(String format, Object... args)

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

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

7 无副作用

副作用指得就是函数违背了只做一件事的承诺

下面来看一段代码

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的功能,

这样就让这段代码出现了副作用,可能就会在某些情况调用的时候产生bug,也违背了我们一个函数只做一件事的初衷,如果必须要这么做,我们可以考虑重命名为checkPasswordAndInitSession

输出参数

参数很自然就会当做函数的输入,但是也有情况是作为输出。例如

void appendFooter(String s) // 追加页脚

这时候读者就会有疑问,s是添加到什么后面,还是把什么东西添加到s后面,s是函数的输入还是最终的输出,副作用就显露出来了,修改后如下

report.appendFooter() // 报告追加页脚

8. 分隔指令和询问

函数要么做什么事情、要么回答什么事情,二者不可兼得

接下来看一个例子,一个函数修改某一个属性,修改成功就返回true,失败就返回false,如果不存在属性就返回false

public boolean set(String attribute, String value){
}

public void test(){
	if(set("username", "java")){
		xxxx
	}
}

看到上面的代码就会存在疑惑,这里是查看username之前就设置为java呢,还是将username设置成java呢?正确的代码如下:

if(attributeExists("username")){
	setAttribute("username", "java")
}

9使用异常替代返回错误码

上一小节鼓励我们在if的时候进行判断,在执行业务逻辑,但是这样却会导致我们代码的嵌套结构变深,导致代码可读性下降

下面看一个例子:

if (deletePage (page) == E_OK) {
	if (registry.deleteReference (page.name) == E_OK) {
		if (configKeys.deleteKey (page.name.makeKey ()) == E_OK) {
			 logger.log ("page deleted");
		}else {
			 logger.log ("configKey not deleted");
	}else {
		logger.log ("deleteReference from registry failed");
}else{
	logger.log ("delete failed"); 
	return E_ERROR;
}

碰到上情况我们可以作如下操作

try{
	deletePage(page);
	registry.deleteReference(page.name);
	configKeys.deleteKey(page.name.makeKey())
}catch(Execption e){
	logger.log(e.getMessage())
}

9.1 抽离try/catch代码块

try/catch代码非常丑陋,而且我们把错误和正常流程一块处理

第一种提取方式

public void delete(Page page){
	try{
		deletePageAndA11References (page) ; 
	}catch(Exception e){ 
	logError(e);
	}
}

private void deletePageAndAl1References (Page page) throws Exception{ 
	deletePage(page);
	registry.deleteReference (page. name) ; 
	configKeys.deleteKey(page. name.makeKey());
}

private void logError(Exception e){ 
	logger. log (e.getMessage());
} 

第二种提取方式

if (deletePage (page) != E_OK) {
	logger.log ("delete failed"); 
	return E_ERROR;
}
if (registry.deleteReference(page.name) != E_OK) {
	logger.log ("deleteReference from registry failed"); 
	return E_ERROR;
}
if (configKeys.deleteKey(page.name.makeKey()) != E_OK) {
	logger.log ("page deleted");
	return E_ERROR;
}

9.2错误处理就是一件事

函数应该只做一件事。错误处理就是一件事。因此,处理错误的函数不该做其他事。这意味着(如上例所示)如果关键字try在某个函数中存在,它就该是这个函数的第一个单词,而且在catch/finally代码块后面也不该有其他内容。

10 别重复自己

工程里面不要有重复的代码,如果发现一定要通过重构的手段将其消灭

11 结构化编程

结构化编程:一个函数只有一个入口和一个出口,只存在一个return,循环中不能有break和continue

如果我们可以保持函数的短小,不用遵循上面的原则

12.如何写出这样的函数

我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。不过我会配上一套单元测试,覆盖每行丑陋的代码。

然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时我还拆散类。同时保持测试通过。

最后,遵循本章列出的规则,我组装好这些函数。

我并不从一开始就按照规则写函数。我想没人做得到。

13 小结

本章主要围绕如何写一个好的函数进行讲解

1.函数要短小

2.只做一件事

3.参数尽量要少

4.尽量避免副作用

5.异常和正常逻辑要隔离

6.不要重复

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
代码整洁之道-读书笔记之函数
一函数理论上只做一件事情,只做一个抽象层次的事情,通俗的说就是看看当前函数是否还可以拆分出一个函数,如果可以说明就不是做一件事
<<上一篇
下一篇>>