软件工程师和码农最大的区别就是写代码的习惯。码农喜欢写重复的代码,而软件工程师会用各种技术杀死重复的冗余代码。

其实我觉得不是。设计模式和OOP是前人在大型项目中积累的经验。这些方法可以提高大型项目的可维护性。框架之所以广泛使用反射、注释、泛型等高级特性,是因为框架往往需要同一套算法来处理不同的数据结构,而这些特性有助于减少重复代码,提高项目可维护性。
在我看来,可维护性是大型项目成熟度的一个重要指标,而提高可维护性的一个非常重要的手段就是减少代码重复。那你为什么这么说?
如果多个重复代码实现完全相同的功能,很容易修改一个地方而忘记修改另一个地方,从而产生Bug。
有些代码不完全重复,但相似度很高。修改这些相似的代码很容易纠错,把原来的不同改成相同的。
今天我就从业务代码中最常见的三个需求入手,谈谈如何利用Java中的一些高级特性、设计模式和一些工具来消除重复代码,让它变得优雅高端。通过今天的学习,也希望改变你认为业务代码没有技术含量的看法。1.使用工厂模式+模板方法模式来消除if…else和重复代码。
假设您想开发一个购物车订购功能,并针对不同的用户进行不同的处理:
普通用户需要收取运费,运费为商品价格的10%,没有商品折扣;
VIP用户还需要收取商品价格10%的快递费,但购买两件以上相同商品,第三件开始享受一定优惠;
内部用户可免运费,无商品折扣。
我们的目标是实现三种类型购物车的业务逻辑,并将参与的地图对象转换为参与的购物车类型。首先,实现普通用户的购物车处理逻辑:
//购物车@Datapublic类Cart {//商品列表private List items = new ArrayList//Total discount private BigDecimal Total discount;//商品总价私有Bigdecimal总价;//总运费私有BigDecimal TotaldeliveryPrice//应付总价私有BigDecimal payPrice}//购物车中的商品@Datapublic类Item {//商品ID private long id//商品数量private int数量;//商品单价私有BigDecimal价格;//商品报价私有BigDecimal couponPrice//商品运费私有BigDecimal deliveryPrice}//普通用户购物车处理公共类normal user cart { public cart process { cart cart = new cart;//将Map的购物车转换成一个物品列表Item list = new items . entry set . stream . foreach;item . setid);item . set price));item . set quantity);itemList.add});cart . set items;//处理运费和商品折扣item list . stream . foreach . multiply))。乘法));//无折扣项目. item.setCouponPrice});//计算商品总价cart . settotalitemprice . stream . map . multiple))。减少);//计算运费总价cart . settotaldeliveryprice . stream . map . reduce);//计算总折扣cart . settotaldiscout . stream . map . reduce);//应付总价=商品总价+运费总价-总折扣cart.setPayPrice.add)。减去));返回购物车;}}
然后实现VIP用户的购物车逻辑。不同于普通用户的购物车逻辑,VIP用户可以享受到同类产品更多的优惠。所以,这部分代码只需要处理额外购买折扣部分:
public class VIP user Cart { public Cart process));//购买两件以上相同商品,第三件> 2)享受一定优惠{item.setcouponprice.multiply)。pide))。乘-2)))));} else { item.setCouponPrice} });...返回购物车;}}
最后是免运费不打折的内部用户,也只是处理商品折扣和运费时的逻辑差异:
public class internal user Cart { public Cart process;//不打折});...返回购物车;}}
对比代码量可以发现,三个购物车的代码有70%是重复的。原因很简单。虽然不同类型的用户计算运费和折扣的方式不同,但是整个购物车的初始化、总价统计、总运费、总折扣、支付价格的逻辑是一样的。
正如我们在开头提到的,代码重复本身并不可怕。可怕的是遗漏或者改正错误。比如给VIP用户写购物车的同学,发现商品总价计算有Bug。不是把所有项目的价格加在一起,而是把所有项目的价格*数量加在一起。请与Java项目分享缺失的项目。
此时,他可能只修改VIP用户购物车的代码,而忽略普通用户和内部用户的购物车。重复的逻辑实现也有同样的Bug。
三个购物车,我们需要根据不同类型的用户使用不同的购物车。如下面的代码所示,使用三个if来实现不同类型用户调用不同购物车的处理方法:
@ GetMappingPublic Cart错误Intuserid){//根据用户id获取用户类型字符串user category = db . Get user category;//普通用户处理逻辑if){ normal user cart normal user cart = new normal user cart;返回normal user cart . process;} //VIP用户处理逻辑if“VIP”)){ VIP user cart VIP user cart = new VIP user cart;返回VIP user cart . process;}//内部用户处理逻辑if“internal”)){ internal user card internal user card = new internal user card;返回internal user cart . process;}返回null}
电商的营销方式多种多样,未来用户类型会更多,购物车也会更多。我们能继续添加更多的购物车类,一遍又一遍地编写重复的购物车逻辑和更多的if逻辑吗?
不,当然,相同的代码应该只出现在一个地方!
如果我们背熟了抽象类和抽象方法的定义,那么我们可能会想,是不是可以在抽象类中定义重复的逻辑,三个购物车只需要实现不同的逻辑?
实际上,这个模式就是模板方法模式。我们在父类中实现了购物车处理的流程模板,然后在需要特殊处理的地方留下空 white,也就是留下了抽象的方法定义,让子类实现逻辑。因为父类的逻辑不完整,不能单独工作,所以需要定义为抽象类。
如下面的代码所示,AbstractCart抽象类实现了购物车的一般逻辑,并定义了两个额外的抽象方法供子类实现。其中,processCouponPrice方法用于计算商品折扣,processDeliveryPrice方法用于计算运费。
公共抽象类抽象cart {//大量处理购物车的重复逻辑在父类中实现公共Cart过程;新项目项目=新项目;});//让子类处理每个商品的折扣处理couponprice;processDeliveryPrice});//计算商品总价//计算总运费cart . settotaldeliveryprice . stream . map . reduce);//计算总折扣cart . settotal discount . stream . map . reduce);//计算应付价格cart.setPayPrice.add)。减去));返回购物车;}//处理商品优惠的逻辑留给子类实现受保护的抽象void process couponprice//处理交付费用的逻辑留给子类实现受保护的抽象void过程交付价格;字间距:0.8像素;背景色:rgb边框半径:5px盒影:rgba 0px 2px 10px框大小:边框-框;overflow-wrap:break-word " data-from-paste = " 1 " data-diagnose-> @ service public类NormalUserCart扩展abstract cart { @ Override protected void processCouponPrice;} @覆盖受保护的void processDeliveryPrice。乘法))。乘法));}}
VIP购物车VipUserCart直接继承NormalUserCart,只需要修改买多多优惠政策:
@Servicepublic类VipUserCart扩展了normal user cart { @ Override protected void processCouponPrice { item . setcouponprice。乘法))。乘-2)));} else { } }}

内部用户购物车InternalUserCart最简单,直接设置0运费和0折扣即可:
@Servicepublic类InternalUserCart扩展abstract cart { @ Override protected void processCouponPrice;}}
抽象类和三个子类的实现图如下:
是不是比三个独立的购物车程序简单很多?接下来,我们来看看如何避免三个if逻辑。
您可能已经注意到,在定义三个购物车子类时,我们在@Service注释中将Bean命名为。因为这三个购物车都被称为XXXUserCart,所以我们可以将用户类型字符串与UserCart拼接起来,形成购物车Bean的名称。然后使用Spring的IoC容器,可以直接通过Bean的名字获取AbstractCart,调用它的process方法实现通用性。
实际上,这是工厂模式,只通过Spring容器实现:
public Cart right)int userId){ abstract Cart Cart = application context . get bean;返回cart.process}
试想一下,如果后面有了新的用户类型和新的用户逻辑,是不是根本不需要对代码做任何修改,只需要添加一个新的XXXUserCart类来继承AbstractCart,实现特殊的折扣和运费处理逻辑就可以了?
这样我们使用工厂模式+模板方法模式,既消除了重复代码,又避免了修改已有代码的风险。这就是设计模式中的开放封闭原则:对修改封闭,对扩展开放。
2.使用注释+反射来消除重复代码。
业务代码可以OOP,你不觉得有点激动吗?我们来看一个三方接口的调用案例,这也是常见的业务逻辑。
假设银行提供了一些API接口,参数的序列化有点特殊。我们需要把参数依次拼在一起,形成一个大字符串,而不是使用JSON。
根据银行提供的API文档顺序,将所有参数做成定长数据,然后拼接在一起成为整个字符串。
因为每个参数都有固定的长度,所以当没有达到长度时,需要填充它:
类型的参数小于长度的部分需要用下划线填充到右边,即字符串的内容在左边;
类型的参数小于长度的部分左填0,即实际数在右边;
对于货币类型的表示,需要将金额向下舍入2位数,以点为单位进行划分。作为数字类型,它也是左填充的。
对所有参数进行MD5操作作为签名。请与Java项目分享缺失的项目。
例如,用户创建方式和支付方式的定义如下:代码很容易实现。您可以根据接口定义直接执行填充操作、签名和调用操作:
Class bank service {//create user方法public static string create user抛出io异常{ stringbuilder stringbuilder = new stringbuilder;//字符串左边,多余的地方用_ stringBuilder.append.replace填充);“%-18s”,身份)。替换);//编号在右边,多出来的地方填0 "%05d ",年龄));//字符串左边,多出来的地方用_ "%-11s ",移动)。替换);//最后添加MD5作为签名stringbuilder . append));退货请求。贴吧。bodyString,ContentType。APPLICATION _ JSON). execute . return content . asstring;}//支付方法公共静态字符串pay throws io异常{new "%020d ",userid));//金额向下取2位数到分钟,分单位。作为数字,在右边,超出的地方填0 "%010d ",金额。setscale.multiple)。长值));返回" http://localhost:45678/reflection/bank/pay ")} }
如您所见,这段代码的重复粒度更细:
三种标准数据类型的处理逻辑重复,稍有不慎就会出现Bug;
过程中的字符串拼接、签名、发送请求的逻辑在所有方法中都是重复的;
方法的实际参数类型和顺序不一定与接口要求一致,容易出错;
每个参数的代码级别都是硬编码的,所以查不清楚。如果有几十个或者几百个参数,出错的概率就很大。
那么我们应该如何修改这段代码呢?没错,就是用注解和倒影!通过使用注释和反射这两个武器,银行要求的所有逻辑都可以用一套代码实现,没有任何重复。
要实现接口逻辑和逻辑实现的分离,首先需要用POJO类定义所有的接口参数。例如,用于创建用户API的以下参数:
@Datapublic类create user API { private String name;私有字符串标识;私有字符串移动;私人年龄;}

有了接口参数定义,我们可以通过自定义注释为接口和所有参数添加一些元数据。如下图,我们定义了接口API的注释BankAPI,包括接口URL地址和接口描述:
@ Retention @ Target @ documented @ inherited public @ interface bank API { String desc默认值" ";字符串url默认值“”;}
然后,我们定义一个自定义注释@BankAPIField,用来描述接口的每个字段规范,包括三个属性:参数的顺序、类型和长度:
@ Retention @ Target @ documented @ inherited public @ interface BankAPIField { int order default-1;int length默认值-1;字符串类型默认值“”;}
接下来,注释就可以发挥它的威力了。


