当前位置: 首页 > news >正文

Java避坑案例 - 消除代码重复_模板方法与工厂模式的最佳实践

文章目录

  • 需求
  • 基础实体类
  • BadVersion
  • 优化: 利用工厂模式 + 模板方法模式,消除 if…else 和重复代码
    • 优化一: 模板方法的应用
      • AbstractCart 类(抽象类)
      • 各种购物车实现(继承抽象类)
        • 普通用户购物车 (NormalUserCart)
        • VIP 用户购物车 (VipUserCart)
        • 内部用户购物车 (InternalUserCart)
    • 优化二: 工厂模式结合 Spring 容器
  • 开闭原则与扩展性
  • 小结

在这里插入图片描述

需求

开发一个购物车下单的功能,针对不同用户进行不同处理:

  • 普通用户需要收取运费,运费是商品价格的 10%,无商品折扣;
  • VIP 用户同样需要收取商品价格 10% 的快递费,但购买两件以上相同商品时,第三件开始享受一定折扣;
  • 内部用户可以免运费,无商品折扣

目标是实现三种类型的购物车业务逻辑,把入参 Map 对象(Key 是商品 ID,Value是商品数量),转换为出参购物车类型 Cart

基础实体类

import lombok.Data;import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;@Data
public class Cart {//商品清单private List<Item> items = new ArrayList<>();//总优惠private BigDecimal totalDiscount;//商品总价private BigDecimal totalItemPrice;//总运费private BigDecimal totalDeliveryPrice;//应付总价private BigDecimal payPrice;
}
import lombok.Data;import java.math.BigDecimal;@Data
public class Item {//商品Idprivate long id;//商品数量private int quantity;//商品单价private BigDecimal price;//商品优惠private BigDecimal couponPrice;//商品运费private BigDecimal deliveryPrice;
}

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;public class Db {private static Map<Long, BigDecimal> items = new HashMap<>();static {items.put(1L, new BigDecimal("10"));items.put(2L, new BigDecimal("20"));}public static BigDecimal getItemPrice(long id) {return items.get(id);}public static String getUserCategory(long userId) {if (userId == 1L) return "Normal";if (userId == 2L) return "Vip";if (userId == 3L) return "Internal";return "Normal";}public static int getUserCouponPercent(long userId) {return 90;}
}

BadVersion

普通用户购物车处理

 
public class NormalUserCart {public Cart process(long userId, Map<Long, Integer> items) {Cart cart = new Cart();//把Map的购物车转换为Item列表List<Item> itemList = new ArrayList<>();items.entrySet().stream().forEach(entry -> {Item item = new Item();item.setId(entry.getKey());item.setPrice(Db.getItemPrice(entry.getKey()));item.setQuantity(entry.getValue());itemList.add(item);});cart.setItems(itemList);//处理运费和商品优惠itemList.stream().forEach(item -> {//运费为商品总价的10%item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));//无优惠item.setCouponPrice(BigDecimal.ZERO);});//计算纯商品总价cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));//计算运费总价cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));//计算总优惠cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));//应付总价=商品总价+运费总价-总优惠cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));return cart;}
}

VipUserCart

与普通用户购物车逻辑的不同在于,VIP 用户能享受同类商品多买的折扣。所以,这部分代码只需要额外处理多买折扣部分。

 public class VipUserCart {public Cart process(long userId, Map<Long, Integer> items) {// ......itemList.stream().forEach(item -> {//运费为商品总价的10%item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));//购买两件以上相同商品,第三件开始享受一定折扣if (item.getQuantity() > 2) {item.setCouponPrice(item.getPrice().multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100"))).multiply(BigDecimal.valueOf(item.getQuantity() - 2)));} else {item.setCouponPrice(BigDecimal.ZERO);}});// 省略 ......return cart;}
}

InternalUserCart

最后是免运费、无折扣的内部用户,同样只是处理商品折扣和运费时的逻辑差异

 public class InternalUserCart {public Cart process(long userId, Map<Long, Integer> items) {// 省略 ......itemList.stream().forEach(item -> {//免运费item.setDeliveryPrice(BigDecimal.ZERO);//无优惠item.setCouponPrice(BigDecimal.ZERO);});// 省略 ......return cart;}
}

对比一下代码量可以发现,三种购物车 70% 的代码是重复的。原因很简单,虽然不同类型用户计算运费和优惠的方式不同,但整个购物车的初始化、统计总价、总运费、总优惠和支付价格的逻辑都是一样的.

代码重复本身不可怕,可怕的是漏改或改错。比如,写 VIP 用户购物车的同学发现商品总价计算有 Bug,不应该是把所有 Item 的 price 加在一起,而是应该把所有 Item 的 price*quantity 加在一起。这时,他可能会只修改 VIP 用户购物车的代码,而忽略了普通用户、内部用户的购物车中,重复的逻辑实现也有相同的 Bug.


有了三个购物车后,我们就需要根据不同的用户类型使用不同的购物车了。如下代码所示,使用三个 if 实现不同类型用户调用不同购物车的 process 方法

  /*** 根据用户ID处理购物车信息,根据用户类别选择不同的处理方式* 此方法存在潜在的问题:安全性风险、性能问题、代码可维护性差* 建议重构以提高代码质量和安全性** @param userId 用户ID,用于识别用户类别* @return 根据用户类别处理后的购物车对象,如果用户类别不匹配则返回null*/
@GetMapping("badVersion")
public Cart badVersion(@RequestParam("userId") int userId) {// 获取用户类别,以便根据类别处理购物车String userCategory = Db.getUserCategory(userId);// 根据用户类别创建并处理对应的购物车if (userCategory.equals("Normal")) {NormalUserCart normalUserCart = new NormalUserCart();// 处理普通用户的购物车return normalUserCart.process(userId, items);}if (userCategory.equals("Vip")) {VipUserCart vipUserCart = new VipUserCart();// 处理VIP用户的购物车return vipUserCart.process(userId, items);}if (userCategory.equals("Internal")) {InternalUserCart internalUserCart = new InternalUserCart();// 处理内部用户的购物车return internalUserCart.process(userId, items);}// 如果用户类别不匹配,返回nullreturn null;
}

优化: 利用工厂模式 + 模板方法模式,消除 if…else 和重复代码


优化一: 模板方法的应用

如果我们知道抽象类和抽象方法的定义的话,这时或许就会想到,是否可以把重复的逻辑定义在抽象类中,三个购物车只要分别实现不同的那份逻辑呢?

其实,这个模式就是模板方法模式。我们在父类中实现了购物车处理的流程模板,然后把需要特殊处理的地方留空白也就是留抽象方法定义,让子类去实现其中的逻辑。由于父类的逻辑不完整无法单独工作,因此需要定义为抽象类。

模板方法模式(Template Method Pattern)

在父类中定义了一个流程的框架,让子类实现流程中的细节部分。这避免了重复逻辑出现在多个子类中。

优化点:

  • process 方法中的逻辑统一提取,减少代码冗余。
  • 每个子类只需要实现它独特的运费和优惠逻辑,不会影响公共逻辑。

AbstractCart 类(抽象类)

如下代码所示,AbstractCart 抽象类实现了购物车通用的逻辑,额外定义了两个抽象方法
让子类去实现。其中,processCouponPrice 方法用于计算商品折扣,processDeliveryPrice 方法用于计算运费

/*** 抽象购物车类,用于处理用户购物车的相关操作*/
public abstract class AbstractCart {/*** 处理用户购物车,计算总价、运费、折扣等信息* * @param userId 用户ID,用于获取用户特定的优惠和配送信息* @param items 购物车中的商品及其数量,键为商品ID,值为数量* @return 返回一个Cart对象,包含处理后的购物车信息*/public Cart process(long userId, Map<Long, Integer> items) {// 创建一个新的Cart对象Cart cart = new Cart();// 创建一个商品列表,用于存储购物车中的所有商品项List<Item> itemList = new ArrayList<>();// 遍历商品及其数量的映射,创建并添加商品对象到商品列表中items.entrySet().stream().forEach(entry -> {Item item = new Item();item.setId(entry.getKey());item.setPrice(Db.getItemPrice(entry.getKey()));item.setQuantity(entry.getValue());itemList.add(item);});cart.setItems(itemList);// (让子类处理每一个商品的优惠)  遍历商品列表,为每个商品处理优惠价格和配送价格  itemList.stream().forEach(item -> {processCouponPrice(userId, item);processDeliveryPrice(userId, item);});// 计算购物车中所有商品的总价cart.setTotalItemPrice(cart.getItems().stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add));// 计算购物车中所有商品的总运费cart.setTotalDeliveryPrice(cart.getItems().stream().map(Item::getDeliveryPrice).reduce(BigDecimal.ZERO, BigDecimal::add));// 计算购物车中所有商品的总折扣cart.setTotalDiscount(cart.getItems().stream().map(Item::getCouponPrice).reduce(BigDecimal.ZERO, BigDecimal::add));// 计算购物车的最终支付价格cart.setPayPrice(cart.getTotalItemPrice().add(cart.getTotalDeliveryPrice()).subtract(cart.getTotalDiscount()));return cart;}/*** 处理商品的优惠价格* * @param userId 用户ID,用于获取用户特定的优惠信息* @param item 购物车中的商品项*/protected abstract void processCouponPrice(long userId, Item item);/*** 处理商品的配送价格* * @param userId 用户ID,用于获取用户特定的配送信息* @param item 购物车中的商品项*/protected abstract void processDeliveryPrice(long userId, Item item);
}

各种购物车实现(继承抽象类)

普通用户购物车 (NormalUserCart)

有了这个抽象类,三个子类的实现就非常简单了。普通用户的购物车 NormalUserCart,实现的是 0 优惠和 10% 运费的逻辑

@Service(value = "NormalUserCart")
public class NormalUserCart extends AbstractCart {@Overrideprotected void processCouponPrice(long userId, Item item) {item.setCouponPrice(BigDecimal.ZERO);}@Overrideprotected void processDeliveryPrice(long userId, Item item) {item.setDeliveryPrice(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())).multiply(new BigDecimal("0.1")));}
}

VIP 用户购物车 (VipUserCart)

VIP 用户的购物车 VipUserCart,直接继承了 NormalUserCart,只需要修改多买优惠策略


@Service(value = "VipUserCart")
public class VipUserCart extends NormalUserCart {@Overrideprotected void processCouponPrice(long userId, Item item) {if (item.getQuantity() > 2) {item.setCouponPrice(item.getPrice().multiply(BigDecimal.valueOf(100 - Db.getUserCouponPercent(userId)).divide(new BigDecimal("100"))).multiply(BigDecimal.valueOf(item.getQuantity() - 2)));} else {item.setCouponPrice(BigDecimal.ZERO);}}
}

内部用户购物车 (InternalUserCart)

内部用户购物车 InternalUserCart 是最简单的,直接设置 0 运费和 0 折扣即可

@Service(value = "InternalUserCart")
public class InternalUserCart extends AbstractCart {@Overrideprotected void processCouponPrice(long userId, Item item) {item.setCouponPrice(BigDecimal.ZERO);}@Overrideprotected void processDeliveryPrice(long userId, Item item) {item.setDeliveryPrice(BigDecimal.ZERO);}
}

抽象类和三个子类的实现关系图,如下所示
在这里插入图片描述


优化二: 工厂模式结合 Spring 容器

定义三个购物车子类时,我们在 @Service 注解中对 Bean 进行了命名。既然三个购物车都叫 XXXUserCart,那我们就可以把用户类型字符串拼接 UserCart构成购物车 Bean 的名称,然后利用 Spring 的 IoC 容器,通过 Bean 的名称直接获取到AbstractCart,调用其 process 方法即可实现通用 .

其实,这就是工厂模式,只不过是借助 Spring 容器实现罢了

为了避免 if-else 结构,我们可以通过 Spring IoC 容器实现工厂模式,动态获取所需的购物车 Bean

   @RestControllerpublic class CartController {@Autowiredprivate ApplicationContext applicationContext;@GetMapping("/cart")public Cart getCart(@RequestParam("userId") long userId, @RequestParam Map<Long, Integer> items) {String userCategory = Db.getUserCategory(userId);AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart");return cart.process(userId, items);}
}

试想, 之后如果有了新的用户类型、新的用户逻辑,是不是完全不用对代码做任何修改,只要新增一个 XXXUserCart 类继承 AbstractCart,实现特殊的优惠和运费处理逻辑就可以
了?


开闭原则与扩展性

这种设计完全符合开闭原则:

  • 对修改关闭:现有逻辑无需修改,减少 Bug 风险。
  • 对扩展开放:新增用户类型只需创建新的购物车类继承 AbstractCart

小结

有多个并行的类实现相似的代码逻辑。我们可以考虑提取相同逻辑在父类中实现,差异逻辑通过抽象方法留给子类实现。使用类似的模板方法把相同的流程和逻辑固定成模板,保留差异的同时尽可能避免代码重复。

同时,可以使用 Spring 的 IoC 特性注入相应的子类,来避免实例化子类时的大量 if…else 码

在这里插入图片描述


http://www.mrgr.cn/news/56349.html

相关文章:

  • Electron入门笔记
  • 黑盒测试和白盒测试的具体方法(附加实际应用中的技巧和注意事项)
  • 爬虫日常实战
  • C++ 模版和继承
  • tracert和ping的区别
  • Excel中如何进行傅里叶变换(FT),几步完成
  • 【10月最新】植物大战僵尸杂交版即将新增【植物】内容介绍预告(附最新版本下载链接)
  • 23种设计模式具体实现方法
  • 点云数据介绍
  • SCANeR Studio 仿真数据获取和车辆座舱数据输入
  • 强心剂!EEMD-MPE-KPCA-LSTM、EEMD-MPE-LSTM、EEMD-PE-LSTM故障识别、诊断
  • 10.22学习
  • vue中实现css布局
  • 西门子 SMART PLC 扫码串口通讯
  • 【不要离开你的舒适圈】:猛兽才希望你落单,亲人总让你回家,4个维度全面构建舒适圈矩阵
  • Shell重定向输入输出
  • 数据库表的创建
  • 如何自定义一个自己的 Spring Boot Starter 组件(从入门到实践)
  • 算法的学习笔记—数组中的逆序对(牛客JZ51)
  • 安全测试概述和用例设计
  • Modbus协议缺陷(Modbus缺陷)(一次性可读取的寄存器数量有限、不支持寄存器位级写入操作)
  • 【C++】踏上C++学习之旅(三):“我“ 与 “引用“ 的浪漫邂逅
  • 每日算法一练:剑指offer——数组篇(3)
  • IO进程_day4
  • HomeAssistant自定义组件学习-【一】
  • 个税自然人扣缴客户端数据的备份与恢复(在那个文件夹)