这45个小技巧,让你的代码突然又优雅了

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 这45个小技巧,让你的代码突然又优雅了



不知道大家有没有经历过维护一个已经离职的人的代码的痛苦,一个方法写老长,还有很多的if else ,根本无法阅读,更不知道代码背后的含义,最重要的是没有人可以问,此时只能心里默默地问候这个留坑的兄弟。。

其实造成这些原因的很大一部分原因是由于代码规范的问题,如果写的规范,注释好,其实很多问题也就解决了。所以本文我就从代码的编写规范,格式的优化,设计原则和一些常见的代码优化的技巧等方面总结了了45个小技巧分享给大家,如果不足,欢迎指正。

1、规范命名

命名是写代码中最频繁的操作,比如类、属性、方法、参数等。好的名字应当能遵循以下几点:

见名知意

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

int i = 0;

名称 i 没有任何的实际意义,没有体现出数量的意思,所以我们应当指明数量的名称

int count = 0;
能够读的出来

如下代码:

private String sfzh;
private String dhhm;

这些变量的名称,根本读不出来,更别说实际意义了。

所以我们可以使用正确的可以读出来的英文来命名

private String idCardNo;
private String phone;

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

2、规范代码格式

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

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

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

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

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

3、写好代码注释

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

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

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

for (Integer id : ids) {
    if (id == 0) {
        continue;
    }
    //做其他事
}

为什么 id == 0 需要跳过,代码是无法看出来了,就需要注释了。

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

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

4、try catch 内部代码抽成一个方法

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

如下图是Eureka服务端源码中服务下线的实现中的一段代码

整个方法非常长,try中代码是真正的服务下线的代码实现,finally可以保证读锁最终一定可以释放。

所以这段代码其实就可以对核心的逻辑进行抽取。

protected boolean internalCancel(String appName, String id, boolean isReplication) {
    try {
        read.lock();
        doInternalCancel(appName, id, isReplication);
    } finally {
        read.unlock();
    }
    // 剩余代码
}
private boolean doInternalCancel(String appName, String id, boolean isReplication) {
    //真正处理下线的逻辑
}

5、方法别太长

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

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

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

6、抽取重复代码

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

所以一般遇到这种情况,可以抽取成一个工具类,还可以抽成一个公共的父类。

7、多用return

在有时我们平时写代码的情况可能会出现if条件套if的情况,当if条件过多的时候可能会出现如下情况:

if (条件1) {
    if (条件2) {
        if (条件3) {
            if (条件4) {
                if (条件5) {
                    System.out.println("三友的java日记");
                }
            }
        }
    }
}

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

if (!条件1) {
    return;
}
if (!条件2) {
    return;
}
if (!条件3) {
    return;
}
if (!条件4) {
    return;
}
if (!条件5) {
    return;
}
System.out.println("三友的java日记");

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

8、if条件表达式不要太复杂

比如在如下代码:

if (((StringUtils.isBlank(person.getName())
        || "三友的java日记".equals(person.getName()))
        && (person.getAge() != null && person.getAge() > 10))
        && "汉".equals(person.getNational())) {
    // 处理逻辑
}

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

boolean sanyouOrBlank = StringUtils.isBlank(person.getName()) || "三友的java日记".equals(person.getName());
boolean ageGreaterThanTen = person.getAge() != null && person.getAge() > 10;
boolean isHanNational = "汉".equals(person.getNational());
if (sanyouOrBlank
    && ageGreaterThanTen
    && isHanNational) {
    // 处理逻辑
}

此时就很容易看懂if的逻辑了

9、优雅地参数校验

当前端传递给后端参数的时候,通常需要对参数进场检验,一般可能会这么写

@PostMapping
public void addPerson(@RequestBody AddPersonRequest addPersonRequest) {
    if (StringUtils.isBlank(addPersonRequest.getName())) {
        throw new BizException("人员姓名不能为空");
    }
    if (StringUtils.isBlank(addPersonRequest.getIdCardNo())) {
        throw new BizException("身份证号不能为空");
    }
    // 处理新增逻辑
}

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

针对参数校验这个问题,有第三方库已经封装好了,比如hibernate-validator框架,只需要拿来用即可。

所以就在实体类上加@NotBlank、@NotNull注解来进行校验

@Data
@ToString
private class AddPersonRequest {
    @NotBlank(message = "人员姓名不能为空")
    private String name;
    @NotBlank(message = "身份证号不能为空")
    private String idCardNo;
    //忽略
}

此时Controller接口就需要方法上就需要加上@Valid注解

@PostMapping
public void addPerson(@RequestBody @Valid AddPersonRequest addPersonRequest) {
    // 处理新增逻辑
}

10、统一返回值

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

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

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

Spring中很多方法可以做到统一返回值,而不用每个方法都返回,比如基于AOP,或者可以自定义HandlerMethodReturnValueHandler来实现统一返回值。

11、统一异常处理

当你没有统一异常处理的时候,那么所有的接口避免不了try catch操作。

@GetMapping("/{id}")
public Result<T> selectPerson(@PathVariable("id") Long personId) {
    try {
        PersonVO vo = personService.selectById(personId);
        return Result.success(vo);
    } catch (Exception e) {
        //打印日志
        return Result.error("系统异常");
    }
}

每个接口都得这么玩,那不得满屏的try catch。

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

12、尽量不传递null值

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

当然为了更好的表明该方法是不是可以传null值,可以通过@NonNull和@Nullable注解来标记。@NonNull就表示不能传null值,@Nullable就是可以传null值。

//示例1
public void updatePerson(@Nullable Person person) {
    if (person == null) {
        return;
    }
    personService.updateById(person);
}
//示例2
public void updatePerson(@NonNull Person person) {
    personService.updateById(person);
}

13、尽量不返回null值

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

public Optional<Person> getPersonById(Long personId) {
    return Optional.ofNullable(personService.selectById(personId));
}

如果不想这么写,也可以通过@NonNull和@Nullable表示方法会不会返回null值。

14、日志打印规范

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

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

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

15、统一类库

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

16、尽量使用工具类

比如在对集合判空的时候,可以这么写

public void updatePersons(List<Person> persons) {
    if (persons != null && persons.size() > 0) {
    }
}

但是一般不推荐这么写,可以通过一些判断的工具类来写

public void updatePersons(List<Person> persons) {
    if (!CollectionUtils.isEmpty(persons)) {
    }
}

不仅集合,比如字符串的判断等等,就使用工具类,不要手动判断。

17、尽量不要重复造轮子

就拿格式化日期来来说,我们一般封装成一个工具类来调用,比如如下代码

private static final SimpleDateFormat DATE_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDateTime(Date date) {
    return DATE_TIME_FORMAT.format(date);
}

这段代码看似没啥问题,但是却忽略了SimpleDateFormat是个线程不安全的类,所以这就会引起坑。

一般对于这种已经有开源的项目并且已经做得很好的时候,比如Hutool,就可以把轮子直接拿过来用了。

18、类和方法单一职责

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

就拿Nacos来说,在Nacos1.x的版本中,有这么一个接口HttpAgent

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

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

19、尽量使用聚合/组合代替继承

继承的弊端:

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

所以一般推荐使用聚合/组合代替继承。

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

比如说,OrderService需要使用UserService,可以注入一个UserService而非通过继承UserService。

聚合和组合的区别就是,组合是当对象一创建的时候,就直接给属性赋值,而聚合的方式可以通过set方式来设置。

组合:

public class OrderService {
    private UserService userService = new UserService();
}

聚合:

public class OrderService {
    private UserService userService;
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
6月前
|
存储 API C语言
C/C++爱心代码
C/C++爱心代码
2451 2
|
7月前
|
前端开发 Java C++
一行代码就能完成的事情,为什么要写两行?
一行代码就能完成的事情,为什么要写两行?
|
存储 设计模式 安全
代码优雅的45个小技巧,你知道吗?
代码优雅的45个小技巧,你知道吗?
103 0
记一次代码评鉴
前言 近期公司组织了一次代码评鉴,在这边记录下学习到的一些规范吧
x11获得窗口名的代码
x11获得窗口名的代码
136 0
|
Web App开发 安全 编译器
如何保护你的代码 - Ollvm(一)
如何保护你的代码 - Ollvm(一)
如何保护你的代码 - Ollvm(一)
|
数据可视化 JavaScript 前端开发
你的代码长啥样?
你的代码长啥样?
170 0
你的代码长啥样?
王垠的40行代码,究竟diao在哪里
王垠是谁? 不用我说了吧!!! 别傻谈,亮码瞧! ;; A simple CPS transformer which does proper tail-call and does not;; duplicate contexts for if-expressions.
7990 0

热门文章

最新文章