代码整洁之道
定义变量名字的时候常用的单词
in,此单词可以翻译成是用的意思,比如说你想定义一个消逝的时间以日计的变量,你就可以定义成int elapsedTimeInDays;如果你想定义一个消逝的时间以年计的变量,那么你就可以定义成是int elapsedTimeInYears;
使用info后缀表示信息,比如如果我们想定义一个变量表示学生信息,那么我们可以写成是Sting studentInfo;
source源头通常配合target目标地一块出现使用;
有意义的命名
类的命名一定要是名词,类中方法的命名一定要是动词
只有类的命名用名词,方法的命名用动词,我们程序员在阅读代码的过程中才像是正在阅读现实中的解决问题,我们写的代码才会易读性比较强。
首先如果是我们给类命名,因为类其实就是对应于我们现实世界中的一个对象,因此类的命名必须是名词,比如说Student学生类,Employee员工类,像这些类你可以发现命名的时候都是使用的名词。
而像我们类里面的方法,它是对应于现实中的此对象的一个行为的,因此我们对方法的命名都需要使用动词,比如说Student学生类中有一个drink喝饮料的方法,我们就可以给此方法命名成drink动词。
变量的命名一定要符合它的具体身份,要名副其实
我们在开发的时候,在命名变量的时候,要求见名知意,当我们读到这个变量的时候,我们就可以具体的知道这个变量是干什么的。比如说我们现在要定义一个变量,这个变量是记录我们消逝的时间的并且计量单位是以日计,加入我们写成是int d;那么你看到变量d肯定什么都不知道,会一头雾水。但是如果你定义成是int elapsedTimeInDays;那么你是不是一看到这个变量就能知道它是什么意思。
我们命名变量的一个要求就是,要一眼就能让别人看出来它是干什么的。
变量的命名在别人能够看出来的基础上要尽量做到短小精悍,名字越短越好
比如说如果你想定义一个表示学生姓名的变量,你写成String studentNameString;肯定是没有写成String name;让读者更容易去理解的,读者一看就知道name变量表示的是学生的名字,虽然studentNameString变量也能让读者知道它表示的是学生的名字,但是这个变量太长了,读者读起来太不方便了,会很累。所以你的变量命名,在读者能读懂的基础上越简洁越好,越短小精悍越好。
别用常用工具中已有的变量名字命名方法名
比如说,你现在想在某个类中增加一个增加元素的方法,那么你前要不要把这个方法的名字命名成是add,因为我们的的List集合中已经有add方法了,所以如果我们在自定义类中再写一个add方法,那么可能会让读者产生歧义,读者的第一反应可能是这个方法是不是List集合中已有的方法,所以它需要去思考,因此如果你命名成add方法名,会让读者地起来很累。
要把命名做有意义的区分,不要做无意义的废话区分
比如,如果我们现在有三个类,一个是Student类,一个是StudentInfo类,另外一个是StudnetData类,你能够区分这是三个类的区别吗?显然不能,你只能知道这三个类都和学生有关系。所以像通过Info和Data区分,这些都是写没有意义的区分,我们要尽量避免这种情况的发生。我们可以写成是Student类,StudentAddress类,StudentSchool类,这样你一看StudentAddress类就知道它和学生的住址有关系,一看StudentSchool类就知道它和学生的学校有关系。
怎样写简洁的,别人一眼可以看的懂的方法
先看一下下面的这段方法代码的架构:
public String getTotalMoney(String good){
if(...) {
if(...) {
if(...) {
}
}
} else {
if(...) {
if(...) {
} else {
}
}
}
if(...) {
}
...
}
像上面的这个方法架构你看着什么感觉?肯定是心烦意乱的,你肯定也看不懂,那么为什么会看不懂呢?主要是因为你用上面的这个架构写出来的方法,方法的易读性非常的差劲,因为代码太多了,每个细碎的点太多了,我们的主方法不应该是这样的方法。我们的主方法里面不应该有细枝末节,主方法主要是面向的功能方法。其实像上面的这个主方法,它里面有很多段代码,每一段代码可能都有一段特殊的功能,我们可以把有特殊功能的代码抽取出来,封装成另外一个方法,如下:
public String getTotalMoney(String good){
//功能1
功能1(...);
//功能2
功能2(...);
//功能3
功能3(...);
}
public ... 功能1(...) {...}
public ... 功能2(...) {...}
public ... 功能3(...) {...}
//如果按照上面的这样写,把主方法里面的每一段具有某个功能的方法都抽取出来,封装成一个单独的方法,这样就可以使你的主方法简洁许多,这个时候你再去读getTotalMoney这个方法里面的代码,是不是一眼就能看出来它是什么意思了。
还有一点是,向if代码块里面的层级,和else代码块里面的层级,尽量只让它们有一级,就是if块里面千万不要再继续套if块,else块里面也不要再继续套if块,像最上面的那个没有重构之前的方法架构,它的if里面套的if块和else里面套的else块都太多了,这样会让读你代码的程序员非常难以阅读,难以理解。还有while循环也需要只有一层,就是while块里面不能还有while块。
每个方法只做一件事
每个方法都应该只做一件事,也就是每个方法里面只能有一个功能。比如说我们的主方法,它里面主要是调用我们各种小的功能方法,主方法的主要作用就是为读我们方法的程序员展示此方法的主要流程。比如说你的主方法是购物,那么主方法里面需要有很多小的功能方法,比如说获取物品信息功能方法,获取用户信息功能方法,结账功能方法,如下:
public void goShopping() {
//获取物品要购买的物品信息
getShopItemInfo();
//获取用户信息
getUserInfo();
//结账
pay();
}
public void getShopItemInfo(){
....
}
public void getUserInfo(){
...
}
public boolean pay(){
...
}
//像上面的关于购物功能的例子,主方法就是goShopping,主方法里面是每个小的功能方法,我们通过这个主方法就能知道购物的流程,第一步先获取物品,第二步获取用户信息,第三步用户结账。因此读我们方法的程序员,根本就不需要读我们getShopItemInfo(),getUserInfo(),pay()的这三个方法里面的具体细节,就可以知道主方法的作用,就可以了解到我们实际要处理的现实问题。这就大大提高我们我们编写的代码的易读性。当然你也可以把getShopItemInfo(),getUserInfo(),pay()的方法体代码都写到主方法goShopping里面,但是这样会出现什么问题呢?就是阅读你代码的其它程序员会真的看不懂你这个主方法到底有什么作用,因此这样会让你写的代码易读性非常差劲儿。
所以我们要求每个方法里面只做一件事,比如说你的主方法就是要告诉读者你解决的现实问题的实际流程是什么样的,因此你的主方法里面必须全是其它的小功能代码。而像你的小功能代码里面同样也是只做一件事,如果你的小功能代码里面有其他功能,那么这个功能同样也需要抽取出来,抽取到另外一个方法当中。比如说假设你上面的小功能方法pay()中,有一段代码是需要判断一下用户的钱数是否足够,如下:
public boolean pay(){
User user = getSelect(user);
if(user == null) {
return false;
}
int userMoney = user.selectMoney();
int totalMoney = data.selectAllMoney();
if(userMoney < totalMoney) {
return false;
}
//实际付钱
user.payMoney();
return true;
}
//像上面的这个用户付钱的方法当中,它并不是只干一件事,也就是说它里面并不是只有一个功能,因此这不符合我们的代码整洁之道,我们需要把这个方法里面的多余功能抽取出来,这个方法里面的前半部分只有一个功能,就是校验用户钱数是否足够,后半部分才是真正的付钱,因此我们可以把前半部分封装到另外一个方法中,如下:
public boolean pay(){
if(!isEoughUserMoney){
return false;
}
//实际付钱
user.payMoney();
return true;
}
public boolean isEoughUserMoney(){
User user = getSelect(user);
if(user == null) {
return false;
}
int userMoney = user.selectMoney();
int totalMoney = data.selectAllMoney();
if(userMoney < totalMoney) {
return false;
}
return true;
}
//经过上面的重构之后,明显可以发现我们的主方法(现在pay方法相当于主方法)里面的代码简洁易读多了。
因此,我们在写方法的时候一定要记住一条死的准则,一个方法只敢一件事情,也就是一个方法里只能有一个功能,多余的功能代码要抽取到另外一个方法当中。如果另外一个方法当中仍有多余的代码,我们要继续往外边抽,一定要抽取干净。我们的目的是,让我们的主方法里面只有解决现实问题的具体流程,让读者一看便知道此方法解决的问题是什么,而对于具体每个功能的细节代码,就需要封装到主方法之外的方法中了。
方法参数越少越好
在定义方法的时候,我们的方法参数越少,用户看到我们的方法越容易理解,最理想的情况是方法没有参数,我们的方法最多只能有三个参数,如果超过了三个参数,你就需要把这些参数想办法封装成一个类了。为什么方法的参数越多,我们读起来越难理解呢?因为方法参数越多,你需要翻译的信息越多,你需要去想这个方法到底是什么含义,所以你在阅读这个方法的时候,会更加困难一些。
比如说:
writeField()方法就比writeField(name)方法容易理解;而writeField(name)方法就比writeField(name,outputStream)更容易理解。如果我们一个方法中超过了三个参数,那这就糟糕透了,我们需要想办法把一些参数封装到类里面。
方法中重复的代码要提取出来
对于一个方法中如果有两段或两段以上的重复代码一定要抽取出来,抽取到另外一个方法中,比如有一个用户购完物付钱的方法,如下:
public boolean pay(User studentUser,User employeeUser) {
int totalMoneyUser = getTotalMoney(studentUser);
int totalMoneyEmployee = getTotalMoney(employeeUser);
int totalMoney = getTotalMoney();
if(totalMoney < totalMoneyUser) {
return false;
}
if(totalMoney < totalMoneyEmployee) {
return false;
}
return true;
}
上面的代码其实有两段逻辑重复的代码,我们可以把它抽取出来,抽取到另外一个方法中,如下:
public boolean pay(User studentUser,User employeeUser) {
return isEoughMoney(studentUser) && isEoughMoney(employeeUser) ? true : false;
}
public boolean isEoughMoney(User user) {
int totalMoney = getTotalMoney();
int totalMoneyUser = getTotalMoney(user);
if(totalMoney < totalMoneyUser) {
return false;
}
return true;
}
注释
为什么会出现注释?注释的恰当用法是为了弥补我们在用代码表达意图时遭遇的失败,我们因为没办法用我们的变量名或者方法名表达我们的意图,因此我们需要给变量名或者是方法名加上一个注释,来向读者解释他们的意思,因此注释就是来弥补我们用代码表达意图时遭遇的失败的。
注释是非常不推荐使用的,为什么呢?因为注释会撒谎,因为注释存在的时间越久,就离其所描述的代码越远,越来越变得全然错误,原因很简单,程序员不能坚持维护注释。而且一些坏的注释完全就是废话或者是与我们本来的方法的所要表达的意思不一致,所以会特别的误导读者,让读者难以理解我们的代码所要表达的真正的意图。
不准确的注释要比没注释坏的多,它们满口胡言,它们预期的东西永不能实现。它们设定了无需也不应再遵循的旧规则。真实只在一处地方有:代码。只有代码能忠实地告诉你它做的事。那是唯一真正准确的信息来源。所以,尽管有时也需要注释,但我们应该多花心思减少注释量。
因此,我们平常要尽量给变量还有方法起一个名副其实的名字,让读者一看到这个名字就知道表达的是什么意思,这样我们就可以省去写注释了。
但有一些注释确实是必须要写的,比如说//todo注释,这个注释主要是提醒读者,当前方案不够完美,提醒读者后期注释的方法或者变量需要优化,如下:
//todo 此方法后期需要改变
public void appendMoney(){...}
再比如说,有一些代码需要警示其他程序员,因此我们必须要给这段代码加上注释,防止其他程序员进行操作,如下:
//这个方法不能动,否则后面会有大麻烦
public void appendMoney(){...}
能用函数或变量时就别用注释
看看一下代码概要:
// does the module from the global list <mod> depend on the subsystem we are part of?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))
可以改写成一下没有注释的版本
ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))
格式
你应该选用一套管理代码格式的简单规则,然后贯彻这些规则。如果你在团队中工作,则团队应该一致同意采用一套简单的格式规则,所有成员都要遵从。
几个要点:
- 类中的成员属性要紧密联系在一起,中间不能换行。类中的行为方法,彼此之间一定要换行。等号左右要各留出一格空格,方法的右括号和右边的花括号之间需要留出一格空格
public class ReporterConfig {
private String className;
private List<Property> properties = new ArrayList<Property>();
public void addProperty(Property property) {
properties.add(property);
}
public void removeProperty(Property property) {
properties.remove(property);
}
}
- if,else,while,for后面的小括号不要留空格,但是最右边的小括号和花括号之间需要空出一个格,for的each循环中的:冒号左右要各留一格空格。
private static void findJavaFiles(File parentDirectory, List<File> files) {
for(File file : parentDirectory.listFiles()) {
if(file.getName().endsWith(".java")) {
files.add(file);
} else if(file.isDirectory) {
findJavaFiles(file, files);
}
}
while((line = br.readLine()) != null) {
measureLIne(line);
}
}
- 定义方法的时候,方法的参数彼此之前需要在逗号后面空出一个,并且调用方法的时候,给参数传递实参的时候彼此之间也需要在逗号后面空出一格。主方法和主方法里面调用的小功能方法,要垂直放到一起,一般而言,我们想要自上而下展示函数的调用依赖顺序,也就是说,被调用的函数应该放在执行调用的函数下面,这样就建立了一种自顶向下贯穿源代码模板的良好信息流。像报纸文章一般,我们指望最重要的概念先出来,指望以包括最少细节的方式表述它们。我们指望底层细节最后出来,这样我们就能扫过源代码文件,自最前面的几个函数获知要旨,而不至于沉溺到细节中。
public boolean pay(User user, Good good) {
checkUserExist(user);
checkGoodEnough(good);
pay(user, good); //可以看出我们调用这个方法的时候,参数彼此之间需要在某个逗号后面加一个空格,和我们定以方法的时候一样
}
public void checkUserExist(User user) {
....
}
public void checkGoodEnough(Good good) {
....
}
public void pay(User user, Good good) { //定以方法的时候,参数的逗号后面也需要加上一个空格
....
}
//并且从上面的例子也可以看出,主方法和主方法里面用到的小的功能方法必须要放到一起,被调用的函数放到了执行调用的函数的小面。这样我们的连续的四块就是一个解决付款问题的区域,比较易读,简洁。
- 两个数运算的时候,它们的运算表达式,左右也要各空出一格。但如果你是想突出整体,那么不能再运算符的左右加空格。
public class Quadratic {
public static double root1(double a, double b, double c) {
double determinant = determinant(a, b, c);
return (-b + Math.sqrt(determinant)) / (2*a); //因为这里我们想要突出的是2*a整体所以*中间就不用加空格了
}
public static double root2(int a, int b, int c) {
double determinant = determinant(a, b, c);
return (-b - Math.sqrt(determinant)) / (2*a);
}
private static double determinant(double a, double b, double c) {
return b*b - 4*a*c; //因为这里我们想要突出b*b和4*a*x它们两个整体,因此*号中间就不用加空格了
}
}
- 每一行的字符不能超过一百个,超过了需要换行。
对于idea编译器来说,到中间的竖线的地方刚好一百个字符左右,中间就是每行限制的字符最大个数,每行的长度不能超过这条中间,如果超过了需要换行,如下图:
别给方法返回null值
如果我们程序中有很多方法的返回值是null,那么你的程序中就会有很多隐藏的空指针异常风险。而且如果其他方法返回的是null值,别的调用者就需要在调用的时候检查是否为null的情况,这也会让代码结果非常的繁琐,需要进行很多次的if,如下:
public void registerItem(Item item) {
if(item != null) {
ItemRegistry tegistry = peristentStore.getItemRegistry();
if(registry != null) {
Item existing = registry.getItem(item.getID());
if(existing.getBillingPeriod().hasRetailOwner()) {
existing.register(item);
}
}
}
}
//就如同上面的例子一样,加入你的getItemRegistry方法的返回值可以是null,那么这个方法的调用者,就不得不在调用的时候先检查一下得到的结果registry是否为null,否则会发生空指针异常。并且不知道你有没有注意到if里面的第一行中的peristentStore没有进行null的校验,所以如果它是null就会发生空指针异常,导致整个程序崩溃。
如果你的方法返回值是一个list集合,那么如果返回的是空尽量返回一个空的集合,但是千万不要返回一个null,否则会让你的代码健壮性非常低,易读性非常差,如下:
List<Employee> employees = getEmployees();
if(employees != null) {
for(Employee e : employees) {
totalPay += e.getPay();
}
}
//现在,getEmployees可能返回null,但是否一定要这么做呢?如果修改getEmployees,返回空列表,就能使代码整洁起来,如下:
List<Employee> employees = getEmployees();
for(Employee employee : employees) {
totalPay += e.getPay();
}
因此后面在开发系统的时候,方法的返回值一定不要返回null。
我们该提炼方法中的变量还是不提炼方法中的变量?
首先,我们不管是提炼方法中的变量,还是不提炼方法中的变量,目的其实只有一个,就是为了让我们的代码可读性变强,因此我们在判断该不该提炼方法中的变量的时候,其实也是围绕着这个目的进行的,如果根据实际情况你觉得提炼出一个解释性的变量,代码会变得更加的易读,那么你就提取出来;如果你觉得就算不提取变量,代码也是易读的,那么你就不要提取,因为提取之后会让你的代码变得冗杂繁琐;
下面是一个提取的例子:
//没提取之前
return order.quantity * order.itemPrice - Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1, 100);
//可以发现,如果我们使用一个表达式,而没有对这个表达式提取变量,这个时候读我们代码的人就看不出我们的代码到底是什么意思,因此这个时候为了增加我们代码的易读性,我们必须要从表达式中提取出来一些具有解释说明含义的变量,如下:
int basePrice = order.quantity * order.itemPrice;
int quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
int shippint = Math.min(order.quantity * order.itemPrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
//经过上面的解释性变量提取之后,可以明显发现,我们的代码变得易读多了