文章目录
简介
写出诗一样的代码,不要写出屎一样的代码
命名
大到项目名、模块名、包名、对外暴露的接口,小到类名、函数名、变量名、参数名,只要是做开发,我们就逃不过“起名字”这一关。
命名的好坏,对于代码的可读性来说非常重要,甚至可以说是起决定性作用的。– 命名会影响可读性
1.命名的长度,多长合适
长的命名:
尽管长的命名可以包含更多的信息,更能准确直观地表达意图。
缺点:代码会很长,可能被分割成两行,影响可读性
短的命名:
变量推荐命名使用缩写,sec表示second,str表示string,num 表示number,doc表示document。
不影响变量表达意图,也能控制代码长度
类名这种作用域比较大的,推荐使用长命名,可以准确直观的表达意图
2.利用上下文简化命名
2.1 利用类class上下文简化命名
User是userName的上下文,因此定义为name更合适
public class User {
private String userName;
private String userPassword;
private String userAvatarUrl;
}
ex:
public class User{
private String name;
private String password;
private String avatarUrl;
}
当进行属性访问时
User user=new User();
user.getName();
2.2 利用函数function上下文简化命名
public void uploadUserAvatarImageToAliyun(String userAvatarImageUri);
利用上下文简化命名
public void uploadUserAvatarImage(String imageUri);
3. 命名可读、可搜索
3.1 命名可读
也就是命名要好念,不要绕口,大家都不是raper,不需要饶舌
我这里所说的“可读”,指的是不要用一些特别生僻、难发音的英文单词来命名,比如bob这种就很好发音
然而比如以下这些单词 你自己念念,哈哈哈
1、phenomenon (fi-nom-uh-non),[fɪ'nɒmɪnən]n. 现象
2、anaesthetist (uh-nes-thi-tist),[ə'nisθətɪst] n. 麻醉师
3、remuneration (ri-myoo - nuh-reyshun),[rɪ,mjuːnə'reɪʃ(ə)n]n. 报酬
4、statistics (stuh-tis-tiks), [stə'tɪstɪks] n. 统计
5、ethnicity (eth-nis-i-tee),[eθ'nɪsɪtɪ]n. 种族划分
6、philosophical (fil-uh-sof-i-kuhl), [fɪlə'sɒfɪk(ə)l]adj. 哲学的
7、provocatively(pruh-vok-uh-tiv),[pru'vktivl]adv.煽动地
8、anonymous (uh-non-uh-muhs),[ə'nɒnɪməs]adj. 匿名的
9、thesaurus (thi-sawr-uhs),[θɪ'sɔːrəs]n. 宝库;辞典
10、aluminium (al-yuh-min-ee-uhm),[æl(j)ʊ'mɪnɪəm]adj. 铝的
3.2 命名可搜索
命名可搜索。我们在 IDE 中编写代码的时候,经常会用**“关键词联想”**的方法来自动补全和搜索
最主要的就是 关键字要统一:
方法命令-”关键字联想“
大家都用select,insert,update,delete的时候
都用select的时候:你就不要别出新格的用 find,query,get
都用insert的时候:你就不要用 add,save
都用update的时候:你就不要用 put,modify
都用delete的时候:你就不要用 remove
这样你用IDEA,CTRL+SHIFT+F的时候就可以快速定位,当你使用”关键字联想“的技能时,也可以不费脑子的快速定位到自己想要的元素
命名接口和抽象类
接口命名:比较固定的方式
一种是加前缀“I”,表示一个 Interface – IUserService
实现类加后缀“Impl” – UserServiceImpl
抽象类:带上前缀“Abstract”-- AbstractConfiguration
注释
注释的目的就是让代码更容易看懂
注释的内容主要包含这样三个方面:做什么、为什么、怎么做
注释比代码承载的信息更多:
命名的主要目的是解释“做什么” – 比如这个函数做了什么 --what
1.函数和变量如果命名得好,确实可以不用再在注释中解释它是做什么的
2.对于类来说,包含的信息比较多,一个简单的命名就不够全面详尽了。这个时候,在注释中写明“做什么”就合情合理了。
注释起到总结性作用、文档的作用:
阅读代码可以明确地知道代码“怎么做”的。-- 比如这个函数实现的某个功能是怎么实现的-- how
在注释中,关于具体的代码实现思路,我们可以写一些总结性的说明、特殊情况的说明。这样能够让阅读代码的人通过注释就能大概了解代码的实现思路,阅读起来就会更加容易。
一些总结性注释能让代码结构更清晰
注释起到解释为什么会创建这个类或函数的意图:
比较复杂的类,如果已经有其他相同功能的类或函数了: 需要解释为什么这么做–why
下面这个就是比较经典的例子了 ,一个具有重复功能的国能
what: 做了什么
why:为什么做,因为更轻量
how:怎么做的,user specified object > SPI > configuration > default object.
/**
* (what) Bean factory to create beans.
*
* (why) The class likes Spring IOC framework, but is more lightweight.
*
* (how) Create objects from different sources sequentially:
* user specified object > SPI > configuration > default object.
*/
public class BeansFactory {
// ...
}
总结性注释:
public boolean isValidPasword(String password) {
// check if password is null or empty
if (StringUtils.isBlank(password)) {
return false;
}
// check if the length of password is between 4 and 64
int length = password.length();
if (length < 4 || length > 64) {
return false;
}
// check if password contains only a~z,0~9,dot
for (int i = 0; i < length; ++i) {
char c = password.charAt(i);
if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.')) {
return false;
}
}
return true;
}
注释是不是越多越好?
不是…
太多的问题的原因:就是代码写的可能不太好,要用很多注释来解释,但注释多了可能反而让代码可读性变差
太少:就需要费脑子来一句一句看代码想表达的真实意图。
按照我的经验来说,类和函数一定要写注释,而且要写得尽可能全面、详细,
而函数内部的注释要相对少一些,一般都是靠好的命名、提炼函数、解释性变量、总结性注释来提高代码的可读性。
总结性注释:
这样就只用看注释就知道干了什么,不用读代码
public boolean isValidPasword(String password) {
// check if password is null or empty
if (StringUtils.isBlank(password)) {
return false;
}
// check if the length of password is between 4 and 64
int length = password.length();
if (length < 4 || length > 64) {
return false;
}
// check if password contains only a~z,0~9,dot
for (int i = 0; i < length; ++i) {
char c = password.charAt(i);
if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.')) {
return false;
}
}
return true;
}
函数多大合适
类或函数的代码行数不能太多,但也不能太少。
类或函数的代码行数太多,一个类上千行,一个函数几百行,逻辑过于繁杂,阅读代码的时候,很容易就会看了后面忘了前面。
相反,类或函数的代码行数太少,在代码总量相同的情况下,被分割成的类和函数就会相应增多,调用关系就会变得更复杂
标准:
当一个类的代码读起来让你感觉头大了,实现某个功能时不知道该用哪个函数了,想用哪个函数翻半天都找不到了,只用到一个小功能要引入整个类(类中包含很多无关此功能实现的函数)的时候,这就说明类的行数过多了。
这个时候就需要做类拆分
一行代码多长合适
标准:一行代码最长不能超过 IDE 显示的宽度
善用空行分割代码块
对于比较长的函数,
如果逻辑上可以分为几个独立的代码块,
在不方便将这些独立的代码块抽取成小函数的情况下,
为了让逻辑更加清晰,结构也更清晰
public class Test {
public void execFunc(){
// 参数校验
chkParam();
// 处理数据
doData();
// 入库
insertData();
}
}
四行缩进还是两行缩进
个人比较推荐使用两格缩进,这样可以节省空间。特别是在代码嵌套层次比较深的情况下,累计缩进较多的话,容易导致一个语句被折成两行,影响代码可读性
不管是用两格缩进还是四格缩进,一定不要用 tab 键缩进
因为在不同的 IDE 下,tab 键的显示宽度不同,有的显示为四格缩进,有的显示为两格缩进
大括号是否要另起一行
// PHP
class ClassName
{
public function foo()
{
// method body
}
}
// Java
public class ClassName {
public void foo() {
// method body
}
}
个人还是比较推荐,将括号放到跟语句同一行的风格。理由跟上面类似,节省代码行数。
但是将大括号另起新的一行的方式,也有它的优势。这样的话,左右括号可以垂直对齐,哪些代码属于哪一个代码块,更一目了然
主要是团队要统一
类中成员的排列顺序
在java类文件中:
1.先要书写类所属的包名
2.罗列 import 引入的依赖类, Google 编码规范中,依赖类按照字母序从小到大排列
在类中:
成员变量排在函数的前面。
先定义变量 public int num=1;
再定义函数 public void func(){}
成员变量之间或函数之间,都是按照
“先静态(静态函数或静态成员变量)、
后普通(非静态函数或非静态成员变量)”的方式来排列的。
除此之外,成员变量之间或函数之间,
还会按照作用域范围从大到小的顺序来排列,
先写 public 成员变量或函数,
然后是 protected 的,最后是 private 的。
还有另外一种排列习惯,那就是把有调用关系的函数放到一块
代码分割成更小的代码块
大部分人阅读代码的习惯都是,先看整体再看细节。
所以,我们要有模块化和抽象思维,善于将大块的复杂逻辑提炼成类或者函数,屏蔽掉细节,–屏蔽掉细节
让阅读代码的人不至于迷失在细节中,这样能极大地提高代码的可读性。
不过,只有代码逻辑比较复杂的时候,我们其实才建议提炼类或者函数。
毕竟如果提炼出的函数只包含两三行代码,在阅读代码的时候,还得跳过去看一下,这样反倒增加了阅读成本
举例子:
invest函数中的最开始的时间处理的代码是不是很难懂?
// 重构前的代码
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;
}
重构后
我们将这部分逻辑抽象成一个函数,
并且命名为 isLastDayOfMonth,从名字就能清晰地了解它的功能,判断今天是不是当月的最后一天。
这里,我们就是通过将复杂的逻辑代码提炼成函数,大大提高了代码的可读性。
避免参数过多
个人觉得,函数包含 3、4 个参数的时候还是能接受的,
大于等于 5 个的时候,我们就觉得参数有点过多了,会影响到代码的可读性,使用起来也不方便。
两种处理方式:
考虑函数是否职责单一,是否能通过拆分多个函数的方式来减少参数。
public User getUser(String username, String telephone, String email);
// 拆分成多个函数
public User getUserByUsername(String username);
public User getUserByTelephone(String telephone);
public User getUserByEmail(String email);
将函数的参数封装成对象
public void postBlog(String title, String summary, String keywords, String content, String category, long authorId);
// 将参数封装成对象
public class Blog {
private String title;
private String summary;
private String keywords;
private Strint content;
private String category;
private long authorId;
}
public void postBlog(Blog blog);
除此之外,如果函数是对外暴露的远程接口,将参数封装成对象,还可以提高接口的兼容性。
在往接口中添加新的参数的时候,老的远程接口调用者有可能就不需要修改代码来兼容新的接口了
勿用函数参数来控制逻辑
不要在函数中使用布尔类型的标识参数来控制内部逻辑,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”来控制逻辑的情况:
我们也应该将其拆分成多个函数。拆分之后的函数职责更明确,不容易用错
public List<Transaction> selectTransactions(Long userId, Date startDate, Date endDate) {
if (startDate != null && endDate != null) {
// 查询两个时间区间的transactions
}
if (startDate != null && endDate == null) {
// 查询startDate之后的所有transactions
}
if (startDate == null && endDate != null) {
// 查询endDate之前的所有transactions
}
if (startDate == null && endDate == null) {
// 查询所有的transactions
}
}
// 拆分成多个public函数,更加清晰、易用
public List<Transaction> selectTransactionsBetween(Long userId, Date startDate, Date endDate) {
return selectTransactions(userId, startDate, endDate);
}
public List<Transaction> selectTransactionsStartWith(Long userId, Date startDate) {
return selectTransactions(userId, startDate, null);
}
public List<Transaction> selectTransactionsEndWith(Long userId, Date endDate) {
return selectTransactions(userId, null, endDate);
}
public List<Transaction> selectAllTransactions(Long userId) {
return selectTransactions(userId, null, null);
}
private List<Transaction> selectTransactions(Long userId, Date startDate, Date endDate) {
// ...
//具体执行的查询 --值负责查询
}
public static void main(String[] args){
//调用方来判断参数
if (startDate != null && endDate != null) {
// 查询两个时间区间的transactions
selectTransactionsBetween();
}
if (startDate != null && endDate == null) {
// 查询startDate之后的所有transactions
selectTransactionsStartWith()
}
if (startDate == null && endDate != null) {
// 查询endDate之前的所有transactions
selectTransactionsEndWith()
}
if (startDate == null && endDate == null) {
// 查询所有的transactions
selectAllTransactions();
}
}
函数设计要职责单一
我们在前面讲到单一职责原则的时候,针对的是类、模块这样的应用对象。
实际上,对于函数的设计来说,更要满足单一职责原则。
相对于类和模块,函数的粒度比较小,代码行数少,所以在应用函数设计的单一职责原则的时候,没有像应用到类或者模块那样模棱两可,能多单一就多单一。
public boolean checkUserIfExisting(String telephone, String username, String email) {
if (!StringUtils.isBlank(telephone)) {
User user = userRepo.selectUserByTelephone(telephone);
return user != null;
}
if (!StringUtils.isBlank(username)) {
User user = userRepo.selectUserByUsername(username);
return user != null;
}
if (!StringUtils.isBlank(email)) {
User user = userRepo.selectUserByEmail(email);
return user != null;
}
return false;
}
// 拆分成三个函数
public boolean checkUserIfExistingByTelephone(String telephone);
public boolean checkUserIfExistingByUsername(String username);
public boolean checkUserIfExistingByEmail(String email);
移除过深嵌套层次
代码嵌套层次过深往往是因为 if-else、switch-case、for 循环过度嵌套导致的。
我个人建议,嵌套最好不超过两层,超过两层之后就要思考一下是否可以减少嵌套。
过深的嵌套本身理解起来就比较费劲,除此之外,嵌套过深很容易因为代码多次缩进,导致嵌套内部的语句超过一行的长度而折成两行,影响代码的整洁。
// 示例一
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;
}
使用编程语言提供的 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;
}
调整执行顺序来减少嵌套:
// 重构前的代码
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;
}
将部分嵌套逻辑封装成函数调用,以此来减少嵌套
// 重构前的代码
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;
}
// 下面是执行加salt的逻辑,应该封装成函数
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) { // 封装password 加salt
String passwordWithSalt = password;
if (password.length() < 8) {
// ...
} else {
// ...
}
return passwordWithSalt;
}
学会使用解释性变量
常用的用解释性变量来提高代码的可读性的情况有下面 2 种。
常量取代魔法数字。示例代码如下所示:
public double CalculateCircularArea(double radius) {
return (3.1415) * radius * radius;
}
// 常量替代魔法数字
public static final Double PI = 3.1415; //把PI抽离出来 定义为常量
public double CalculateCircularArea(double radius) {
return PI * radius * radius;
}
使用解释性变量来解释复杂表达式。示例代码如下所示:
if (date.after(SUMMER_START) && date.before(SUMMER_END)) { //这个应该使用解释性变量
// ...
} else {
// ...
}
// 引入解释性变量后逻辑更加清晰
boolean isSummer = date.after(SUMMER_START)&&date.before(SUMMER_END);
if (isSummer) {
// ...
} else {
// ...
}