百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

我写出这样干净的代码,老板直夸我

csdh11 2025-01-26 21:48 26 浏览

一份整洁的代码对于一个系统是多么重要。如果代码写的乱七八糟,最后的结果就是无法对这些代码进行有效的管控。很有可能会毁掉这个系统。

什么才是整洁的代码?

Biarne Stroustrup -【C++语言发明者,C++Programming Language(中译版《C++程序设计语言》)一书作者】,我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。

有意义的命名

见名知意

命名要名副其实,虽然起个好名字要花时间,但省下来的时间比花掉的时间多。

变量、函数或类的名称需要表达出:它为什么会存在,它做什么事,应该怎么用。如果这个名称还需要注释来补充,那就不算名不副实。

int?d;?//消逝的时间,以日计???...1

int?daysSinceCreation;???????//2

如上代码,变量d什么也没有说明。和后面的注释八竿子打不着,第二行的代码就清晰多了。

public?List??getThem()?{
????List??list1?=?new?ArrayList??();
????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?

如图:魔法值太多。可以给魔法值命名。

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?

向上面这样,至少可以搜索得到。

类名与方法名

类名应该是名词短语。如:Student、Person、Account。

方法名应该是动词短语。如:getStudent、listPerson、save

规范的方法

短小精悍

有些开发写的方法内容上千行,这样的方法估计连自己看着都累,为何不将内容作适当抽取呢。

方法要短小。一般一个方法20行就足够了。

阿里巴巴要求一个方法总行数不能超过80行。

只做一件事

就是说每个方法只应该有一个功能,如果你要写的方法功能较多,建议抽取,然后再组合。

public?void?drawLottery()?{
???listUser();??//1.查询用户
???drawHandler();?//2.抽奖算法
???resultHandler();//3.抽奖结果处理
}

如上面代码,将多个方法组合起来成一个方法清晰明了。如果把这3个功能全写在drawLottery()。后面的开发来看,估计头都要看秃。

使用描述性的名称

先来举个栗子

?List?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?students?=?getStudents();
if(students?!=?null)?{
???students.forEach(student?->?{
?????student.setName(name);
???});
}

这里有非空判断,是因为getStudents()有返回null的情况。如果该方法修改为返回空list(建议返回不可变集合ImmutableList.of()),就少了if判断,何乐而不为。

List?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?sts?=?xxxMapper.listByIds(ids);
//然后转换成Map
Map>?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的日期时间类吧。

不要怕麻烦,写完代码后,请花点时间,优化下自己的代码,并养成习惯。

这是对自己负责,也是对系统负责。

相关推荐

探索Java项目中日志系统最佳实践:从入门到精通

探索Java项目中日志系统最佳实践:从入门到精通在现代软件开发中,日志系统如同一位默默无闻却至关重要的管家,它记录了程序运行中的各种事件,为我们排查问题、监控性能和优化系统提供了宝贵的依据。在Java...

用了这么多年的java日志框架,你真的弄懂了吗?

在项目开发过程中,有一个必不可少的环节就是记录日志,相信只要是个程序员都用过,可是咱们自问下,用了这么多年的日志框架,你确定自己真弄懂了日志框架的来龙去脉嘛?下面笔者就详细聊聊java中常用日志框架的...

物理老师教你学Java语言(中篇)(物理专业学编程)

第四章物质的基本结构——类与对象...

一文搞定!Spring Boot3 定时任务操作全攻略

各位互联网大厂的后端开发小伙伴们,在使用SpringBoot3开发项目时,你是否遇到过定时任务实现的难题呢?比如任务调度时间不准确,代码报错却找不到方向,是不是特别头疼?如今,随着互联网业务规模...

你还不懂java的日志系统吗 ?(java的日志类)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

谈谈枚举的新用法--java(java枚举的作用与好处)

问题的由来前段时间改游戏buff功能,干了一件愚蠢的事情,那就是把枚举和运算集合在一起,然后运行一段时间后buff就出现各种问题,我当时懵逼了!事情是这样的,做过游戏的都知道,buff,需要分类型,且...

你还不懂java的日志系统吗(javaw 日志)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

Java 8之后的那些新特性(三):Java System Logger

去年12月份log4j日志框架的一个漏洞,给Java整个行业造成了非常大的影响。这个事情也顺带把log4j这个日志框架推到了争议的最前线。在Java领域,log4j可能相对比较流行。而在log4j之外...

Java开发中的日志管理:让程序“开口说话”

Java开发中的日志管理:让程序“开口说话”日志是程序员的朋友,也是程序的“嘴巴”。它能让程序在运行过程中“开口说话”,告诉我们它的状态、行为以及遇到的问题。在Java开发中,良好的日志管理不仅能帮助...

吊打面试官(十二)--Java语言中ArrayList类一文全掌握

导读...

OS X 效率启动器 Alfred 详解与使用技巧

问:为什么要在Mac上使用效率启动器类应用?答:在非特殊专业用户的环境下,(每天)用户一般可以在系统中进行上百次操作,可以是点击,也可以是拖拽,但这些只是过程,而我们的真正目的是想获得结果,也就是...

Java中 高级的异常处理(java中异常处理的两种方式)

介绍异常处理是软件开发的一个关键方面,尤其是在Java中,这种语言以其稳健性和平台独立性而闻名。正确的异常处理不仅可以防止应用程序崩溃,还有助于调试并向用户提供有意义的反馈。...

【性能调优】全方位教你定位慢SQL,方法介绍下!

1.使用数据库自带工具...

全面了解mysql锁机制(InnoDB)与问题排查

MySQL/InnoDB的加锁,一直是一个常见的话题。例如,数据库如果有高并发请求,如何保证数据完整性?产生死锁问题如何排查并解决?下面是不同锁等级的区别表级锁:开销小,加锁快;不会出现死锁;锁定粒度...

看懂这篇文章,你就懂了数据库死锁产生的场景和解决方法

一、什么是死锁加锁(Locking)是数据库在并发访问时保证数据一致性和完整性的主要机制。任何事务都需要获得相应对象上的锁才能访问数据,读取数据的事务通常只需要获得读锁(共享锁),修改数据的事务需要获...