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.rhkb.cn/news/454215.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【JavaScript】Javascript基础Day02:运算符、分支、循环

Javascript——Day02 01. 赋值运算符02. 自增运算符03. 比较运算符04. 逻辑运算符以及优先级05. if单分支语句06. if双分支语句07. if多分支语句08. 三元运算符09. 数字补0案例10. switch分支语句11. 断点调试12. while循环13. 退出循环 01. 赋值运算符 02. 自增运算符 03. 比较…

【java】抽象类和接口(了解,进阶,到全部掌握)

各位看官早安午安晚安呀 如果您觉得这篇文章对您有帮助的话 欢迎您一键三连&#xff0c;小编尽全力做到更好 欢迎您分享给更多人哦 大家好我们今天来学习Java面向对象的的抽象类和接口&#xff0c;我们大家庭已经来啦~ 一&#xff1a;抽象类 1.1:抽象类概念 在面向对象的概念中…

Python|基于Kimi大模型,实现上传文档并进行“多轮”对话(7)

前言 本文是该专栏的第7篇,后面会持续分享AI大模型干货知识,记得关注。 假设有这样的需求,需要你通过python基于kimi大模型,上传对应的文档并根据对应的prompt提示词,进行多轮对话。此外,还需要将kimi大模型生成的内容进行存储。具体场景,如下图所示: 也就是说,当我们…

这种V带的无极变速能用在新能源汽车上吧?

CVT的无极变速器的结构能用在电动汽车上吗&#xff1f;

【优选算法篇】在分割中追寻秩序:二分查找的智慧轨迹

文章目录 C 二分查找详解&#xff1a;基础题解与思维分析前言第一章&#xff1a;热身练习1.1 二分查找基本实现解题思路图解分析C代码实现易错点提示代码解读 1.2 在排序数组中查找元素的第一个和最后一个位置解题思路1.2.1 查找左边界算法步骤&#xff1a;图解分析C代码实现 1…

LeetCode94:二叉树的中序遍历

文章目录 &#x1f60a;1.题目&#x1f609;2.解法1.递归2.迭代 &#x1f60a;1.题目 尝试一下该题 &#x1f609;2.解法 1.递归 /*** Definition for a binary tree node.* struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* …

企业级 RAG 全链路优化关键技术

2024 云栖大会 - AI 搜索企业级 RAG 全链路优化关键技术 在2024云栖大会上&#xff0c;阿里云 AI 搜索研发负责人之一的邢少敏先生带领大家深入了解如何利用 RAG 技术优化决策支持、内容生成、智能推荐等多个核心业务场景&#xff0c;为企业数字化转型与智能化升级提供强有力的…

【Linux】了解pthread线程库,清楚并没有线程创建接口,明白Linux并不存在真正意义的线程(附带模型图详解析)

前言 大家好吖&#xff0c;欢迎来到 YY 滴Liunx系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的《Lin…

ECHO-GL:盈利电话驱动的异质图学习股票 走势预测

目录 简单概括1 背景知识相关的工作图学习在股票预测中的应用股票预测中的收益电话会议 方法异质股票图结构建造时间分配机制滑动窗机构库存空间关系模块异构边缘类型消息传递音频引导的注意聚合财报电话会议后股票动态模块预测和优化 实验消融研究 (for Q2)模拟交易 (for Q3) …

vue组件传值之$attrs

1.概述&#xff1a;$attrs用于实现当前组件的父组件&#xff0c;向当前组件的子组件通信&#xff08;祖-》孙&#xff09; 2.具体说明&#xff1a;$attrs是一个对象&#xff0c;包含所有父组件传入的标签属性。 注意&#xff1a;$attrs会自动排除props中声明的属性&#xff0…

【不要离开你的舒适圈】:猛兽才希望你落单,亲人总让你回家,4个维度全面构建舒适圈矩阵

单打独斗的英雄时代已经落幕 抱团取暖才是社会寒冬的良策 自然界中&#xff0c;每个物种都占据着自己的领地和生存空间。 生态位的差异决定了它们的生存方式&#xff0c;一旦离开领地&#xff0c;失去群体的庇护&#xff0c;就会沦为野兽的美餐。 人类社会同样存在隐形圈层…

【C++】踏上C++学习之旅(三):“我“ 与 “引用“ 的浪漫邂逅

文章目录 前言1. "引用"的概念1.1 "引用"的语法 2. "引用"的特性3. "引用"的使用场景3.1 "引用"做参数3. 2 "引用"做返回值3.2.1 "引用"做返回值时需要注意的点 4. 常引用5. "引用"在底层的实…

自动化数据处理:使用Selenium与Excel打造的数据爬取管道

随着互联网信息爆炸式增长&#xff0c;获取有效数据成为决策者的重要任务。人工爬取数据不仅耗时且效率低下&#xff0c;因此自动化数据处理成为一种高效解决方案。本文将介绍如何使用Selenium与Excel实现数据爬取与处理&#xff0c;结合代理IP技术构建一个可稳定运行的数据爬取…

RocketMQ快速开始

前置推荐阅读&#xff1a;RocketMQ简介-CSDN博客 本地部署 RocketMQ 这一节介绍如何快速部署一个单节点单副本 RocketMQ 服务&#xff0c;并完成简单的消息收发。 系统要求 64位操作系统&#xff0c;推荐 Linux/Unix/macOS64位 JDK 1.8 1.下载安装Apache RocketMQ​ RocketMQ…

aws 把vpc残留删除干净

最近忘了把vpc 删干净导致又被收了冤大头钱 在删除vpc 的收发现又eni在使用&#xff0c;但是忘了是哪个资源在占用 先用命令行把占用的资源找出来停掉 使用 AWS 命令行界面&#xff08;CLI&#xff09;来查看 VPC 的使用情况 列出子网&#xff1a; aws ec2 describe-subnets …

抖音列表页采集-前言

准备工作&#xff1a; 1.关于selenium介绍&#xff1a; python自动化入门的话&#xff0c;selenium绝对是最方便的选择&#xff0c;基本逻辑即为&#xff1a;程序模拟人的行为操作浏览器&#xff0c;这样的操作需要借用浏览器驱动&#xff0c;我选用的是chrome浏览器&#xff…

浮动练习(3)

##每台电脑分辨率不同&#xff0c;数值高度宽度需要自己调&#xff0c;仅供参考 <!DOCTYPE html> <html> <head> <meta charset"UTF-8"> <title></title> <style> div{ …

港大和字节提出长视频生成模型Loong,可生成具有一致外观、大运动动态和自然场景过渡的分钟级长视频。

HKU, ByteDance&#xff5c;⭐️ 港大和字节联合提出长视频生成模型Loong&#xff0c;该模型可以生成外观一致、运动动态大、场景过渡自然的分钟级长视频。选择以统一的顺序对文本标记和视频标记进行建模&#xff0c;并使用渐进式短到长训练方案和损失重新加权来克服长视频训练…

MATLAB(Octave)混电动力能耗评估

&#x1f3af;要点 处理电动和混动汽车能耗的后向和前向算法模型(simulink)&#xff0c;以及图形函数、后处理函数等实现。构建储能元数据信息&#xff1a;电池标称特性、电池标识符等以及静止、恒定电流和恒定电压等特征阶段。使用电流脉冲或要识别的等效电路模型类型配置阻抗…

多功能纤维上线,大脑肠道 “无线畅聊” 不是梦

大家好&#xff01;今天来了解一篇多功能微电子纤维研究——《Multifunctional microelectronic fibers enable wireless modulation of gut and brain neural circuits》发表于《Nature Biotechnology》。我们都知道大脑和内脏器官的沟通对生存至关重要&#xff0c;可一直以来…