学而时习之,不亦说乎
前言
模板方法模式 + 工厂模式
需求场景
产品需要开发一个购物车金额结算功能,不同类型的客户,购物车结算的逻辑不同,需要按如下规则结算:
- 普通用户:商品无优惠,运费是商品总价的5%
- VIP用户:商品打9折,运费是商品总价的5%
- 内部用户:商品打8折,不需要运费。
常规实现
初步分析需求,我们拟定会新增商品item、购物车cart实体类,一个购物车Service类,在该类中实现不同客户的购物车结算逻辑。代码如下:
@Data
public class BsItem {
/**
* 商品ID
*/
private Long id;
/**
* 商品数量
*/
private int count;
/**
* 商品单价
*/
private BigDecimal price;
/**
* 商品优惠
*/
private BigDecimal discountPrice;
/**
* 商品运费
*/
private BigDecimal transportPrice;
@Data
public class BsCart {
/**
* 商品清单
*/
private List<BsItem> items = new ArrayList<>();
/**
* 总优惠
*/
private BigDecimal totalDiscount;
/**
* 商品总价
*/
private BigDecimal totalItemPrice;
/**
* 总运费
*/
private BigDecimal totalTransportPrice;
/**
* 应付总价
*/
private BigDecimal totalPayPrice;
}
public interface IBsCartService {
/**
* 生成用户结算购物车
* @param userId
* @param items
* @return
*/
BsCart genUserCart(String userId, Map<Long, Integer> items);
}
@Service
public class BsCartServiceImpl implements IBsCartService {
@Override
public BsCart genUserCart(String userId, Map<Long, Integer> items) {
BsCart cart = new BsCart();
//把Map的购物信息转换为Item列表
List<BsItem> itemList = new ArrayList<>();
items.entrySet().forEach(entry -> {
BsItem bsItem = new BsItem();
bsItem.setId(entry.getKey());
bsItem.setPrice(this.getItemPrice(entry.getKey()));
bsItem.setCount(entry.getValue());
itemList.add(bsItem);
});
//根据用户ID获得用户类型
String userCategory = this.getUserCategory(userId);
if ("Normal".equals(userCategory)) {
//处理商品优惠和运费
itemList.forEach(bsItem -> {
//商品无优惠
bsItem.setDiscountPrice(BigDecimal.ZERO);
//运费收取商品总价的5%
bsItem.setTransportPrice(
bsItem.getPrice().multiply(BigDecimal.valueOf(bsItem.getCount()))
.multiply(new BigDecimal("0.05")));
});
}else if("VIP".equals(userCategory)){
//处理商品优惠和运费
itemList.forEach(bsItem -> {
//商品打9折
bsItem.setDiscountPrice( bsItem.getPrice().multiply(BigDecimal.valueOf(bsItem.getCount()))
.multiply(new BigDecimal("0.9")));
//运费收取商品总价的5%
bsItem.setTransportPrice(
bsItem.getPrice().multiply(BigDecimal.valueOf(bsItem.getCount()))
.multiply(new BigDecimal("0.05")));
});
}else if("Inner".equals(userCategory)){
//处理商品优惠和运费
itemList.forEach(bsItem -> {
//商品打8折
bsItem.setDiscountPrice( bsItem.getPrice().multiply(BigDecimal.valueOf(bsItem.getCount()))
.multiply(new BigDecimal("0.8")));
//免运费
bsItem.setTransportPrice(BigDecimal.ZERO);
});
}
//设置购物车商品列表
cart.setItems(itemList);
//计算商品总价
cart.setTotalItemPrice(itemList.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getCount()))).reduce(BigDecimal.ZERO, BigDecimal::add));
//计算商品优惠价
cart.setTotalDiscount(itemList.stream().map(BsItem::getDiscountPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//计算运费总价
cart.setTotalTransportPrice(itemList.stream().map(BsItem::getTransportPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//应付总价=商品总价+运费总价-总优惠
cart.setTotalPayPrice(cart.getTotalItemPrice().add(cart.getTotalTransportPrice()).subtract(cart.getTotalDiscount()));
return cart;
}
/**
* 获取用户类型,实际业务中需要从数据库查询获取
* @param userId
* @return
*/
private String getUserCategory(String userId) {
return "Normal";
}
/**
* 获取商品价格,实际业务中需要从数据库查询获取
* @param itemId
* @return
*/
private BigDecimal getItemPrice(Long itemId) {
return new BigDecimal("20");
}
}
我们主要看BsCartServiceImpl购物车实现类,在没有考虑设计模式的情况下,我们在实际开发中可能就按照如上的实现方式开发。
仔细分析这种实现可以优化的地方:
- 首先genUserCart承担的功能较重,方法中包含了:商品的初始化,优惠金额、运费的计算、整个购物车的初始化、统计总价、总运费、总优惠和支付价格的逻辑,根据责任单一原则,尽可能不要让一个类承担过多的职责,避免职责耦合在一起。
- 如果后续有新的用户类型加入,则需要不断更改此处的逻辑,写更多的 if...else。违背了开放封闭原则,应该尽量通过拓展的方式来实现变化
优化实现
- 针对第一点,我们使用模板方法模式进行改造,创建一个抽象类,其中实现购物车处理的流程模板,包括商品初始化,统计总价,总运费等,然后把需要特殊处理的地方留空白也就是留抽象方法定义,让子类去实现其中的逻辑。
AbstractCart 抽象类实现了购物车通用的逻辑,额外定义了两个抽象方法让子类去实现。其中,processTransportPrice 方法用于计算商品优惠,processDiscountPrice 方法用于计算运费。具体代码如下 :
public abstract class AbstractCart {
/**
* 处理购物车的大量重复逻辑在父类实现
* @param userId
* @param items
* @return
*/
public BsCart process(String userId, Map<Long,Integer> items) {
BsCart cart = new BsCart();
//把Map的购物信息转换为Item列表
List<BsItem> itemList = new ArrayList<>();
items.entrySet().forEach(entry -> {
BsItem bsItem = new BsItem();
bsItem.setId(entry.getKey());
bsItem.setPrice(this.getItemPrice(entry.getKey()));
bsItem.setCount(entry.getValue());
itemList.add(bsItem);
});
//让子类处理每一个商品的优惠
itemList.stream().forEach(item -> {
//处理优惠
processDiscountPrice(userId, item);
//处理运费
processTransportPrice(userId, item);
});
//设置购物车商品列表
cart.setItems(itemList);
//计算商品总价
cart.setTotalItemPrice(itemList.stream().map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getCount()))).reduce(BigDecimal.ZERO, BigDecimal::add));
//计算商品优惠价
cart.setTotalDiscount(itemList.stream().map(BsItem::getDiscountPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//计算运费总价
cart.setTotalTransportPrice(itemList.stream().map(BsItem::getTransportPrice).reduce(BigDecimal.ZERO, BigDecimal::add));
//应付总价=商品总价+运费总价-总优惠
cart.setTotalPayPrice(cart.getTotalItemPrice().add(cart.getTotalTransportPrice()).subtract(cart.getTotalDiscount()));
return cart;
}
/**
* 处理商品优惠的逻辑留给子类实现
* @param userId
* @param item
*/
protected abstract void processDiscountPrice(String userId, BsItem item);
/**
* 处理配送费的逻辑留给子类实现
* @param userId
* @param item
*/
protected abstract void processTransportPrice(String userId, BsItem item);
/**
* 获取商品价格,实际业务中需要从数据库查询获取
* @param itemId
* @return
*/
private BigDecimal getItemPrice(Long itemId) {
return new BigDecimal("20");
}
创建各用户类型购物车子类,继承该父类,实现各自的优惠、运费计算逻辑。
@Service("NormalUserCart")
public class NormalUserCart extends AbstractCart{
@Override
protected void processDiscountPrice(String userId, BsItem bsItem) {
//商品无优惠
bsItem.setDiscountPrice(BigDecimal.ZERO);
}
@Override
protected void processTransportPrice(String userId, BsItem bsItem) {
//运费收取商品总价的5%
bsItem.setTransportPrice(
bsItem.getPrice().multiply(BigDecimal.valueOf(bsItem.getCount()))
.multiply(new BigDecimal("0.05")));
}
}
@Service("VipUserCart")
public class VipUserCart extends NormalUserCart {
@Override
protected void processDiscountPrice(String userId, BsItem bsItem) {
//商品打9折
bsItem.setDiscountPrice( bsItem.getPrice().multiply(BigDecimal.valueOf(bsItem.getCount()))
.multiply(new BigDecimal("0.9")));
}
}
@Service("InnerUserCart")
public class InnerUserCart extends AbstractCart{
@Override
protected void processDiscountPrice(String userId, BsItem bsItem) {
//商品打8折
bsItem.setDiscountPrice( bsItem.getPrice().multiply(BigDecimal.valueOf(bsItem.getCount()))
.multiply(new BigDecimal("0.8")));
}
@Override
protected void processTransportPrice(String userId, BsItem bsItem) {
//免运费
bsItem.setTransportPrice(BigDecimal.ZERO);
}
}
- 针对第二点,我们使用工厂设计模式进行改造,定义三个购物车子类时,我们在 @Service 注解中对 Bean 进行了命名。既然三个购物车都叫 XXXUserCart,那我们就可以把用户类型字符串拼接 UserCart 构成购物车 Bean 的名称,然后利用 Spring 的 IoC 容器,通过 Bean 的名称直接获取到 AbstractCart,调用其 process 方法即可实现通用。
@Resource
private ApplicationContext applicationContext;
@Override
public BsCart genUserCartBetter(String userId, Map<Long, Integer> items) {
//根据用户ID获得用户类型
String userCategory = this.getUserCategory(userId);
AbstractCart cart = (AbstractCart) applicationContext.getBean(userCategory + "UserCart");
return cart.process(userId, items);
}
结束语
至此,利用模板设计模式和工厂设计模式,不仅消除了方法功能过重,代码重复问题,还实现了代码的可扩展性,对修改关闭,对拓展开放,后续有新的类型的用户购物车,仅需要创建XXXUserCart,继承AbstractCart,实现其优惠和运费的特定逻辑即可。
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情”