关于java:如何优雅的消除系统重复代码

10次阅读

共计 13078 个字符,预计需要花费 33 分钟才能阅读完成。

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

1、有没有应用设计模式优化代码构造?

2、有没有利用一些高级个性来简化代码实现?

3、有没有借助框架的能力来扩大利用能力?

4、本人设计的业务模型够不够形象?

5、代码扩展性强不强,需要如果有变动模块代码能不能做到最小化批改?

通过这样的反诘和思考,咱们能够一直自我扫视本人写的代码。通过在代码上的深耕细作,咱们所负责的模块的品质就会比他人更高,呈现 Bug 的概率就会更低,稳定性就会更高,那么将来负责更多业务模块的机会也就会更多,只有这样咱们能力真正跨出窘境,实现冲破。因而本文次要从优化日常工作中常常遇到的反复代码动手,和大家探讨下如何通过一些技巧来打消平台中的反复代码,以打消零碎中的反复代码为切入点,晋升零碎稳定性。

为什么要打消反复代码

在程序猿的日常工作中,不仅要追随业务侧的倒退一直开发新的需要,同时也须要保护老的已有平台。无论是开发新需要还是保护老零碎,咱们都会遇到同样一个问题,零碎中总是充斥着很多反复的代码。可能是因为工期很赶没工夫优化,也有可能是历史起因欠下的技术债。无论是什么起因,零碎中大量的反复代码十分影响平台整体的可维护性。大神们的谆谆教导 Don’t Repeat Yourself 言犹在耳。那么平台中的反复代码会带来怎么的稳定性危险呢?

系统维护老本高

如果我的项目中呈现大量的反复代码,阐明零碎中这部分业务逻辑并没有进行很好的形象,因而会导致前期的代码保护面临很多问题。无论是批改原有逻辑还是新增业务逻辑可能须要在不同的文件中进行批改,我的项目保护老本相当高。另外前期保护的同学看到同样的逻辑写了多遍,不明确这到底是代码的坏滋味还是有什么非凡的业务思考,这也在无形中减少了前期维护者的代码逻辑了解难度。

程序 Bug 概率高

大家都晓得反复代码意味着业务逻辑雷同或者类似,如果这些雷同或者类似的代码呈现了 Bug,在修复的过程中就须要批改很多中央,导致一次上线变更的内容比拟多,存在肯定的危险,毕竟线上问题 70%-80% 都是因为新的变更引起的。另外如果反复的中央比拟多,很有可能呈现漏改的状况。因而反复的代码理论就是暗藏在工程中的老炸弹,可能始终相安无事,也可能不晓得什么时候就会 Bom 一声给你惊喜,因而咱们必须要进行反复代码打消。

如何优雅的打消反复代码

在打消反复代码之前,咱们首先须要确定到底什么是反复代码,或者说反复代码的特色到底是什么。有的同学可能会说,这还不简略嘛,反复代码不就是那些截然不同的然而散落在工程不同中央的代码嘛。当然这句话也没错,然而不够全面,反复代码不仅仅指那些不同文件中的完全相同的代码,还有一些代码业务流程类似然而并不是完全相同,这类代码咱们也把它称之为反复代码。反复代码的几个个性:

1、代码构造完全相同

比方工程中好几个中央都有读取配置文件的逻辑,代码都是雷同的,那么咱们能够把不同中央读取配置文件的逻辑放到一个工具类中,这样今后再有读取配置文件的须要的时候能够间接调用工具类中办法即可,不须要再反复写雷同的代码,这也是咱们日常工作中最常见的应用形式。

2、代码逻辑构造类似

在我的项目中常常遇到尽管代码并不是完全相同,然而逻辑构造却十分类似。比方电商平台在进行营销流动的时候,经常通过邀请的形式来进行用户红包支付的流动,然而对于新老用户的红包赠予规定是不同的,同时也会依据邀请用户的数量的不同给予不同的红包优惠。然而无论新老用户都会经验依据用户类型获取红包计算规定,依据规定计算减免的红包,最初付款的时候减去红包数额这样一个业务逻辑。尽管外表看上去代码并不相同,然而实际上逻辑根本是一样的,因而也属于反复代码。

上面就和大家分享几种比拟实用的打消反复的代码的技巧,思考到安全性,代码都进行了脱敏以及简化解决。

对立参数校验

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

public OrderDTO queryOrderById(String id) {if(StringUtils.isEmpty(id)) {return null;}

OrderDTO order = orderBizService.queryOrder(id);
if(Objects.isNull(Order)) {return null;}
...
}

public List<UserDTO> queryUsersByType(List<String> 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<UserDTO> queryUsersByType(List<String> type) {Assert.notEmpty(type);

...
}

对立异样解决

以下这类 Controller 代码在我的项目中是不是很常见?大家能够翻翻本人的我的项目工程代码,可能很多工程中 Cotroller 层都充斥着这样的 try{}catch{} 逻辑解决,相当于每个接口实现都要进行异样解决,看起来十分冗余写起来也麻烦。实际上咱们能够通过定义对立的全局异样处理器来进行优化,防止反复的进行异样捕捉。

@GetMapping("list")
public ResponseResult<OrderVO> getOrderList(@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 ResponseResult<OrderVO> getOrderList(@RequestParam("id")String userId) {List<OrderVO> 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) {
// 计算商品总价格
List<Goods> goods = shoppingService.queryGoodsById(userId);

Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);

// 计算优惠
Bigdecimal discount = total.multiply(new Bigdecimal(0.1)); 

// 计算应酬金额
Bigdecimal payPrice = total - dicount;
return payPrice;
}
// 省略代码
...
}

VIP 用户结算逻辑:

public Class VIPUserSettlement {

// 省略代码
...

public Bigdecimal calculate(String userId) {
// 计算商品总价格
List<Goods> goods = shoppingService.queryGoodsById(userId);

Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);

// 计算优惠
Bigdecimal discount = total.multiply(new Bigdecimal(0.2)); 

// 计算应酬金额
Bigdecimal payPrice = total - dicount; 
return payPrice;
}
// 省略代码
...
}

黑卡用户结算逻辑:

public Class VIPUserSettlement {

// 省略代码
...

public Bigdecimal calculate(String userId) {
// 计算商品总价格
List<Goods> goods = shoppingService.queryGoodsById(userId);

Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);

// 计算优惠
Bigdecimal discount = total.multiply(new Bigdecimal(0.2)); 

// 计算应酬金额
Bigdecimal payPrice = total - dicount; 
return payPrice; 
} 
// 省略代码
...
}

在这样的场景下,咱们能够发现,在三个类中计算商品总额以及计算最初的应酬金额逻辑都是一样的,惟一不同的是每个用户类型对应的优惠金额是不同的。因而咱们能够把逻辑雷同的局部形象到 AbstractSettleMent 中,而后定义计算优惠金额的形象办法由各个不同的用类型子类去实现。这样各个子类只有关怀本人的优惠实现就能够了,反复的代码都被形象复用大大减少反复代码的应用。

public Class AbstractSettlement {

// 省略代码
...

public abstact Bigdecimal calculateDiscount();

public Bigdecimal calculate(String userId) {
// 计算商品总价格
List<Goods> goods = shoppingService.queryGoodsById(userId);

Bigdecimal total = goods.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getAmount()))).reduce(BigDecimal.ZERO, BigDecimal::add);

// 计算优惠
Bigdecimal discount = calculateDiscount(); 

// 计算应酬金额
Bigdecimal payPrice = total - dicount; 
return payPrice; 

}

// 省略代码
...
}

自定义注解和 AOP

用过 Spring 框架的同学都晓得,AOP 是 Spring 框架外围个性之一,它不仅是一种编程思维更是理论我的项目中能够落地的技术实现技巧。通过自定义注解和 AOP 的组合应用,能够实现一些通用能力的形象。比方很多接口都须要进行鉴权、日志记录或者执行工夫统计等操作,然而如果在每个接口中都编写鉴权或者日志记录的代码那就很容易产生很多反复代码,在我的项目前期不好保护。针对这种场景 咱们能够应用 AOP 同时联合自定义注解实现接口的切面编程,在须要进行通用逻辑解决的接口或者类中减少对应的注解即可。

假如有这样的业务场景,须要计算指定某些接口的耗时状况,个别的做法是在每个接口中都加上计算接口耗时的逻辑,这样各个接口中就会有这样反复计算耗时的逻辑,反复代码就这样产生了。那么通过自定义注解和 AOP 的形式能够轻松地解决代码反复的问题。首先定义一个注解,用于须要统计接口耗时的接口办法上。

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeCost {}

定义切面实现类:

@Aspect 
@Component
public class CostTimeAspect {@Pointcut(value = "@annotation(com.mufeng.eshop.anotation.CostTime)") 
public void costTime(){}

@Around("runTime()") 
public Object costTimeAround(ProceedingJoinPoint joinPoint) {
Object obj = null;
try {long beginTime = System.currentTimeMillis();
obj = joinPoint.proceed();
// 获取办法名称
String method = joinPoint.getSignature().getName();
// 获取类名称
String class = joinPoint.getSignature().getDeclaringTypeName();
// 计算耗时
long cost = System.currentTimeMillis() - beginTime;
log.info("类:[{}],办法:[{}] 接口耗时:[{}]", class, method, cost + "毫秒");
} catch (Throwable throwable) {throwable.printStackTrace();
}
return obj;
}
}

优化前的代码:

@GetMapping("/list")
public ResponseResult<List<OrderVO>> getOrderList(@RequestParam("id")String userId) {long beginTime = System.currentTimeMillis();
List<OrderVO> orderVo = orderBizService.queryOrder(userId); 
log.info("getOrderList 耗时:" + System.currentTimeMillis() - beginTime + "毫秒");
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}

@GetMapping("/item")
public ResponseResult<OrderVO> getOrderById(@RequestParam("id")String orderId) {long beginTime = System.currentTimeMillis();
OrderVO orderVo = orderBizService.queryOrderById(orderId);
log.info("getOrderById 耗时:" + System.currentTimeMillis() - beginTime + "毫秒");
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}

优化后的代码:

@GetMapping("/list")
@TimeCost
public ResponseResult<List<OrderVO>> getOrderList(@RequestParam("id")String userId) {List<OrderVO> orderVo = orderBizService.queryOrder(userId); 
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}

@GetMapping("/item")
@TimeCost
public ResponseResult<OrderVO> getOrderList(@RequestParam("id")String orderId) {OrderVO orderVo = orderBizService.queryOrderById(orderId); 
return ResponseResultBuilder.buildSuccessResponese(orderVo);
}

引入规定引擎

大家在做业务开发的时候,可能会遇到这样的场景,业务中充斥着各种各样的规定判断,同时这些业务规定还可能常常发生变化。即使是咱们用了策略模式等设计模式来优化代码构造,然而还是不能防止代码中呈现大量的 if…else… 判断代码,一旦减少或者批改规定都须要在原来的业务规定代码中进行批改,保护起来十分不不便。

假如设有这样的业务,销售人员的处分依据理论的利润进行计算,不同的利润计算处分的规定并不相同。应用规定引擎之前,可能会有这样的代码构造,须要依据理论利润所处的区间来计算最终的处分金额,不同区间范畴对应的返点规定是不一样的,因而会有很多的 if…else… 判断。另外规定有可能随着业务的倒退还会常常变动,因而前期可能面临一直批改这部分的计算处分的代码的状况。

public double calculate(int profit) {if(profit < 1000) {return profit * 0.1;} else if(1000 < profit && profit< 2000) {return profit * 0.15;} else if(2000 < profit && profit < 3000) {return profit * 0.2;} 
return profit * 0.3;
}

如果遇到这种业务场景,咱们就能够思考应用规定引擎。通过引入规定引擎,咱们能够实现业务代码与业务规定相拆散,将各种业务判断规定从原有的平台代码中抽离进去,当前规定的批改都在规定文件中间接批改就能够了,防止代码自身的变更,从而大大晋升代码的扩展性。这里简略介绍下罕用的规定引擎 Drools 是如何实现规定扩大治理的。

应用 Drools 之后:

应用规定引擎优化之后,所有的规定也就是所有的 if…else… 都会放在规定文件 reward.drl 中,因而代码中不会再有各种反复的 if…else… 代码,真正实现了业务规定与业务数据相拆散。

// 处分规定
package reward.rule
import com.mufeng.eshop.biz.Reward

// rule1:如果利润小于 1000,则处分计算规定为 profit*0.1
rule "reward_rule_1"
when
$reward: Reward(profit < 1000) 
then
$reward.setReward($reward.getProfit() * 0.1);
System.out.println("匹配规定 1,处分为利润的 1 成");
end

// rule2:如果利润大于 1000 小于 2000,则处分计算规定为 profit*0.15
rule "reward_rule_2"
when
$reward: Reward(profit >= 1000 && profit < 2000)
then
$reward.setReward($reward.getProfit() * 0.15);
System.out.println("匹配规定 2,处分为利润的 1.5 成");
end

// rule3:如果利润大于 2000 小于 3000,则处分计算规定为 profit*0.2
rule "reward_rule_3"
when
$order: Order(profit >= 2000 && profit < 3000)
then
$reward.setReward($reward.getProfit() * 0.2);
System.out.println("匹配规定 3,处分为利润的 2 成");
end

// rule4:如果利润大于等于 3000,则处分计算规定为 profit*0.3
rule "reward_rule_4"
when
$order: Order(profit >= 3000)
then
$reward.setReward($reward.getProfit() * 0.3);
System.out.println("匹配规定 4,处分为利润的 3 成");
end

在代码中只有将待判断的数据插入到规定引擎的工作内存中,而后执行规定就能够获取到最终的后果,是不是很不便的实现业务规定的解耦,在理论的 Java 代码中也不必看到各种 if…else… 判断。

定义规定引擎实现:

public class DroolsEngine {

private KieHelper kieHelper;

public DroolsEngine() {this.kieHelper = new KieHelper();
}

public void executeRule(String rule, Object unit, boolean clear) {kieHelper.addContent(rule, ResourceType.DRL);
KieSession kieSession = kieHelper.getKieContainer().newKieSession();
// 插入判断实体
kieSession.insert(unit);
// 执行规定
kieSession.fireAllRules();
if (clear) {kieSession.dispose();
}
}
}
public class Profit {public double calculateReward(Reward reward) {
String rule = "classpath:rules/reward.drl";
File rewardFile = new File(rule);
String rewardDrl = FileUtils.readFile(rewardFile, "utf-8");
DroolsEngine engine = new DroolsEngine();
engine.executeRule(rewardDrl, reward, true);
return reward.getReward();}
}

通过引入 Drools 规定引擎,代码中不再有各种规定判断的反复的 if…else… 判断语句,而且如果前期要批改处分规定,代码不必批改,间接更改规定即可,零碎的扩展性以及可维护性进一步晋升。

打消反复代码方法论

上文中给大家介绍了几种打消反复代码的实战小技巧,不晓得大家有没有发现尽管具体落地实操的伎俩各不相同,无论是提取专用逻辑作为工具类、应用 AOP 进行面向切面编程还是进行通用逻辑形象,又或者是借助规定引擎拆散实现与规定。理论它们的核心思想实质上都是统一的,都是通过抽离或者形象类似代码逻辑后进行对立解决。将这种核心思想放在微服务外部就是在零碎中的打消反复业务逻辑,如果放在架构层面来看其实和中台思维的实质也是相通的,将用户、领取这种各个平台都会用到的服务形象为中台,理论就是一种凌乱到有序的软件复杂度治理过程以及一种万物归一的思维。

那么在日常的理论我的项目中咱们应该怎么落地实际打消反复代码呢?这里总结了通过上述文章对于反复代码的解决,咱们来试图来提炼打消反复代码的方法论。

Find: 技术同学须要有一双能够发现反复代码的眼睛,可能将外表上的反复我代码以及暗藏的反复代码辨认进去。反复代码不仅仅是示意长得截然不同的代码,那些外围业务逻辑一样理论也是一种反复代码。

Analysis: 当咱们找到了反复代码之后,就要思考该如何进行优化了,如果只是工具类型的反复代码,那么间接提取作为一个工具类就能够了,也不必思考太多。然而如果是波及业务流程可能须要进一步的进行形象。

Action: 依据不同的反复代码的类型,咱们须要制订不通过的优化反复代码的计划。依据不同的计划实现通过引入规定引擎还是模板办法进行形象。

总结

明天和大家次要分享了几种我的项目中打消反复代码的实际计划,同时积淀了如何优雅打消代码反复的方法论,心愿通过这样的积淀以及总结能够在大家遇到同样的问题的时候能够有所帮忙,通过理论的优化代码落地来晋升平台的可维护性。

正文完
 0