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

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.不要重复

原文地址:https://cloud.tencent.com/developer/article/2130547

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340