设计模式-给代码健健身

169 阅读5分钟

学而时习之,不亦说乎

前言

模板方法模式 + 工厂模式

需求场景

产品需要开发一个购物车金额结算功能,不同类型的客户,购物车结算的逻辑不同,需要按如下规则结算:

  1. 普通用户:商品无优惠,运费是商品总价的5%
  2. VIP用户:商品打9折,运费是商品总价的5%
  3. 内部用户:商品打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购物车实现类,在没有考虑设计模式的情况下,我们在实际开发中可能就按照如上的实现方式开发。

仔细分析这种实现可以优化的地方:

  1. 首先genUserCart承担的功能较重,方法中包含了:商品的初始化,优惠金额、运费的计算、整个购物车的初始化、统计总价、总运费、总优惠和支付价格的逻辑,根据责任单一原则,尽可能不要让一个类承担过多的职责,避免职责耦合在一起。
  2. 如果后续有新的用户类型加入,则需要不断更改此处的逻辑,写更多的 if...else。违背了开放封闭原则,应该尽量通过拓展的方式来实现变化

优化实现

  1. 针对第一点,我们使用模板方法模式进行改造,创建一个抽象类,其中实现购物车处理的流程模板,包括商品初始化,统计总价,总运费等,然后把需要特殊处理的地方留空白也就是留抽象方法定义,让子类去实现其中的逻辑。

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);
    }
}
  1. 针对第二点,我们使用工厂设计模式进行改造,定义三个购物车子类时,我们在 @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 天,点击查看活动详情