当前位置: 首页 > 科技观察

阿里P8架构师教你干掉代码重复-大量ifelse

时间:2023-03-21 20:49:34 科技观察

本文将教你如何优雅地消除代码重复,改变你认为业务代码没有技术含量的观念。1CRUD工程师的“痛”很多CRUD工程师抱怨业务开发没有技术含量,设计模式和高并发都用不上,就是堆CRUD。每次面试被问到“说说常见的设计模式?”,我只能说单例到精通。即使听说过其他的设计模式,我也只会简单的讲一下,因为我从来没有真正用过。对于反射和注解,我只是知道在框架中用的比较多,但是框架我不会自己写,更不用说怎么用了。设计模式来源于世界级软件大师在大型项目中的经验。实践证明,反射、注解、泛型等有利于维护大型项目的高级特性在框架中得到了广泛的应用,因为框架往往需要使用同一套算法来处理不同的数据结构,而这些特性可以帮助减少重复代码,也有利于维护。提高项目的可维护性是每个码农都必须关注的。一个很重要的方法就是减少代码重复,因为重复太多会导致:修改一个地方忘记修改另一个地方,有些导致bug的代码并不是完全重复,而是相似度很高。修改这些相似的代码很容易纠正(cv)错误,把原来的区别改成相同的2Factory+模板方法模式,消除多个if和Duplicatecode2.1需要开发购物车下单,区别处理针对不同的用户:普通用户需要收取运费,为商品价格的10%,没有商品折扣的VIP用户也需要收取商品价格10%的运费,但购买两个为同一个商品以上,第三款开始享受一定优惠。内部用户可享受免费送货服务。没有产品折扣。实现三类购物车业务逻辑,将输入的Map对象(K:商品ID,V:商品数量)转化为Reference购物车类型Cart。2.2菜鸟变现购物车中的商品2.2.1普通用户2.2.2VIP用户VIP用户购买更多同类型商品可享受优惠。只是另外处理multibuy折扣部分。2.2.3内部用户免运费,无折扣,仅在处理产品折扣和运费时存在逻辑差异。三个购物车的代码有一半以上是重复的。虽然不同类型的用户计算运费和折扣的方式不同,但是整个购物车的初始化、总价统计、总运费、总折扣、支付价格的逻辑是相同的。代码重复本身并不可怕,可怕的是遗漏或改正错误。例如,一个学生为VIP用户编写购物车,发现在计算产品总价时存在错误。他们不应将所有商品的价格相加,而应将所有商品的价格*数量相加。他可能只修复了VIP用户购物车的代码,省略了普通用户和内部用户购物车重复逻辑实现的相同bug。如果是三个购物车,需要根据不同的用户类型使用不同的购物车。用多个if实现不同类型的用户调用不同的购物车流程:只能多加购物车类,重复写购物车逻辑,多写if逻辑?当然不是,同样的代码应该只出现在一个地方!2.3重构秘籍——模板方法模式可以在抽象类中定义重复的逻辑,三个购物车只需要实现不同部分的逻辑即可。这其实就是模板方法模式。在父类中实现购物车处理的流程模板,然后定义需要特殊处理的抽象方法,让子类实现。由于父类逻辑不能单独工作,需要定义为抽象类。如下代码所示,AbstractCart抽象类实现了购物车的通用逻辑,另外定义了两个抽象方法供子类实现。其中processCouponPrice方法用于计算产品折扣,processDeliveryPrice方法用于计算运费。有了抽象类,三个子类的实现就简单了。普通用户的购物车NormalUserCart实现0折扣10%运费。VIP用户的购物车VipUserCart直接继承NormalUserCart,只需要修改多买优惠政策即可。三个子类的实现图2.4重构秘笈的工厂模式——消除多个if由于三个购物车都叫XXXUserCart,用户类型字符串可以和UserCart拼接成购物车bean的名字,然后使用IoC容器来通过Bean的名称直接从AbstractCart中获取,调用其process方法即可实现通用。这是工厂模型,借助Spring容器实现:如果有新的用户类型和用户逻辑,只需要新增一个XXXUserCart类继承AbstractCart,实现特价和运费处理逻辑。工厂+模板方法模式消除了重复代码,避免修改已有代码。这就是设计模式中的开闭原则:对修改关闭,对扩展开放。3注解+反射消除重复代码3.1需求Bank提供了一些API接口,参数的序列化没有使用JSON,而是需要我们将参数依次拼在一起形成一个大字符串。按照银行提供的API文档的顺序,将所有参数组成定长数据,然后拼接成一个完整的字符串。由于每个参数都有固定的长度,所以不到长度时需要补:string类型的参数不满足长度部分需要在右边补下划线,即字符串内容left向左,number类型的参数长度补0,即实际数字向右。作为数字类型的单位也是左填充的。对所有参数做MD5运算作为签名(为了便于理解,Demo中没有加盐)。例如用户创建方法和支付方法定义如下:3.2菜鸟直接根据接口定义实现填写、签名、请求调用:publicclassBankService{//createuserpublicstaticStringcreateUser(Stringname,Stringidentity,Stringmobile,intage)throwsIOException{StringBuilderstringBuilder=newStringBuilder();//String向左,填充多余的空间_stringBuilder.append(String.format("%-10s",name).replace('','_'));stringBuilder.append(String.format("%-18s",identity).replace('','_'));//数字在右边,多余的地方补0stringBuilder.append(String.format("%05d",age));//字符串左stringBuilder.append(String.format("%-11s",mobile).replace('','_'));//MD5签名stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));returnRequest.Post("http://localhost:45678/reflection/bank/createUser").bodyString(stringBuilder.toString(),ContentType.APPLICATION_JSON).execute().returnContent().asString();}//向右支付publicstaticStringpay(longuserId,BigDecimalamount)throwsIOException{StringBuilderstringBuilder=newStringBuilder();//向右添加数字stringBuilder.append(String.format("%020d",userId));//金额向下取整为2位,以分为单位,为右边的数字,多余的地方补0stringBuilder.append(String.format("%010d",amount.setScale(2,RoundingMode.DOWN).multiply(newBigDecimal("100")).longValue()));//MD5签名stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));returnRequest.Post("http://localhost:45678/reflection/bank/pay").bodyString(stringBuilder.toString(),ContentType.APPLICATION_JSON).execute().returnContent().asString();}}这段代码的重复粒度更细:三种标准数据类型的处理逻辑包括重复处理流程中的字符串拼接、签名和发送请求的逻辑,以及实际的输入参数的参数类型和顺序method在所有的方法中都是重复的,不一定和接口要求一致,容易出错的代码级别是针对每个参数硬编码的,无法查清楚。如果有几十个或几百个参数,出错的概率就非常高。所有逻辑都使用一组代码实现,没有任何重复。要实现接口逻辑和逻辑实现的分离,首先必须用POJO类定义所有的接口参数。创建用户API参数@DatapublicclassCreateUserAPI{privateStringname;privateStringidentity;privateStringmobile;privateintage;}有了接口参数定义,可以通过自定义注解为接口添加一些元数据和所有参数。如下定义一个接口API注解BankAPI,包括接口URL地址和接口描述,然后定义一个自定义注解@BankAPIField来描述接口的各个字段规范,包括参数的顺序、类型、长度三个属性:定义CreateUserAPI类描述创建对于用户界面信息,可以通过在界面上添加@BankAPI注解来补充界面的URL、描述等元数据;参数的顺序、类型、长度等元数据可以通过在每个字段上添加@BankAPIField注解来补充:类似PayAPI类的两个类继承的AbstractAPI类是一个空实现,因为这种情况下的接口没有公开数据.通过这两个类,可以秒级完成API列表形式的校验。如果我们的核心翻译过程(也就是将注解和接口API序列化为请求需要的字符串的过程)没问题的话,只要注解和形式保持一致,API请求翻译是不会有问题的。API参数的描述是通过注解实现的。看反射如何配合注解实现动态接口参数组装:privatestaticStringremoteCall(AbstractAPIapi)throwsIOException{//从类中获取BankAPI注解,然后获取其URL属性,然后远程调用BankAPIbankAPI=api.getClass().getAnnotation(BankAPI.class);bankAPI.url();StringBuilderstringBuilder=newStringBuilder();//使用stream快速获取类中所有被BankAPIField注解的字段,按照order属性对字段进行排序,然后设置私有字段可以反射访问。Arrays.stream(api.getClass().getDeclaredFields())//获取所有字段//找到标注有annotations的字段.filter(field->field.isAnnotationPresent(BankAPIField.class))//按照注解中的顺序Fieldsorting.sorted(Comparator.comparingInt(a->a.getAnnotation(BankAPIField.class).order())).peek(field->field.setAccessible(true))//设置可以访问私有字段.forEach(field->{//实现反射获取注解的值,然后根据BankAPIField获取的参数类型按照三种标准进行格式化,所有参数的格式化逻辑都集中在这里//获取注解BankAPIFieldbankAPIField=field.getAnnotation(BankAPIField.class);Objectvalue="";try{//反射获取字段值value=field.get(api);}catch(IllegalAccessExceptione){e.printStackTrace();}//根据正确给字段类型填写方式formatstringswitch(bankAPIField.type()){case"S":{stringBuilder.append(String.format("%-"+bankAPIField.length()+"s",value.toString()).replace('','_'));break;}case"N":{stringBuilder.append(String.format("%"+bankAPIField.length()+"s",value.toString()).replace('','0'));break;}case"M":{if(!(valueinstanceofBigDecimal))thrownewRuntimeException(String.format("{}of{}必须是BigDecimal",api,field));stringBuilder.append(String.format("%0"+bankAPIField.length()+"d",((BigDecimal)值).setScale(2,RoundingMode.DOWN).multiply(newBigDecimal("100")).longValue()));break;}default:break;}});//实现参数添加和请求调用//签名逻辑stringBuilder.append(DigestUtils.md2Hex(stringBuilder.toString()));Stringparam=stringBuilder.toString();longbegin=System.currentTimeMillis();//发送请求Stringresult=Request.Post("http://localhost:45678/reflection"+bankAPI.url()).bodyString(param,ContentType.APPLICATION_JSON).execute().returnContent().asString();log.info("调用银行API{}url:{}参数:{}耗时:{}ms",bankAPI.desc(),bankAPI.url(),param,System.currentTimeMillis()-开始);returnresult;}所有处理参数排序、填充、签名、请求调用的核心逻辑都聚集在remoteCall中。有了这个方法,BankService中各个接口的实现就很简单了,只是参数的组装,然后调用remoteCall。涉及类结构的公共处理可以根据这种模式减少代码的重复。反射可以让我们在不知道类结构的情况下,按照固定的逻辑来处理类成员注解。它给了我们为这些成员补充元数据的能力,这样我们在使用反射实现通用逻辑的时候,就可以从外部获取到更多我们关心的数据。4属性拷贝对于一个三层架构的系统,由于层与层之间的解耦以及各层对数据的要求不同,每一层都会有自己的POJO实体。手动编写这些实体之间的分配代码很容易出错。对于复杂的业务系统,实体有几十个甚至上百个属性是很正常的。例如,ComplicatedOrderDTO描述了一个订单中的几十个属性。如果转换成类似的DO,复制大部分字段,然后将数据放入数据库,必然需要进行很多属性映射赋值。就像这样,加密麻麻的代码是不是已经让你头晕了?.setAddressId(orderDTO.getAddressId());orderDO.setCancelable(orderDTO.isCancelable());orderDO.setCommentable(orderDTO.isComplainable());//属性错误orderDO.setComplainable(orderDTO.isCommentable());//属性错误orderDO.setCancelable(orderDTO.isCancelable());orderDO.setCouponAmount(orderDTO.getCouponAmount());orderDO.setCouponId(orderDTO.getCouponId());orderDO.setCreateDate(orderDTO.getCreateDate());orderDO.setDirectCancelable(orderDTO.isDirectCancelable());orderDO.setDeliverDate(orderDTO.getDeliverDate());orderDO.setDeliverGroup(orderDTO.getDeliverGroup());orderDO.setDeliverGroupOrderStatus(orderDTO.getDeliverGroupOrderStatus());orderDO.setDeliverMethod(orderDTO.getDeliverMethod());orderDO.setDeliverPrice(orderDTO.getDeliverPrice());orderDO.setDeliveryManId(orderDTO.getDeliveryManId());orderDO.setDeliveryManMobile(orderDO.getDeliveryManMobile());//对图错误orderDO.setDeliveryManName(orderDTO.getDeliveryManName());orderDO.setDistance(orderDTO.getDistance());orderDO.setExpectDate(orderDTO.getExpectDate());orderDO.setFirstDeal(orderDTO.isFirstDeal());orderDO.setHasPaid(orderDTO.isHasPaid());orderDO.setHeadPic(orderDTO.getHeadPic());orderDO.setLongitude(orderDTO.getLongitude());orderDO.setLatitude(orderDTO.getLongitude());//属性赋值错误;orderDO.setMerchantAddress(orderDTO.getMerchantAddress());orderDO.setMerchantName(orderDTO.getMerchantName());orderDO.setMerchantPhone(orderDTO.getMerchantPhone());orderDO.setOrderNo(orderDTO.getOrderNo());orderDO.setOutDate(orderDTO.getOutDate());orderDO.setPayable(orderDTO.isPayable());orderDO.setPaymentAmount(orderDTO.getPaymentAmount());orderDO.setPaymentDate(orderDTO.getPaymentDate());orderDO.setPaymentMethod(orderDTO.getPaymentMethod());orderDO.setPaymentTimeLimit(orderDTO.getPaymentTimeLimit());orderDO.setPhone(orderDTO.getPhone());orderDO.setRefundable(orderDTO.isRefundable());orderDO.setRemark(orderDTO.getRemark());orderDO.setStatus(orderDTO.getStatus());orderDO.setTotalQuantity(orderDTO.getTotalQuantity());orderDO.setUpdateTime(orderDTO.getUpdateTime());orderDO.setName(orderDTO.getName());orderDO.setUid(orderDTO.getUid());如果原来的DTO有100个字段,我们需要复制90个字段给DO,保留10个不赋值,最后怎么验证正确性呢?你数数吗?即使有90行代码也不一定正确,因为属性可能会被重复赋值。有时字段名称相似,例如可抱怨的和可评论的。很容易反对两个目标字段的重复赋值。同源字段显然需要将DTO的值赋值给DO。但是在设置的时候,取值是取自DO本身,导致赋值无效。使用像BeanUtils这样的Mapping工具来做Bean的转换,copyProperties方法也可以让我们提供需要忽略的属性:5总结重复代码总有一天会出现如果有多个并行类实现相似的代码逻辑,可以考虑提取相同的逻辑并在父类中实现。差异逻辑通过抽象方法留给子类。使用相似的模板方法,将相同的流程和逻辑固定到一个模板中,在保留差异的同时尽可能避免代码重复。同时可以利用Spring的IoC特性注入相应的子类,避免在实例化子类时出现大量的if...else代码。使用硬编码方法重复实现相同的数据处理算法考虑将规则转化为自定义注解作为元数据来描述类或字段和方法,然后通过反射动态读取这些元数据、字段或调用方法,实现规则参数和规则的分离定义。也就是把变化的部分,也就是规则的参数放到注解中,规则的定义统一处理。业务代码中常见的DO、DTO、VO转换时手工赋值大量字段。当遇到具有数百个属性的复杂类型时,是非常非常容易出错的。不要手动分配值。考虑使用Bean映射工具。此外,您还可以考虑使用单元测试来验证所有字段分配的正确性。代码重复是评价项目质量的重要指标。如果一个项目几乎没有重复的代码,那么它的内部抽象一定非常好。重构时,首要任务是消除重复。参考《重构》三招摆脱代码重复https://blog.csdn.net/qq_32447301/article/details/107774036