代码重复使用的方法

核心提示软件工程师和码农最大的区别就是平时写代码时习惯问题,码农很喜欢写重复代码而软件工程师会利用各种技巧去干掉重复的冗余代码。业务同学抱怨业务开发没有技术含量,用不到设计模式、Java 高级特性、OOP,平时写代码都在堆 CRUD,个人成长无从谈

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

商科学生抱怨业务开发没有技术含量,不会用设计模式,不会用Java的高级特性,不会用OOP。通常他们写代码都是积垢,个人成长无从谈起。

其实我觉得不是。设计模式和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;字符串类型默认值“”;}

接下来,注释就可以发挥它的威力了。

 
友情链接
鄂ICP备19019357号-22