如何优雅的消除系统重复代码

 

引言

很多同学在工作一段时间之后可能都有这样的困境,大家觉得自己总是在写业务代码,技术上感觉好像没有多大的长进,不知不觉就成为了CURD Boy或者Girl,自己想要去改变但是又不知道该从何处进行入手。有的同学会去学习如何做架构、有的同学可能会去学习各种新技术还有的同学甚至转产品经理来试图解除困境。但是我觉得找到跨出这种困境的途径反而还是要从我们每天写的代码入手。即便当前每天做着CRUD的事情,但是我们自己不能把自己定义为只会CURD的工具人。那么我们到底如何从代码层面入手改变困境呢?我们可以回过头看看自己以前写的代码,或者是当前正在实现的各种各样的需求,反问自己以下5个问题。

有没有使用设计模式优化代码结构?有没有利用一些高级特性来简化代码实现?有没有借助框架的能力来扩展应用能力?自己设计的业务模型够不够抽象?代码扩展性强不强,需求如果有变化模块代码能不能做到最小化修改?

通过这样的反问和思考,我们可以不断自我审视自己写的代码。通过在代码上的深耕细作,我们所负责的模块的质量就会比别人更高,出现Bug的概率就会更低,稳定性就会更高,那么未来负责更多业务模块的机会也就会更多,只有这样我们才能真正跨出困境,实现突破。因此本文主要从优化日常工作中经常遇到的重复代码入手,和大家探讨下如何通过一些技巧来消除平台中的重复代码,以消除系统中的重复代码为切入点,提升系统稳定性。

为什么要消除重复代码

在程序猿的日常工作中,不仅要跟随业务侧的发展不断开发新的需求,同时也需要维护老的已有平台。无论是开发新需求还是维护老系统,我们都会遇到同样一个问题,系统中总是充斥着很多重复的代码。可能是由于工期很赶没时间优化,也有可能是历史原因欠下的技术债。无论是什么原因,系统中大量的重复代码非常影响平台整体的可维护性。大神们的谆谆教导Don’t Repeat Yourself 言犹在耳。那么平台中的重复代码会带来怎样的稳定性风险呢?

系统维护成本高

如果项目中出现大量的重复代码,说明系统中这部分业务逻辑并没有进行很好的抽象,因此会导致后期的代码维护面临很多问题。无论是修改原有逻辑还是新增业务逻辑可能需要在不同的文件中进行修改,项目维护成本相当高。另外后期维护的同学看到同样的逻辑写了多遍,不明白这到底是代码的坏味道还是有什么特殊的业务考虑,这也在无形中增加了后期维护者的代码逻辑理解难度。

程序Bug概率高

大家都知道重复代码意味着业务逻辑相同或者相似,假如这些相同或者相似的代码出现了Bug,在修复的过程中就需要修改很多地方,导致一次上线变更的内容比较多,存在一定的风险,毕竟线上问题70%-80%都是由于新的变更引起的。另外如果重复的地方比较多,很有可能出现漏改的情况。因此重复的代码实际就是隐藏在工程中的老炸弹,可能一直相安无事,也可能不知道什么时候就会Bom一声给你惊喜,因此我们必须要进行重复代码消除。

如何优雅的消除重复代码

在消除重复代码之前,我们首先需要确定到底什么是重复代码,或者说重复代码的特征到底是什么。有的同学可能会说,这还不简单嘛,重复代码不就是那些一模一样的但是散落在工程不同地方的代码嘛。当然这句话也没错,但是不够全面,重复代码不仅仅指那些不同文件中的完全相同的代码,还有一些代码业务流程相似但是并不是完全相同,这类代码我们也把它称之为重复代码。重复代码的几个特性:

代码结构完全相同

比如工程中好几个地方都有读取配置文件的逻辑,代码都是相同的,那么我们可以把不同地方读取配置文件的逻辑放到一个工具类中,这样今后再有读取配置文件的需要的时候可以直接调用工具类中方法即可,不需要再重复写相同的代码,这也是我们日常工作中最常见的使用方式。

代码逻辑结构相似

在项目中经常遇到虽然代码并不是完全相同,但是逻辑结构却非常相似。比如电商平台在进行营销活动的时候,常常通过邀请的方式来进行用户红包领取的活动,但是对于新老用户的红包赠予规则是不同的,同时也会根据邀请用户的数量的不同给予不同的红包优惠。但是无论新老用户都会经历根据用户类型获取红包计算规则,根据规则计算减免的红包,最后付款的时候减去红包数额这样一个业务逻辑。虽然表面看上去代码并不相同,但是实际上逻辑基本是一样的,因此也属于重复代码。

下面就和大家分享几种比较实用的消除重复的代码的技巧,考虑到安全性,代码都进行了脱敏以及简化处理。

统一参数校验

当我们进行项目开发的时候,会编写一些类的实现方法,不可避免的会进行一些参数校验或者业务规则校验,因此会在实现方法中写一些判断参数是否有效或者返回结果是否有效的的的代码。

public OrderDTO queryOrderById(String id) {

if(StringUtils.isEmpty(id)) {

return null;

}

OrderDTO order = orderBizService.queryOrder(id);

if(Objects.isNull(Order)) {

return null;

}

...

}

public List queryUsersByType(List type) {

if(StringUtils.isEmpty(id)) {

return null;

}

...

}

这种参数校验的方式,很多人会喜欢使用@Valid这种注解来进行参数有效性的判断,但是我觉得还是不够方便,它只能进行一些参数的校验,并不能进行业务结果的有效性判断。那么对于这种校验类的代码如何才能消除重复if...else...判断代码呢?因此我一般会统一定义一个Assert断言来进行参数或者业务结果的校验,当然也可以使用Spring框架提供的Assert抽象类来进行判断,但是它抛出的异常是IllegalArgumentException,我习惯抛出自己定义的全局统一的异常信息,这样可以通过全局的异常处理类来进行统一处理。因此我们首先定义一个业务断言类,主要针对biz层出现的参数以及业务结果进行断言,这样可以避免重复写if...else...判断代码。

public class Assert {

public static void notEmpty(String param) {

if(StringUtils.isEmpty(param)) {

throw new BizException(ErrorCodes.PARAMETER_IS_NULL, "param is empty or null");

}

}

public static void notNull(Object o) {

if (Objects.isNull(o)) {

throw new BizException(ErrorCodes.PARAMETER_IS_NULL, "object is null");

}

}

public static void notEmpty(Collection collection) {

if(CollectionUtils.isEmpty(collection)) {

throw new BizException(ErrorCodes.PARAMETER_IS_NULL, "collection is empty or null");

}

}

}

我们看下优化后的代码是不是看上去清爽许多。

public OrderDTO queryOrderById(String id) {

Assert.notEmpty(id);

OrderDTO order = orderBizService.queryOrder(id);

Assert.notNull(order);

...

}

public List queryUsersByType(List type) {

Assert.notEmpty(type);

...

}

统一异常处理

以下这类Controller代码在项目中是不是很常见?大家可以翻翻自己的项目工程代码,可能很多工程中Cotroller层都充斥着这样的try{}catch{}逻辑处理,相当于每个接口实现都要进行异常处理,看起来非常冗余写起来也麻烦。实际上我们可以通过定义统一的全局异常处理器来进行优化,避免重复的进行异常捕获。

@GetMapping("list")

public ResponseResultgetOrderList(@RequestParam("id")String userId) {

try {

OrderVO orderVo = orderBizService.queryOrder(userId);

return ResponseResultBuilder.buildSuccessResponese(orderDTO);

} catch (BizException be) {

// 捕捉业务异常

return ResponseResultBuilder.buildErrorResponse(be.getCode, be.getMessage());

} catch (Exception e) {

// 捕捉兜底异常

return ResponseResultBuilder.buildErrorResponse(e.getMessage());

}

}

那么我们应该怎么优化这些重复的异常捕捉处理代码呢?首先我们需要定义一个统一的异常处理器,通过它来对Controller接口的异常进行统一的异常处理,包括异常捕获以及异常信息提示等等。这样就不用在每个实现接口中编写try{}catch{}异常处理逻辑了。示意代码只是简单的说明实现方法,在项目中进行落地的时候,大家可以定义处理更多的异常类型。

@ControllerAdvice

@ResponseBody

public class UnifiedException {

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)

@ExceptionHandler(BizException.class)

@ResponseBody

public ResponseResult handlerBizException(BizException bizexception) {

return ResponseResultBuilder.buildErrorResponseResult(bizexception.getCode(), bizexception.getMessage());

}

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)

@ExceptionHandler(Exception.class)

@ResponseBody

public ResponseResult handlerException(Exception ex) {

return ResponseResultBuilder.buildErrorResponseResult(ex.getMessage());

}

}

优化后的Controller如下所示,大量的try...catch...不见了,代码结构变得更加清晰直接。

@GetMapping("list")

public ResponseResultgetOrderList(@RequestParam("id")String userId) {

List orderVo = orderBizService.queryOrder(userId);

return ResponseResultBuilder.buildSuccessResponese(orderVo);

}

优雅的属性拷贝

在实际的项目开发中我们所开发的微服务都是分层的有的是MVC三层,有的按照DDD领域分层是四层。无论是三层还是四层都会涉及不同层级的之间的调用,而每个层级都有自己的数据对象模型,比如biz层是dto,domain层是model,repo层是po。因此必然会涉及到数据模型对象之间的相关转换。在一些场景下模型之间的字段很多都是一样的,有的甚至是完全一模一样。比如将DTO转化为业务模型Model,实际上他们之间很多的字段都是一样的,所以经常会出现以下的这种代码,会出现大量的属性赋值 的操作来达到模型转换的需求。实际上我们可以通过一些工具包或者工具类进行属性的拷贝,避免出现大量的重复赋值代码。

public class TaskConverter {

public static TaskDTO taskModel2DTO(TaskModel taskModel) {

TaskDTO taskDTO = new TaskDTO();

taskDTO.setId(taskModel.getId());

taskDTO.setName(taskModel.getName());

taskDTO.setType(taskModel.getType());

taskDTO.setContent(taskModel.getContent());

taskDTO.setStartTime(taskModel.getStartTime());

taskDTO.setEndTime(taskModel.getEndTime());

return taskDTO;

}

}

使用BeanUtils的进行属性赋值,很明显不再有那又长又没有感情的一条又一条的属性赋值语句了,整个任务数据模型对象的转换代码看上去立马舒服很多。

public class TaskConverter {

public static TaskDTO taskModel2DTO(TaskModel taskModel) {

TaskDTO taskDTO = new TaskDTO();

BeanUtils.copyProperties(taskModel, taskDTO);

return taskDTO;

}

}

当然很多人会说,BeanUtils会存在深拷贝的问题。但是在一些浅拷贝的场景下使用起来还是比较方便的。另外还有Mapstruct工具,大家也可以试用一下。

核心能力抽象

假设有这样的业务场景,系统中需要根据不同的用户类型计算商品结算金额,大致的计算逻辑有三个步骤,分别是计算用户商品总价格,计算不同用户对应的优惠金额,最后计算出用户的结算金额。我们先来看下原有系统中的实现方式。

普通用户结算逻辑:

public Class NormalUserSettlement {

//省略代码

...

public Bigdecimal calculate(String userId) {

//计算商品总价格

Listgoods = shoppingService.queryGoodsById(userId);

Bigdecimal total = goods.stream().map(item -

THE END
Copyright © 2024 亿华云