目录
一份整洁的代码对于一个系统是多么重要。如果代码写的乱七八糟,最后的结果就是无法对这些代码进行有效的管控。很有可能会毁掉这个系统。
什么才是整洁的代码?
Biarne Stroustrup -【C++语言发明者,C++Programming Language(中译版《C++程序设计语言》)一书作者】,我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。
有意义的命名
见名知意
命名要名副其实,虽然起个好名字要花时间,但省下来的时间比花掉的时间多。
变量、函数或类的名称需要表达出:它为什么会存在,它做什么事,应该怎么用。如果这个名称还需要注释来补充,那就不算名不副实。
int d; //消逝的时间,以日计 ...1
int daysSinceCreation; //2
如上代码,变量d什么也没有说明。和后面的注释八竿子打不着,第二行的代码就清晰多了。
public List <int[]> getThem() {
List < int[] > list1 = new ArrayList < int[] > ();
for (int[] x: theList) {
if (x[0] == 4) {
list1.add(x);
}
}
return list1;
}
上面的代码你或许有疑问:
-
(1) theList中是什么类型的东西?
-
(2) theList零下标条目的意义是什么?
-
(3)值4的意义是什么?
-
(4)我怎么使用返回的列表?
可能当时人知道意思,但接手开发肯定会一脸懵逼的。
如果对命名有困惑的,可以看看这个网站:https://unbug.github.io/codelf/
输入想要翻译的中文,下面会列举出「Github」上面使用过的相关命名。
避免误导
比如你想定义一组账号,不要用accountList,这样会误认为这是个List类型,除非真的是List类型的。可以使用accountGroup。
再来看下面代码:
int a=l;
if(O==D)
a=O1;
else
l=o1;
上面这串代码整的傻傻分不清O和0
,l和1
。简直亮瞎我的眼。
有意义的区分
public static void copyChars(char a1[],char a2[]) {
....
}
参数过于混乱,改成
public static void copyChars(char source[],char destination[]) {
....
}
看着舒服多了。
可搜索的名称
for(int j = 0;j < 34;j++) {
s += (t[j] * 4) / 5;
}
如图:魔法值太多。可以给魔法值命名。
private static final int WORK_DAYS_PRE_WEEEK = 5 ;
private static final int NUMBER_OF_TASKS = 34 ;
private static final int REAL_USE_DAYS = 4 ;
privat int sum = 0;
for(int j = sum;j < NUMBER_OF_TASKS;j++) {
s += (t[j] * REAL_USE_DAYS) / WORK_DAYS_PRE_WEEEK;
}
向上面这样,至少可以搜索得到。
类名与方法名
类名应该是名词短语。如:Student、Person、Account
。
方法名应该是动词短语。如:getStudent、listPerson、save
规范的方法
短小精悍
有些开发写的方法内容上千行,这样的方法估计连自己看着都累,为何不将内容作适当抽取呢。
方法要短小。一般一个方法20行就足够了。
阿里巴巴要求一个方法总行数不能超过80行。
只做一件事
就是说每个方法只应该有一个功能,如果你要写的方法功能较多,建议抽取,然后再组合。
public void drawLottery() {
listUser(); //1.查询用户
drawHandler(); //2.抽奖算法
resultHandler();//3.抽奖结果处理
}
如上面代码,将多个方法组合起来成一个方法清晰明了。如果把这3个功能全写在drawLottery()
。后面的开发来看,估计头都要看秃。😭
使用描述性的名称
先来举个栗子
List<UpkeepConfig> upkeepConfigs = upkeepConfigMapper.getAll(upkeepConfig);
上面代码getAll()
一看以为是获取所有的list
,但是仔细看不是这个意思。我认为这样命名比较合适:
listByEntity()
,这样命名我很快就能知道:1.这个方法是返回list;2.这个方法是一个条件查询;3.入参是一个实体。
别害怕长名称。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说清其功用的名称。
方法参数
最理想的参数数量是零(零参数函数),其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够特殊的理由才能用三个以上参数(多参数函数)——所以无论如何也不要这么做。
试想如果一个方法参数过长,也不利于其他开发者阅读,不利于测试编写测试用例。
public Object getTransferTaskByCondition(HttpServletRequest request,String taskStatus
,String keyword,String materialNumber,String deviceCode
,String vehicleVinNumber,String vehicleServiceDuty
,String arriDutyCell,String equipElement,String tenant,String blDivisionCode
,String plateNumber,Integer pageNum,Integer pageSize,String deviceTypeCode){
...
}
上面这个方法的参数就问你怕不怕。
上面代码参数可以做适当封装。
public Object getTransferTaskByCondition(HttpServletRequest request,TransferTask transferTaskParam){
...
}
如果函数看来需要两个、三个或三个以上参数,就说明其中一些参数应该封装为类了。
无副作用
方法承诺只做一件事,但还是会做其他被藏起来的事。有时,它会对自己类中的变量做出未能预期的改动。有时,它会把变量搞成向方法传递的参数或是系统全局变量。无论哪种情况,都是具有破坏性的,会导致古怪的时序性耦合及顺序依赖。
public class UserValidator {
ptivate Cryptographer cryptographer;
public boolean checkPassword(String UserName,String password){
User user = UserService.getByName(userName);
if(user != User.NULL){
String codedPhrase = user.getPassword();
String phrase = cryptographer.decrypt(codedPhrase ,password);
if ("valid Password"equals(phrase)){
Session.initialize();
return true;
}
return false;
}
}
如上面代码,反方法名checkPassword
以为就是一个密码校验。但是看方法有一个Session初始化。该名称并未暗示它会初始化该次会话。所以,当某个误信方法名的调用者想要检查用户有效性时,就得冒抹除现有会话数据的风险。
分隔指令与询问
方法要么做什么事,要么回答什么事。方法应该修改某对象的状态,或是返回该对象的有关信息。两样都干常会导致混乱。看看下面的例子:
public boolean set(String attribute, String value);
这个方法我们知道,设置某个属性成功返回true,否则返回false。
但如果这样
if(set("userName","lvshen")){
....
}
其他开发阅读这段代码时,会有疑问,这是在表达 username属性值是否之前已设置为 lvshen吗?或者它是在表达username属性值是否成功设置为 lvshen呢?从这行调用很难判断其含义,因为set看不清是动词还是形容词。
这时好的解决方案是:
if (attributeExists("username")){
setAttribute("username","lvshen");
}
抽离try/catch代码块
建议将try和catch代码块的主体部分抽离出来,如下
public void delete(Page page) {
try{
deletePageAndAllReferences(page);
}catch(Exception e){
logErrr(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception {
...
}
另外不要对大段代码进行try/catch,这样不利于定位问题。
行动起来
下面这段话摘至《Clean Code》作者:
❝我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称是随意取的,也会有重复的代码。不过我会配上一套单元测试,覆盖每行丑陋的代码。
然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法有时我还拆散类。同时保持测试通过。
最后,遵循本章列出的规则,我组装好这些函数我并不从一开始就按照规则写函数。我想没人做得到
❞
就像写作文一样,好的代码也不是一次性写出来的,需要反复琢磨。
必要和不必要的注释
无用的注释
糟糕的代码才写注释,如果能用代码表达,为何还要加注释呢。
良好的注释能够提高代码的阅读效率。然而乱七八糟的注释有可能会搞坏这个功能。
注释会撒谎。也不是说总是如此或有意如此,但出现得实在太频繁。注释存在的时间越久,就离其所描述的代码越远,理解起来就很容易错误。原因很简单。程序员不能坚持维护注释。
要知道注释也不能美化糟糕的代码,所以花点时间好好重构下代码吧。
有用的注释
当然有些注释也是必要的。比如待开发的「TODO」注释,API的Javadoc注释。
废话注释
/**
*默认构造函数
*/
protected AnnualDateRule();
/**
*每月天数
*/
private int dayofMonth;
/**
*
*@return 每月天数
*/
public int getDayOfMonth(){
return dayofMonth;
}
像上面这种注释就感觉是废话了。
注释掉的代码
不用的代码要不删掉,要不注释说明不要删。如果注释了大段代码,又不做任何说明,其他人看见了也不敢删掉,或者本来是还有用的代码被误删了。
这样导致注释掉的代码堆积在一起,越来越臃肿。
格式
代码顺序
若某个方法调用了另外一个,就应该把它们放到一起,而且调用者应该尽可能放在被调用者上面。这样,程序就有个自然的顺序。若坚定地遵循这条约定,读者将能够确信方法声明总会在其调用后很快出现。这样极大的增强了整个模块的可阅读性。
public void funA() {
funB();
funC();
}
public void funB(){
...
}
public void funC(){
...
}
当然一个开发团队应该有自己固定的格式规则。开发遵循规则就可以了。
别返回null值
假设有着一段代码:
List<Student> students = getStudents();
if(students != null) {
students.forEach(student -> {
student.setName(name);
});
}
这里有非空判断,是因为getStudents()
有返回null的情况。如果该方法修改为返回空list
(建议返回不可变集合ImmutableList.of()
),就少了if
判断,何乐而不为。
List<Student> students = getStudents();
students.forEach(student -> {
student.setName(name);
});
必要的单元测试
对于系统的核心功能,一定要有单元测试,单元测试有利于提高系统健壮性。而且有利于重复测试。这样比用swagger方便的多。而且其他程序员也可以测试该方法并了解其功能。
当然,测试代码也需要干净整洁。不易读懂,混乱的测试代码等同于没有测试。
类
类应该短小,建议不要超过500行。
当然你可能害怕数量巨大的短小的类会让人一难以下子一目了然抓住全局。
这就好比:你是想把工具归置到有许多抽屉、每个抽屉中装有定义和标记良好的组件的工具箱中呢,还是想要少数几个能随便把所有东西扔进去的屉?
近5000行的类就问怕不怕。
逐步改进
系统需要要迭进,在迭进过程中生成干净整洁的代码。这里涉及到重构代码,去除重复性代码。
关于重构,你可以特意留意命名方式,函数大小,代码格式。
❝代码能工作还不够。能工作的代码经常会严重崩溃。满足于仅仅让代码能工作的程序员不够专业。他们会害怕没时间改进代码的结构和设计,我不这么认为。没什么能比糟糕的代码给开发项目带来更深远和长期的损害了。进度可以重订,需求可以重新定义,团队动态可以修正。但糟糕的代码只是一直腐败发酵,无情地拖着团队的后腿。我无数次看到开发团队蹒跚前行,只因为他们匆。 ——来自《Clean Code》
❞
关于自己编码的一些经验
for循环
或许你会经经常写下面的代码:
students.forEach( stu -> {
...
xxxMapper.getById(stu.getId()); //数据库查询
...
});
如果上面的students数量不可控,那么for循环次数也就不可控。就会有未知的数据库查询次数。如果有1000个学生,那么一个用户调用这里查1000次数据库,1000个用户调用这里查 次,在并发场景下对数据库压力有多大,想想都可怕。
建议这么写:
//先一次批量查询
List<Student> sts = xxxMapper.listByIds(ids);
//然后转换成Map
Map<String,List<Student>> stuMap = sts.stream().collect(Collectors.groupingBy(Student::Id));
students.forEach( stu -> {
//通过map获取
stuMap.get(stu.getId());
});
更新
来看这段代码:
SourceDetail update = sourceDetailService.getById(id);
update.setXXX(xxx);
...
sourceDetailService.updateNotNull(update);
updateNotNull
实际上就是通过主键更新,这里知道了主键就没必要先查一次库了。可以这样做:
SourceDetail update = new SourceDetail();
update.setId(xxx);
...
sourceDetailService.updateNotNull(update);
内存节省
Arrays.asList(strArray)
返回值是仍然是一个可变的集合,但是返回值是其内部类,不具有add
方法,可以通过set
方法进行增加值,默认长度是「10」。
Collections.singletonList()
返回的同样是不可变的集合,但是这个长度的集合只有「1」,可以减少内存空间。但是返回的值依然是Collections
的内部实现类,同样没有add
的方法,调用add
,set
方法会报错。
别用Random生成随机数
由于java.util.Random
类依赖于伪随机数生成器,因此该类和相关的java.lang.Math.random()
方法不应用于安全关键应用程序或保护敏感数据。在这种情况下,应该使用依赖于加密强随机数生成器(RNG)的java.security.SecureRandom
类。
「PRNG(伪随机数):」伪随机数, 计算机不能生成真正的随机数,而是通用一定的方法来模拟随机数。伪随机数有一部分遵守一定的规律,另一部分不遵守任何规律。
「RNG(随机数):」随机数是由“随机种子”产生的,“随机种子”是一个无符号整形数。
//反例:
Random random = new Random();
byte bytes[] = new byte[20];
random.nextBytes(bytes);
//正例:
SecureRandom random = new SecureRandom();
byte bytes[] = new byte[20];
random.nextBytes(bytes);
如果再多线程情况下,建议用ThreadLocalRandom
。
ThreadLocalRandom
相对于Random
可以减少多线程资源竞争,保证了线程的安全性。public class ThreadLocalRandom extends Random
因为构造器是默认访问权限,只能在java.util
包中创建对象,故提供了一个方法ThreadLocalRandom.current()
用于返回当前类的对象。
善用Java8 API
还是举例子,如果你要计算两个日期的时间差。你可能会这样做:
Calendar bef = Calendar.getInstance();
Calendar aft = Calendar.getInstance();
bef.setTime(before);
aft.setTime(after);
long result= (aft.getTimeInMillis()-bef.getTimeInMillis())/(1000*3600*24);
这里我建议使用「Java8」的日期类:
long diffMinutes = ChronoUnit.MINUTES.between(Instant.now(), sendDate.toInstant());
ChronoUnit
拥有不可变和线程安全性,而Calendar
用作共享变量本身没有线程安全控制的。同样Instant
也是不可变对象。所以尝试使用Java8的日期时间类吧。
不要怕麻烦,写完代码后,请花点时间,优化下自己的代码,并养成习惯。
这是对自己负责,也是对系统负责。
最后
关注公众号「Lvshen_9」,后台回复"「手册」",获取阿里巴巴Java开发手册最新版。