文章目录
- 需求
- 基础实体类
- 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 码