如何写出漂亮代码

从代码的编写规范,格式的优化,设计原则和一些常见的代码优化的技巧等方面总结了45个小技巧:

1、规范命名
命名是写代码中最频繁的操作,比如类、属性、方法、参数等。好的名字应当能遵循以下几点:见名知意且可读性强

比如需要定义一个变量需要来计数

var int i = 0

名称 i 没有任何的实际意义,没有体现出数量的意思,根本读不出来,更别说实际意义了,所以应当指明数量的名称

var int count = 0

2、规范代码格式
好的代码格式能够让人感觉看起来代码更加舒适。

好的代码格式应当遵守以下几点:

合适的空格
代码对齐,比如大括号要对齐
及时换行,一行不要写太多代码

好在现在开发工具支持一键格式化,可以帮助美化代码格式。

3、写好代码注释
在《代码整洁之道》这本书中作者提到了一个观点,注释的恰当用法是用来弥补我们在用代码表达意图时的失败。换句话说,当无法通过读代码来了解代码所表达的意思的时候,就需要用注释来说明。

作者之所以这么说,是因为作者觉得随着时间的推移,代码可能会变动,如果不及时更新注释,那么注释就容易产生误导,偏离代码的实际意义。而不及时更新注释的原因是,程序员不喜欢写注释。

但是这不意味着可以不写注释,当通过代码如果无法表达意思的时候,就需要注释,比如如下代码

复制代码
for x := range ch {
    fmt.Println(x)
    if x == 99 {
        break
    }
}
复制代码

为什么 x == 99 需要结束循坏,代码是无法看出来了,就需要注释了。

好的注释应当满足一下几点:

解释代码的意图,说明为什么这么写,用来做什么
对参数和返回值注释,入参代表什么,出参代表什么
有警示作用,比如说入参不能为空,或者代码是不是有坑
当代码还未完成时可以使用 todo 注释来注释

4、try catch 内部代码抽成一个方法
try catch代码有时会干扰我们阅读核心的代码逻辑,这时就可以把try catch内部主逻辑抽离成一个单独的方法

5、写的方法别太长
一旦代码太长,给人的第一眼感觉就很复杂,让人不想读下去;同时方法太长的代码可能读起来容易让人摸不着头脑,不知道哪一些代码是同一个业务的功能。

曾经就遇到过一个方法写了2000+行,各种if else判断,光理清代码思路就用了很久,最终理清之后,就用策略模式给重构了。

所以一旦方法过长,可以尝试将相同业务功能的代码单独抽取一个方法,最后在主方法中调用即可。

6、抽取重复代码
当一份代码重复出现在程序的多处地方,就会造成程序又臭又长,当这份代码的结构要修改时,每一处出现这份代码的地方都得修改,导致程序的扩展性很差。

所以一般遇到这种情况,可以抽取成一个工具类,方便在不同地方调用。

7、多用return
有时写代码的情况可能会出现if条件套if的情况,当if条件过多的时候可能会出现如下情况:

复制代码
if (条件1) {
    if (条件2) {
        if (条件3) {
            if (条件4) {
                if (条件5) {
                    fmt.Println("test")
                }
            }
        }
    }
}
复制代码

面对这种情况,可以换种思路,使用return来优化

复制代码
if (!条件1) {
    return
}
if (!条件2) {
    return
}
if (!条件3) {
    return
}
if (!条件4) {
    return
}
if (!条件5) {
    return
}
fmt.Println("test")
复制代码

这样优化就感觉看起来更加直观

8、if条件表达式不要太复杂
比如在以下代码:

复制代码
if (((StringUtils.isBlank(person.getName())
|| "编程之美~".equals(person.getName()))
&& (person.getAge() != null && person.getAge() > 10))
&& "".equals(person.getNational())) {
// 处理逻辑
}
复制代码

这段逻辑,这种条件表达式乍一看不知道是什么,仔细一看还是不知道是什么,这时就可以这么优化

复制代码
var sanyouOrBlank bool = StringUtils.isBlank(person.getName()) || "编程之美~".equals(person.getName())
var ageGreaterThanTen bool = person.getAge() != null && person.getAge() > 10
var isHanNational bool = "".equals(person.getNational())

if (sanyouOrBlank
&& ageGreaterThanTen
&& isHanNational) {
// 处理逻辑
}
复制代码

把判断的条件提取出来,此时就很容易看懂if的逻辑了

9、优雅地参数校验
当前端传递给后端参数的时候,通常需要对参数进场检验,参数少的话可能每个单独做校验

这种写虽然可以,但是当字段的多的时候,光校验就占据了很长的代码,不够优雅。

针对参数校验这个问题,可以自己封装或者找第三方库

10、统一返回值
后端在设计接口的时候,需要统一返回值

{
"code":0,
"message":"成功",
"data":"返回数据"
}

不仅是给前端参数,也包括提供给第三方的接口等,这样接口调用方法可以按照固定的格式解析代码,不用进行判断。如果不一样,相信我,前端半夜都一定会来找你。

很多方法可以做到统一返回值,而不用每个方法都单独返回,或者可以自定义来实现统一返回值。

11、统一异常处理
当没有统一异常处理的时候,那么所有的接口避免不了recover()操作。

每个接口都得这么玩,那不得满屏的recover()。

所以可以基于框架提供的统一异常处理机制来完成。

12、尽量不传递null值
这个很好理解,不传null值可以避免方法不支持为null入参时产生的空指针问题。 

13、尽量不返回null值
尽量不返回null值是为了减少调用者对返回值的为null判断,如果无法避免返回null值,可以通过返回Optional来代替null值。

14、日志打印规范
好的日志打印能帮助我们快速定位问题

好的日志应该遵循以下几点:

可搜索性,要有明确的关键字信息
异常日志需要打印出堆栈信息
合适的日志级别,比如异常使用error,正常使用info
日志内容太大不打印,比如有时需要将图片转成Base64,那么这个Base64就可以不用打印

15、统一类库
在一个项目中,可能会由于引入的依赖不同导致引入了很多相似功能的类库,比如常见的json类库,又或者是一些常用的工具类,当遇到这种情况下,应当规范在项目中到底应该使用什么类库,而不是一会用Fastjson,一会使用Gson。

16、尽量使用工具类
比如字符串的判断等,就使用工具类,不要手动判断。

17、尽量不要重复造轮子
就拿格式化日期来说,一般封装成一个工具类来调用

18、类和方法单一职责
单一职责原则是设计模式的七大设计原则之一,它的核心意思就是字面的意思,一个类或者一个方法只做单一的功能。

这个类只干了一件事,那就是封装http请求参数,向第三方服务发送请求,接收响应,这其实就是单一职责原则的体现。

当其它的地方需要向第三方服务发送请求时,只需要通过这个接口的实现,传入参数就可以发送请求了,而不需要关心如何携带服务端鉴权参数、http请求参数如何组装等问题。

19、尽量使用聚合/组合代替继承
继承的弊端:

灵活性低。java语言是单继承的,无法同时继承很多类,并且继承容易导致代码层次太深,不易于维护
耦合性高。一旦父类的代码修改,可能会影响到子类的行为
所以一般推荐使用聚合/组合代替继承。

聚合/组合的意思就是通过成员变量的方式来使用类。

20、使用设计模式优化代码
在平时开发中,使用设计模式可以增加代码的扩展性。

比如说,当需要做一个可以根据不同的平台做不同消息推送的功能时,就可以使用策略模式的方式来优化。

设计一个接口,短信通知实现:

//调用短信通知的api发送短信
app通知实现

//调用通知app通知的api
最后提供一个方法,当需要进行消息通知时,调用notifyMessage,传入相应的参数就行。

假设此时需要支持通过邮件通知,只需要有对应实现就行。

21、不滥用设计模式
用好设计模式可以增加代码的扩展性,但滥用设计模式是不可取的。

22、面向接口编程
在一些可替换的场景中,应该引用父类或者抽象,而非实现。

举个例子,在实际项目中可能需要对一些图片进行存储,但是存储的方式很多,比如可以选择阿里云的OSS,又或者是七牛云,存储服务器等等。所以对于存储图片这个功能来说,这些具体的实现是可以相互替换的。

所以在项目中,不应当在代码中耦合一个具体的实现,而是可以提供一个存储接口

如果选择了阿里云OSS作为存储服务器,那么就可以基于OSS实现一个FileStorage,在项目中哪里需要存储的时候,只要实现注入这个接口就可以了。

假设用了一段时间之后,发现阿里云的OSS比较贵,此时想换成七牛云的,那么此时只需要基于七牛云的接口实现FileStorage接口,然后注入到IOC,那么原有代码用到FileStorage根本不需要动,实现轻松的替换。

23、经常重构旧的代码
随着时间的推移,业务的增长,有的代码可能不再适用,或者有了更好的设计方式,那么可以及时的重构业务代码。

就拿上面的消息通知为例,在业务刚开始的时候可能只支持短信通知,于是在代码中就直接耦合了短信通知的代码。但是随着业务的增长,逐渐需要支持app、邮件之类的通知,那么此时就可以重构以前的代码,抽出一个策略接口,进行代码优化。

24、null值判断
空指针是代码开发中的一个难题,作为程序员的基本修改,应该要防止空指针。

可能产生空指针的原因:

数据返回对象为null
自动拆箱导致空指针
rpc调用返回的对象可能为空格

所以在需要这些的时候,需要强制判断是否为null。

25、pojo类重写toString方法
pojo一般内部都有很多属性,重写toString方法可以方便在打印或者测试的时候查看内部的属性。

26、枚举某些值可以用const +iota来表示,尽量避免魔法数字

27、资源释放写到defer

28、使用线程池代替手动创建线程
使用线程池还有以下好处:

降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控。

所以为了达到更好的利用资源,提高响应速度,就可以使用线程池的方式来代替手动创建线程。

29、线程设置名称
在日志打印的时候,日志是可以把线程的名字给打印出来。

所以,设置线程的名称可以帮助更好地知道代码是通过哪个线程执行的,更容易排查问题。

30、涉及线程间可见性加volatile

31、考虑线程安全问题
在平时开发中,有时需要考虑并发安全的问题。

举个例子来说,一般在调用第三方接口的时候,可能会有一个鉴权的机制,一般会携带一个请求头token参数过去,而token也是调用第三方接口返回的,一般这种token都会有个过期时间,比如24小时。

一般会将token缓存到Redis中,设置一个过期时间。向第三方发送请求时,会直接从缓存中查找,但是当从Redis中获取不到token的时候,都会重新请求token接口,获取token,然后再设置到缓存中。

整个过程看起来是没什么问题,但是实则隐藏线程安全问题。

假设当出现并发的时候,同时来两个线程AB从缓存查找,发现没有,那么AB此时就会同时调用token获取接口。假设A先获取到token,B后获取到token,但是由于CPU调度问题,线程B虽然后获取到token,但是先往Redis存数据,而线程A后存,覆盖了B请求的token。

这下就会出现大问题,最新的token被覆盖了,那么之后一定时间内token都是无效的,接口就请求不通。

针对这种问题,可以使用double check机制来优化获取token的问题。

所以,在实际中,需要多考虑业务是否有线程安全问题,有读写安全问题,那么就用线程安全的数据结构,业务有安全的问题,那就可以通过加锁的手段来解决。

32、慎用异步
虽然在使用多线程可以帮助提高接口的响应速度,但是也会带来很多问题。

事务问题
一旦使用了异步,就会导致两个线程不是同一个事务的,导致异常之后无法正常回滚数据。

cpu负载过高
之前有个小伙伴遇到需要同时处理几万调数据的需求,每条数据都需要调用很多次接口,为了达到老板期望的时间要求,使用了多线程跑,开了很多线程,此时会发现系统的cpu会飙升

意想不到的异常
还是上面提到的例子,在测试的时候就发现,由于并发量激增,在请求第三方接口的时候,返回了很多错误信息,导致有的数据没有处理成功。

虽然说慎用异步,但不代表不用,如果可以保证事务的问题,或是CPU负载不会高的话,那么还是可以使用的。

33、减小锁的范围
减小锁的范围就是给需要加锁的代码加锁,不需要加锁的代码不要加锁。这样就能减少加锁的时间,从而可以较少锁互斥的时间,提高效率。

34、有类型区分时定义好枚举
比如在项目中不同的类型的业务可能需要上传各种各样的附件,此时就可以定义好不同的一个附件的枚举,来区分不同业务的附件。

不要在代码中直接写死,不定义枚举,代码阅读起来非常困难,直接看到魔法数字都是懵的。

35、远程接口调用设置超时时间
比如在进行微服务之间进行rpc调用的时候,又或者在调用第三方提供的接口的时候,需要设置超时时间,防止因为各种原因,导致线程”卡死“在某个地方。

以前就遇到过线上就遇到过这种问题。当时的业务是订阅kafka的消息,然后向第三方上传数据。在某个周末,突然就接到电话,说数据无法上传了,通过排查线上的服务器才发现所有的线程都线程”卡死“了,最后定位到代码才发现原来是没有设置超时时间。

36、切片使用应当指明初始化大小
比如在写代码的时候,经常会用到slice、map来临时存储数据,但是用不好可能也会导致性能的问题。

切片在扩容的过程中,由于涉及到数组的拷贝,就会导致性能消耗;同时也会由于扩容的问题,消耗性能。所以可以在构造的时候指定切片的容量大小。

37、尽量不要使用BeanUtils来拷贝属性

38、如何高效地拼接字符串

性能比较:strings.Join ≈ strings.Builder  >  bytes.Buffer >  "+"  >  fmt.Sprintf

39、@Transactional应指定回滚的异常类型
平时在写代码的时候需要通过rollbackFor显示指定需要对什么异常回滚,原因在这:

默认是只能回滚RuntimeException和Error异常,所以需要手动指定,比如指定成Expection等。

40、谨慎方法内部调用动态代理的方法

41、需要什么字段就select什么字段
查询全字段有以下几点坏处:

1)增加不必要的字段的网络传输
比如有些文本的字段,存储的数据非常长,但是本次业务使用不到,但是如果查了就会把这个数据返回给客户端,增加了网络传输的负担

2)会导致无法使用到覆盖索引
比如说,现在有身份证号和姓名做了联合索引,现在只需要根据身份证号查询姓名,如果直接select name 的话,那么在遍历索引的时候,发现要查询的字段在索引中已经存在,那么此时就会直接从索引中将name字段的数据查出来,返回,而不会继续去查找聚簇索引,减少回表的操作。

所以建议是需要使用什么字段就查询什么字段。

42、不循环调用数据库
不要在循环中访问数据库,这样会严重影响数据库性能。

43、用业务代码代替多表join
原本也可以将两张表根据人员的id进行关联查询。但是不推荐这么,阿里也禁止多表join的操作

而之所以会禁用,是因为join的效率比较低。

MySQL是使用了嵌套循环的方式来实现关联查询的,也就是for循环会套for循环的意思。用第一张表做外循环,第二张表做内循环,外循环的每一条记录跟内循环中的记录作比较,符合条件的就输出,这种效率肯定低。

44、装上阿里代码检查插件
平时写代码由于各种因为,比如领导、项目经理会一直催进度,导致写代码都来不及思考,怎么快怎么来,cv大法上线,虽然有心想写好代码,但是手确不听使唤。所以建议装一个阿里的代码规范插件,如果有代码不规范,会有提醒,这样就可以知道哪些是可以优化的了。

45、及时跟同事沟通
写代码的时候不能闭门造车,及时跟同事沟通,比如刚进入一个新的项目,对项目工程不熟悉,一些技术方案不了解,如果上来就直接写代码,很有可能就会踩坑。

posted @   李若盛开  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
点击右上角即可分享
微信分享提示