很多同砚在工作一段时间之后大概都有如许的逆境,各人以为本身总是在写业务代码,技能上感觉似乎没有多大的上进,不知不觉就成为了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@ResponseBodypublic class UnifiedException {@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)@ExceptionHandler(BizException.class)@ResponseBodypublic ResponseResult handlerBizException(BizException bizexception) {return ResponseResultBuilder.buildErrorResponseResult(bizexception.getCode(), bizexception.getMessage());}@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)@ExceptionHandler(Exception.class)@ResponseBodypublic 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 @Componentpublic 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")@TimeCostpublic ResponseResult<List<OrderVO>> getOrderList(@RequestParam("id")String userId) {List<OrderVO> orderVo = orderBizService.queryOrder(userId); return ResponseResultBuilder.buildSuccessResponese(orderVo);}@GetMapping("/item")@TimeCostpublic 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.ruleimport com.mufeng.eshop.biz.Reward// rule1:如果利润小于1000,则夸奖盘算规则为profit*0.1rule "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.15rule "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.2rule "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.3rule "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:根据差别的重复代码的范例,我们须要订定不通过的优化重复代码的方案。根据差别的方案实现通过引入规则引擎照旧模板方法举行抽象。
总结
今天和各人紧张分享了几种项目中消除重复代码的实践方案,同时沉淀了怎样优雅消除代码重复的方法论,渴望通过如许的沉淀以及总结可以在各人遇到同样的题目的时间可以有所资助,通过现实的优化代码落地来提拔平台的可维护性。 |