设计模式:让你最快速地改善代码质量的20条编程规范

关于命名

(1) 命名的关键是能准确达意。

  • 实际上,在足够表达其含义的情况下,命名当然是越短越好。
  • 大家都比较熟知的词,推荐用缩写。这样一方面能让命名短一些,另一方面又不影响阅读理解,比如,sec 表示 second、str 表示 string、num 表示 number、doc 表示 document。
  • 除此之外,对于作用域比较小的变量,我们可以使用相对短的命
    名,比如一些函数内的临时变量。相反,对于类名这种作用域比较大的,推荐用比较长长的命名方式。

(2)我们可以借助类的信息来简化属性、函数的命名,利用函数的信息来简化函数参数的命名。

比如:

public class User {
	private String userName;
	private String userPassword;
	private String userAvatarUrl;
	//...
}
  • 在 User 类这样一个上下文中,我们没有在成员变量的命名中重复添加“user”这样一个前缀单词,而是直接命名为 name、password、avatarUrl。
  • 在使用这些属性时候,我们能借助对象这样一个上下文,表意也足够明确。具体代码如下所示:
User user = new User();
user.getName(); // 借助 user 对象这个上下文

(3)命名要可读、可搜索。不要使用生僻的、不好读的英文单词来命名。除此之外,命名要符合项目的统一规范,不要用些反直觉的命名。

在命名的时候,最好能符合整个项目的命名习惯。

  • 大家都用“selectXXX”表示查询,你就不要用“queryXXX”;
  • 大家都用“insertXXX”表示插入一条数据,你就要不用“addXXX”

统一规约是很重要的,能减少很多不必要的麻烦。

(4)

  • 接口有两种命名方式:一种是在接口中带前缀“I”;另一种是在接口的实现类中带后缀“Impl”。
  • 对于抽象类的命名,也有两种方式,一种是带上前缀“Abstract”,一种是不带前缀。

这两种命名方式都可以,关键是要在项目中统一。

关于注释

(1)注释的目的就是让代码更容易看懂。只要符合这个要求的内容,你就可以将它写到注释里。总结一下,注释的内容主要包含这样三个方面:做什么、为什么、怎么做。对于一些复杂的类和接口,我们可能还需要写明“如何用”。

(2)注释本身有一定的维护成本,所以并非越多越好。类和函数一定要写注释,而且要写得尽可能全面、详细,而函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码可读性。

函数、类多大才合适?

  • 函数的代码行数不要超过一屏幕的大小,比如 50 行。
  • 类的大小限制比较难确定:当一个类的代码读起来让你感觉头大了,实现某个功能时不知道该用哪个函数了,想用哪个函数翻半天都找不到了,只用到一个小功能要引入整个类(类中包含很多无关此功能实现的函数)的时候,这就说明类的行数过多了。

一行代码多长最合适

  • 最好不要超过 IDE 显示的宽度。
  • 当然,限制也不能太小,太小会导致很多稍微长点的语句被折成两行,也会影响到代码的整洁,不利于阅读。

善用空行分割单元块

  • 对于比较长的函数,为了让逻辑更加清晰,可以使用空行来分割各个代码块。
  • 在类内部,成员变量与函数之间、静态成员变量与普通成员变量之间、函数之间,甚至成员变量之间,都可以通过添加空行的方式,让不同模块的代码之间的界限更加明确。

四格缩进还是两格缩进?

  • 取决于个人喜好。只要项目内部能够统一就行了。
  • 当然,还有一个选择的标准,那就是跟业内推荐的风格统一、跟著名开源项目统一。当我们需要拷贝一些开源的代码到项目里的时候,能够让引入的代码跟我们项目本身的代码,保持风格统一。
  • 除此之外,值得强调的是,不管是用两格缩进还是四格缩进,一定不要用 tab 键缩进。因为在不同的 IDE 下,tab 键的显示宽度不同,有的显示为四格缩进,有的显示为两格缩进。如果在同一个项目中,不同的同事使用不同的缩进方式(空格缩进或 tab 键缩进),有可能会导致有的代码显示为两格缩进、有的代码显示为四格缩进。

大括号是否要另起一行?

  • 个人还是比较推荐将大括号放到跟上一条语句同一行的风格,这样可以节省代码行数。
  • 但是,将大括号另起一行,也有它的优势,那就是,左右括号可以垂直对齐,哪些代码属于哪一个代码块,更加一目了然。

不过,还是那句话,大括号跟上一条语句在同一行,还是另起新的一行,只要团队统一、业内统一、跟开源项目看齐就好了,没有绝对的优劣之分。

类中成员的排列顺序

  • 在 Java 类文件中,先要书写类所属的包名,然后再罗列 import 引入的依赖类。在Google 编码规范中,依赖类按照字母序从小到大排列。
  • 在类中,成员变量排在函数的前面。成员变量之间或函数之间,都是按照“先静态(静态函数或静态成员变量)、后普通(非静态函数或非静态成员变量)”的方式来排列的。除此之外,成员变量之间或函数之间,还会按照作用域范围从大到小的顺序来排列,先写 public成员变量或函数,然后是 protected 的,最后是 private 的。
  • 不过,不同的编程语言中,类内部成员的排列顺序可能会有比较大的差别。比如 C++ 中,成员变量会习惯性放到函数后面。除此之外,函数之间的排列顺序,会按照刚刚我们提到的作用域的大小来排列。实际上,还有另外一种排列习惯,那就是把有调用关系的函数放到一块。比如,一个 public 函数调用了另外一个 private 函数,那就把这两者放到一块。

把代码分割成更小的单元块

大部分人阅读代码的习惯都是,先看整体再看细节。所以,我们要有模块化和抽象思维,善于将大块的复杂逻辑提炼成类或者函数,屏蔽掉细节,让阅读代码的人不至于迷失在细节中,这样能极大地提高代码的可读性。不过,只有代码逻辑比较复杂的时候,我们其实才建议提炼类或者函数。毕竟如果提炼出的函数只包含两三行代码,在阅读代码的时候,还得跳过去看一下,这样反倒增加了阅读成本。

举个例子。代码具体如下所示。重构前,在 invest() 函数中,最开始的那段关于时间处理的代码,是不是很难看懂?重构之后,我们将这部分逻辑抽象成一个函数,并且命名为 isLastDayOfMonth,从名字就能清晰地了解它的功能,判断今天是不是当月的最后一天。这里,我们就是通过将复杂的逻辑代码提炼成函数,大大提高了代码的可读性。

// 重构前的代码
public void invest(long userId, long financialProductId) {
	Calendar calendar = Calendar.getInstance();
	calendar.setTime(date);
	calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1));
	if (calendar.get(Calendar.DAY_OF_MONTH) == 1) {
		return;
	}
	//...
}
// 重构后的代码:提炼函数之后逻辑更加清晰
public void invest(long userId, long financialProductId) {
	if (isLastDayOfMonth(new Date())) {
		return;
	}
//...
}
public boolean isLastDayOfMonth(Date date) {
	Calendar calendar = Calendar.getInstance();
	calendar.setTime(date);
	calendar.set(Calendar.DATE, (calendar.get(Calendar.DATE) + 1));
	if (calendar.get(Calendar.DAY_OF_MONTH) == 1) {
		return true;
	}
	return false;
}

避免函数参数过多

函数包含 3、4 个参数的时候还是能接受的,大于等于 5 个的时候,我们就觉得参数有点过多了,会影响到代码的可读性,使用起来也不方便。针对参数过多的情况,一般有 2 种处理方法。

(1)考虑函数是否职责单一,是否能通过拆分成多个函数的方式来减少参数

(2)将函数的参数封装成对象

除此之外,如果韩式是对外暴露的远程接口,将参数封装成对象,还可以提高接口的兼容性。在往接口中添加新的参数的时候,老的远程接口调用者有可能就不需要修改代码来兼容新的接口了。

勿用函数参数来控制逻辑

不要在函数中使用布尔类型的标识参数来控制内部逻辑,true 的时候走这块逻辑,false 的时候走另一块逻辑。这明显违背了单一职责原则和接口隔离原则。我建议将其拆成两个函数,可读性上也要更好。

public void buyCourse(long userId, long courseId, boolean isVip);
// 将其拆分成两个函数
public void buyCourse(long userId, long courseId);
public void buyCourseForVip(long userId, long courseId);

不过,如果函数是 private 私有函数,影响范围有限,或者拆分之后的两个函数经常同时被调用,我们可以酌情考虑保留标识参数。示例代码如下所示:

// 拆分成两个函数的调用方式
boolean isVip = false;
//...省略其他逻辑...
if (isVip) {
	buyCourseForVip(userId, courseId);
} else {
	buyCourse(userId, courseId);
}

// 保留标识参数的调用方式更加简洁
boolean isVip = false;
//...省略其他逻辑...
buyCourse(userId, courseId, isVip);

除了布尔类型作为标识参数来控制逻辑的情况外,还有一种“根据参数是否为 null”来控制逻辑的情况。针对这种情况,我们也应该将其拆分成多个函数。拆分之后的函数职责更明确,不容易用错。

函数设计要单一

相对于类和模块,函数的粒度比较小,代码行数少,所以在应用单一职责原则的时候,没有像应用到类或者模块那样模棱两可,能多单一就多单一

移除过深的嵌套层次

代码嵌套层次过深往往是因为 if-else、switch-case、for 循环过度嵌套导致的。建议,嵌套最好不超过两层,超过两层之后就要思考一下否可以减少嵌套。过深的嵌套本身理解起来就比较费劲,除此之外,嵌套过深很容易因为代码多次缩进,导致嵌套内部的语句超过一行的长度而折成两行,影响代码的整洁。

解决嵌套过深的方法也比较成熟,有下面 4 种常见的思路。

(1)去掉多余的 if 或 else 语句。代码示例如下所示:

// 示例一
public double caculateTotalAmount(List<Order> orders) {
	if (orders == null || orders.isEmpty()) {
		return 0.0;
	} else { // 此处的else可以去掉
		double amount = 0.0;
		for (Order order : orders) {
		if (order != null) {
			amount += (order.getCount() * order.getPrice());
		}
	}
		return amount;
	}
}
// 示例二
public List<String> matchStrings(List<String> strList,String substr) {
	List<String> matchedStrings = new ArrayList<>();
	if (strList != null && substr != null) {
		for (String str : strList) {
			if (str != null) { // 跟下面的if语句可以合并在一起
				if (str.contains(substr)) {
					matchedStrings.add(str);
				}
			}
		}
	}
	return matchedStrings;
}

(2)使用编程语言提供的 continue、break、return 关键字,提前退出嵌套。代码示例如下所示:

// 重构前的代码
public List<String> matchStrings(List<String> strList,String substr) {
	List<String> matchedStrings = new ArrayList<>();
	if (strList != null && substr != null){
		for (String str : strList) {
			if (str != null && str.contains(substr)) {
				matchedStrings.add(str);
				// 此处还有10行代码...
			}
		}
	}
	return matchedStrings;
}

// 重构后的代码:使用continue提前退出
public List<String> matchStrings(List<String> strList,String substr) {
	List<String> matchedStrings = new ArrayList<>();
	if (strList != null && substr != null){
		for (String str : strList) {
			if (str == null || !str.contains(substr)) {
				continue;
			}
			matchedStrings.add(str);
			// 此处还有10行代码...
		}
	}
	return matchedStrings;
}

(3)调整执行顺序来减少嵌套。具体的代码示例如下所示:

// 重构前的代码
public List<String> matchStrings(List<String> strList,String substr) {
	List<String> matchedStrings = new ArrayList<>();
	if (strList != null && substr != null) {
		for (String str : strList) {
			if (str != null) {
				if (str.contains(substr)) {
					matchedStrings.add(str);
				}
			}
		}
	}
	return matchedStrings;
}
// 重构后的代码:先执行判空逻辑,再执行正常逻辑
public List<String> matchStrings(List<String> strList,String substr) {
	if (strList == null || substr == null) { //先判空
		return Collections.emptyList();
	}
	List<String> matchedStrings = new ArrayList<>();
	for (String str : strList) {
		if (str != null) {
			if (str.contains(substr)) {
				matchedStrings.add(str);
			}
		}
	}
	return matchedStrings;
}

(4)将部分嵌套逻辑封装成函数调用,以此来减少嵌套。具体的代码示例如下所示:

// 重构前的代码
public List<String> appendSalts(List<String> passwords) {
	if (passwords == null || passwords.isEmpty()) {
		return Collections.emptyList();
	}
	List<String> passwordsWithSalt = new ArrayList<>();
	for (String password : passwords) {
		if (password == null) {
			continue;
		}
		if (password.length() < 8) {
			// ...
		} else {
			// ...
		}
	}
	return passwordsWithSalt;
}
// 重构后的代码:将部分逻辑抽成函数
public List<String> appendSalts(List<String> passwords) {
	if (passwords == null || passwords.isEmpty()) {
		return Collections.emptyList();
	}
	List<String> passwordsWithSalt = new ArrayList<>();
	for (String password : passwords) {
		if (password == null) {
			continue;
		}
		passwordsWithSalt.add(appendSalt(password));
	}
	return passwordsWithSalt;
}
private String appendSalt(String password) {
	String passwordWithSalt = password;
	if (password.length() < 8) {
		// ...
	} else {
		// ...
	}
	return passwordWithSalt;
}

除此之外,常用的还有通过使用多态来替代 if-else、switch-case 条件判断的方法。

学会使用解释性变量

常用的用解释性变量来提高代码的可读性的情况有下面 2 种。

(1)常量取代魔法数字。示例代码如下所示:

public double CalculateCircularArea(double radius) {
	return (3.1415) * radius * radius;
}
// 常量替代魔法数字
public static final Double PI = 3.1415;
public double CalculateCircularArea(double radius) {
	return PI * radius * radius;
}

(2)使用解释性变量来解释复杂表达式。示例代码如下所示:

if (date.after(SUMMER_START) && date.before(SUMMER_END)) {
	// ...
} else {
	// ...
}
// 引入解释性变量后逻辑更加清晰
boolean isSummer = date.after(SUMMER_START)&&date.before(SUMMER_END);
if (isSummer) {
	// ...
} else {
	// ...
}

最后,项目、团队,甚至公司,一定要制定统一的编码规范,并且通过 Code Review 督促执行,这对提高代码质量有立竿见影的效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值