策略模式重构高并发高风险业务模块

结论先行

基于REIS分析模型,策略模式,包含三种角色,分别是策略服务方角色,策略客户方角色,策略代理方角色,策略模式的宗旨是通过策略代理方,将策略客户方和策略服务方解耦合。策略代理方是整个模式的核心,它向上接收策略客户方的业务请求和指派的具体策略服务方对象,向下转发策略客户方的业务请求,执行具体的策略服务对象的方法。

基本思路

案例介绍:京东高并发,高风险的促销模块

第一版代码:小白级

第二版代码:入门级

第三版代码:面向对象级

第四版代码:策略模式级

第五版代码:策略模式+模板方法模式级(后期专门讲解)

案例介绍:京东高并发,高风险的促销业务模块

每年的电商行业大促,对于普通用户来说,是购物狂欢,而对于电商大厂负责黄金流程的相关架构师来说,就是到鬼门关走一趟,每年走两趟。上半年的618(我们京东发起的),下半年的双十一(说996是福报的哪位发起的),记得有一年,我参加由京东技术VP,一个50多岁的老头,主持的618大促总结会议,一个30多岁,负责某个黄金流程核心模块的年轻架构师,踌躇满志,意气风发的坐在前排,技术VP看着他说,“这个系统前面你们部门出来几次事故,今年你算是过关了”,这个年轻架构师心怀忐忑的回答道,“是啊,最算可以睡个好觉了“。我认为,这些年薪百万的架构师,承受的压力,应该对得起这种待遇。

电商黄金流程涉及的业务很多,和大促紧密相连的,除了比较有名的秒杀模块(这个我们后面会给大家专题分享),还有一个就是促销业务模块。

促销业务模块是一个对并发量要求非常高,风险又非常大,业务又非常复杂多变的业务模块。

原因一目了然,越是大促,电商的促销方式越多,店铺的参与度越高,订单量也是水涨船高,同时大促用户抢购等环节,对依赖的各个模块,性能都有很高要求,而促销模块是其中非常重要的模块,所以并发非常大,对性能要求非常高。

同时,由于促销进行价格优惠,都是和钱打交道,一旦出错,基本上都是不可挽回的损失,都是真金白银的损失,所以这些部门的架构师,大促的时候提心吊胆,心惊胆颤也不为过,就差烧香拜佛了。光我知道的,就有好几个架构师,因为大促线上故障而被优化掉了,丢掉了年薪百万的工作。

我所在的部门,也有一部分促销的业务模块,涉及京豆的业务,稍微好点,如果失败,损失的是京豆,比直接损失现金,罪过稍微轻点,但是也出过几次事故,导致多给了用户几十万京豆,换算一下得上万的损失,但是和严重的事故相比,是小巫见大巫。

电商和钱打交道的业务模块,这些架构师,设计系统的时候,不仅要考虑流量的压力,还有一个重要的方面,就是要和薅羊毛的羊毛党打交道,一个不小心,被这些专业黑产发现漏洞,有的是业务方面的,有的是技术方面的,就会被钻空子,导致真正的优惠都被羊毛党拿走了,真正的用户没得到优惠,店铺也没有达到促销的效果。

下面我们看一看京东的一些促销形式,或者叫促销策略。

满减

满减促销,是最常见的一种,就是当购买金额达到一定金额,可以给予一定的直接现金优惠,或者折扣优惠,满减支持以下几个级别:

SKU级别:单个SKU金额达到一定金额,进行优惠。

店铺级别:某个店铺的SKU的金额达到一定金额,进行优惠。

多店铺级别:这个相当于跨店优惠,当多个店铺的SKU达到一定金额,进行优惠。

全网级别:这个平时比较少见,大促的时候出现过几次。

电商常识:

SKU:电商里面管理库存的单位,你可以理解为商品,但又和商品不同,以苹果手机为例,每种颜色的手机,或者每种内存大小的手机,都可以建立一个相应的SKU,用户添加购物车时,选择的是一个SKU。更严格的定义,大家可以百度一下。

多购优惠

多购优惠,一般指的是同一个sku,购买多件的,给予一定的折扣优惠,数量越多,折扣相应越大。

Plus会员优惠

京东的会员,除了普通会员,还有一种是plus会员,有各种优惠政策,特别是运费券,不用在每次购物时凑单,就可以免除运费,还是比较划算的。

系统挑战

促销业务模块,为了应对高并发,每个小的功能模块,基本上都是一个团队,几十人来维护和开发,投入大量的服务器资源,才能带来性能的保障。我们这里关注的是这个业务模块的扩展性问题,促销形式的多变的,要适应商家多种促销形式。下面我们开始进行代码开发。

功能限制,我们的重点是讲解设计模式,所以我们对实现的功能模块进行一定的简化。我们实现的代码,要支持以上几种促销方法,要具备扩展性,同时为了简单,我们只考虑sku级别的优惠,而且对于同一个SKU,同一时刻,只能使用一种促销方式。

下面我们看代码实现。

第一版代码,小白级

UML类图

类图中关键接口和类如下:

Cart:购物车类,包含购物项CartItem列表和会员信息

CartItem:购物项类,包含SKU和购买的数量

SKU:SKU类,包含商品名字,架构,促销策略等信息

Member:会员类

PromotionStrategyEnum:促销策略枚举类

ISettlementService:结算服务接口

SetlementServiceV1:结算服务类,核心类。

Cart:购物车类

package com.geekarchitect.patterns.demo101;

import lombok.Data;

import java.util.List;

/**
 * 购物车
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
@Data
public class Cart {
    /**
     * 购物项列表
     */
    private List<CartItem> cartItemList;
    /**
     * 会员信息
     */
    private Member member;
}

CartItem:购物项类

package com.geekarchitect.patterns.demo101;

import lombok.Data;

/**
 * 购物项
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
@Data
public class CartItem {
    private SKU sku;
    private int quantity;
}

SKU:SKU类

package com.geekarchitect.patterns.demo101;

import lombok.Data;

import java.math.BigDecimal;

/**
 * 商品SKU
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
@Data
public class SKU {

    private String name;
    private BigDecimal price;
    private PromotionStrategyEnum promotionStrategy;
    private String id;

    public SKU() {
    }

    public SKU(String name, BigDecimal price, PromotionStrategyEnum promotionStrategy, String id) {
        this.name = name;
        this.price = price;
        this.promotionStrategy = promotionStrategy;
        this.id = id;
    }
}

Member:会员类

package com.geekarchitect.patterns.demo101;

import lombok.Data;

/**
 * @author 极客架构师@吴念
 * @createTime 2022/4/2
 */
@Data
public class Member {
    private Long id;
    private String nickName;
    private boolean plus;

    public Member() {
    }

    public Member(Long id, String nickName, boolean plus) {
        this.id = id;
        this.nickName = nickName;
        this.plus = plus;
    }
}

PromotionStrategyEnum:促销策略枚举类

package com.geekarchitect.patterns.demo101;

/**
 * 促销策略枚举类
 *
 * @author: 极客架构师@吴念
 * @date: 2022/3/26
 * @param:
 * @return:
 */
public enum PromotionStrategyEnum {
    NONE("无", 0), CACH_BACK("满减", 1), BUY_MORE("多买优惠", 2), FOR_PLUS("plus会员优惠", 3);
    private String name;
    private int code;

    PromotionStrategyEnum(String name, int code) {
        this.name = name;
        this.code = code;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}

ISettlementService:结算服务接口

package com.geekarchitect.patterns.demo101;

/**
 * 结算服务接口
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/24
 */
public interface ISettlementService {
    void settlement(Cart cart);
}

SetlementServiceV1:结算服务类

购物结算基本流程:

  1. 遍历购物车(Cart)里面的购物项(CartItem)列表

  2. 获取购物项里面的SKU

  3. 根据SKU里面的促销策略枚举值,确定促销策略

  4. 根据促销策略,计算促销后的价格(这里为了简单,促销价都是根据原价生成的一个随机值)

package com.geekarchitect.patterns.demo101;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;

/**
 * 结算服务
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
public class SetlementServiceV1 implements ISettlementService {

    private static final Logger LOG = LoggerFactory.getLogger(SetlementServiceV1.class);

    /**
     * 计算促销价格
     *
     * @author: 极客架构师@吴念
     * @date: 2022/4/1
     * @param:
     * @return:
     */
    private BigDecimal promotion(CartItem cartItem, Member member) {
        SKU sku = cartItem.getSku();
        BigDecimal originalSubtotal = sku.getPrice().multiply(new BigDecimal(cartItem.getQuantity()));
        BigDecimal promotionalSubtotal = new BigDecimal(0);
        switch (sku.getPromotionStrategy()) {
            case CACH_BACK:
                //此处省略几百行代码
                promotionalSubtotal = Utils.random(originalSubtotal);
                LOG.info("促销方式:满减,SKU名称:{} 原小计:{},促销后小计:{}", sku.getName(), originalSubtotal, promotionalSubtotal);
                break;
            case BUY_MORE:
                //此处省略几百行代码
                promotionalSubtotal = Utils.random(originalSubtotal);
                LOG.info("促销方式:多买优惠,SKU名称:{} 原小计:{},促销后小计:{}", sku.getName(), originalSubtotal, promotionalSubtotal);
                break;
            case FOR_PLUS:
                //此处省略几百行代码
                if (member.isPlus()) {
                    promotionalSubtotal = Utils.random(originalSubtotal);
                } else {
                    promotionalSubtotal = originalSubtotal;
                }

                LOG.info("促销方式:plus会员优惠,SKU名称:{} 原小计:{},促销后小计:{}", sku.getName(), originalSubtotal, promotionalSubtotal);
                break;
            case NONE:
                promotionalSubtotal = originalSubtotal;
                LOG.info("促销方式:无,SKU名称:{} 原小计:{},促销后小计:{}", sku.getName(), originalSubtotal, promotionalSubtotal);
        }
        return promotionalSubtotal;
    }

    @Override
    public void settlement(Cart cart) {
        LOG.info("第一版代码:小白级");
        BigDecimal total = new BigDecimal(0);
        for (CartItem cartItem : cart.getCartItemList()
        ) {
            total = total.add(promotion(cartItem, cart.getMember()));
        }
        LOG.info("订单总价格:{}", total.doubleValue());
    }
}

Utils类:工具类

package com.geekarchitect.patterns.demo101;

import java.math.BigDecimal;
import java.util.Random;

/**
 * @author 极客架构师@吴念
 * @createTime 2022/4/2
 */
public class Utils {
    /**
     * 生成一个随机数X, 0<=X<max
     *
     * @param max
     * @return
     */
    public static BigDecimal random(BigDecimal max) {
        BigDecimal x = new BigDecimal(new Random().nextDouble() * (max.doubleValue()));
        return x.setScale(2, BigDecimal.ROUND_DOWN);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            System.out.println(random(new BigDecimal(100)));
        }

    }
}

TestSettlementService类:测试类

package com.geekarchitect.patterns.demo101;

import com.geekarchitect.patterns.demo103.SetlementServiceV3;
import com.geekarchitect.patterns.demo104.SetlementServiceV4;
import com.geekarchitect.patterns.demo105.StrategyClientRole;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
 * @author 极客架构师@吴念
 * @createTime 2022/4/2
 */
public class TestSettlementService {
    public static void main(String[] args) {
        TestSettlementService testSettlementService = new TestSettlementService();
        Cart cart = testSettlementService.mockCart();

        //ISettlementService settlementService = new SetlementServiceV1();
        //ISettlementService settlementService=new SetlementServiceV2();
        //ISettlementService settlementService=new SetlementServiceV3();
        //ISettlementService settlementService=new SetlementServiceV4();
        //settlementService.settlement(cart);
        StrategyClientRole strategyClientRole=new StrategyClientRole();
        strategyClientRole.clientMethod();
    }

    public Cart mockCart() {
        Cart cart = new Cart();

        CartItem cartItem01 = new CartItem();
        cartItem01.setSku(new SKU("华为手机", new BigDecimal(6000), PromotionStrategyEnum.BUY_MORE, "1"));
        cartItem01.setQuantity(10);
        CartItem cartItem02 = new CartItem();
        cartItem02.setSku(new SKU("小米手机", new BigDecimal(5000), PromotionStrategyEnum.CACH_BACK, "2"));
        cartItem02.setQuantity(20);
        CartItem cartItem03 = new CartItem();
        cartItem03.setSku(new SKU("三星手机", new BigDecimal(7000), PromotionStrategyEnum.NONE, "3"));
        cartItem03.setQuantity(20);
        List<CartItem> cartItemList = new ArrayList();
        cartItemList.add(cartItem01);
        cartItemList.add(cartItem02);
        cartItemList.add(cartItem03);
        cart.setCartItemList(cartItemList);

        Member member = new Member(1L, "码农老吴", true);
        cart.setMember(member);

        return cart;
    }
}

运行结果

头脑风暴

如上图代码所示,这版代码缺点很明显,SetlementServiceV1.promotion(),代码非常臃肿,如果是真实的业务代码,每个促销策略,里面可能都包含几十行或者几百行代码,我们这里也借鉴了著名作家贾平凹的文风,将”此处省略一万字“,改成了”此处省略几百行代码“。

另外,扩展性也非常差,如果增加新的促销策略,或者修改现有的促销策略,都需要修改这个方法里面的代码。除非是业务确实非常简单,否则大家一般不会编写出这样的代码,我们稍微升级一下,先不考虑扩展性,而是把这个方法代码精简一下,避免出现几千行代码的超大方法。

第二版代码,入门级

SetlementServiceV2代码

package com.geekarchitect.patterns.demo102;

import com.geekarchitect.patterns.demo101.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;

/**
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
public class SetlementServiceV2 implements ISettlementService {

    private static final Logger LOG = LoggerFactory.getLogger(SetlementServiceV2.class);

    private BigDecimal promotion(CartItem cartItem, Member member) {
        BigDecimal promotionalSubtotal = new BigDecimal(0);
        switch (cartItem.getSku().getPromotionStrategy()) {
            case CACH_BACK:
                promotionalSubtotal = cachBackPromotion(cartItem, member);
                break;
            case BUY_MORE:
                promotionalSubtotal = buyMorePromotion(cartItem, member);
                break;
            case FOR_PLUS:
                promotionalSubtotal = forPlusPromotion(cartItem, member);
                break;
            case NONE:
                promotionalSubtotal = nonePromotion(cartItem, member);
        }
        return promotionalSubtotal;
    }

    /**
     * 满减
     *
     * @author: 极客架构师@吴念
     * @date: 2022/4/1
     * @param: [cart]
     * @return: java.math.BigDecimal
     */
    private BigDecimal cachBackPromotion(CartItem cartItem, Member member) {
        SKU sku = cartItem.getSku();
        BigDecimal originalSubtotal = sku.getPrice().multiply(new BigDecimal(cartItem.getQuantity()));
        //此处省略几百行代码
        BigDecimal promotionalSubtotal = Utils.random(originalSubtotal);
        LOG.info("促销方式:满减,SKU名称:{} 原小计:{},促销后小计:{}", sku.getName(), originalSubtotal, promotionalSubtotal);
        return promotionalSubtotal;
    }

    /**
     * 多买优惠
     *
     * @author: 极客架构师@吴念
     * @date: 2022/4/1
     * @param: [cart]
     * @return: java.math.BigDecimal
     */
    private BigDecimal buyMorePromotion(CartItem cartItem, Member member) {
        SKU sku = cartItem.getSku();
        BigDecimal originalSubtotal = sku.getPrice().multiply(new BigDecimal(cartItem.getQuantity()));
        //此处省略几百行代码
        BigDecimal promotionalSubtotal = Utils.random(originalSubtotal);
        LOG.info("促销方式:多买优惠,SKU名称:{} 原小计:{},促销后小计:{}", sku.getName(), originalSubtotal, promotionalSubtotal);
        return promotionalSubtotal;
    }

    /**
     * plus会员优惠
     *
     * @author: 极客架构师@吴念
     * @date: 2022/4/1
     * @param: [cart]
     * @return: java.math.BigDecimal
     */
    private BigDecimal forPlusPromotion(CartItem cartItem, Member member) {
        SKU sku = cartItem.getSku();
        BigDecimal originalSubtotal = sku.getPrice().multiply(new BigDecimal(cartItem.getQuantity()));
        BigDecimal promotionalSubtotal = new BigDecimal(0);
        //此处省略几百行代码
        if (member.isPlus()) {
            promotionalSubtotal = Utils.random(originalSubtotal);
        } else {
            promotionalSubtotal = originalSubtotal;
        }
        LOG.info("促销方式:plus会员优惠,SKU名称:{} 原小计:{},促销后小计:{}", sku.getName(), originalSubtotal, promotionalSubtotal);
        return promotionalSubtotal;
    }

    private BigDecimal nonePromotion(CartItem cartItem, Member member) {
        SKU sku = cartItem.getSku();
        BigDecimal originalSubtotal = sku.getPrice().multiply(new BigDecimal(cartItem.getQuantity()));
        BigDecimal promotionalSubtotal = originalSubtotal;
        LOG.info("促销方式:plus会员优惠,SKU名称:{} 原小计:{},促销后小计:{}", sku.getName(), originalSubtotal, promotionalSubtotal);
        return promotionalSubtotal;
    }

    @Override
    public void settlement(Cart cart) {
        LOG.info("第二版代码:入门级");
        BigDecimal total = new BigDecimal(0);
        for (CartItem cartItem : cart.getCartItemList()
        ) {
            total = total.add(promotion(cartItem, cart.getMember()));
        }
        LOG.info("订单总价格:{}", total.doubleValue());
    }
}

头脑风暴

这一版代码,其他类和接口都没有变化,只有SetlementServiceV1类发生了变化,把每个促销策略,都封装成了独立的方法,原来的promotion()代码将会简化不少,普通的小型项目或许已经够用,但是扩展问题没有根本性变化,当需要新增促销策略时,需要新增一个方法,也没有体现面向对象的思想。我们继续重构。

第三版代码,面向对象级

UML类图

新增接口及类

IPromotionStrategy:促销策略接口

CachBackPromotionStrategy:满减促销策略类

BuyMorePromotionStrategy:多买优惠促销策略类

ForPlusPromotionStrategy:plus会员优惠促销策略类

NonePromotionStrategy:无促销策略类

IPromotionStrategy:促销策略接口

package com.geekarchitect.patterns.demo103;

import com.geekarchitect.patterns.demo101.Cart;
import com.geekarchitect.patterns.demo101.CartItem;
import com.geekarchitect.patterns.demo101.Member;

import java.math.BigDecimal;

/**
 * 促销策略接口
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
public interface IPromotionStrategy {
    /**
     * 计算促销金额
     *
     * @author: 极客架构师@吴念
     * @date: 2022/3/26
     * @param:
     * @return:
     */
    BigDecimal caclulate(CartItem cartItem, Member member);
}

CachBackPromotionStrategy:满减促销策略类

package com.geekarchitect.patterns.demo103;

import com.geekarchitect.patterns.demo101.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;

/**
 * 促销策略:满减
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
public class CachBackPromotionStrategy implements IPromotionStrategy {
    private static final Logger LOG = LoggerFactory.getLogger(CachBackPromotionStrategy.class);

    @Override
    public BigDecimal caclulate(CartItem cartItem, Member member) {
        SKU sku = cartItem.getSku();
        BigDecimal originalSubtotal = sku.getPrice().multiply(new BigDecimal(cartItem.getQuantity()));
        //此处省略几百行代码
        BigDecimal promotionalSubtotal = Utils.random(originalSubtotal);
        LOG.info("促销方式:满减,SKU名称:{} 原小计:{},促销后小计:{}", sku.getName(), originalSubtotal, promotionalSubtotal);
        return promotionalSubtotal;
    }
}

BuyMorePromotionStrategy:多买优惠促销策略类

package com.geekarchitect.patterns.demo103;

import com.geekarchitect.patterns.demo101.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;

/**
 * 促销策略:多买优惠
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
public class BuyMorePromotionStrategy implements IPromotionStrategy {
    private static final Logger LOG = LoggerFactory.getLogger(BuyMorePromotionStrategy.class);

    @Override
    public BigDecimal caclulate(CartItem cartItem, Member member) {
        SKU sku = cartItem.getSku();
        BigDecimal originalSubtotal = sku.getPrice().multiply(new BigDecimal(cartItem.getQuantity()));
        //此处省略几百行代码
        BigDecimal promotionalSubtotal = Utils.random(originalSubtotal);
        LOG.info("促销方式:多买优惠,SKU名称:{} 原小计:{},促销后小计:{}", sku.getName(), originalSubtotal, promotionalSubtotal);
        return promotionalSubtotal;
    }
}

ForPlusPromotionStrategy:plus会员优惠促销策略类

package com.geekarchitect.patterns.demo103;

import com.geekarchitect.patterns.demo101.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;

/**
 * 促销策略:Plus会员优惠
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
public class ForPlusPromotionStrategy implements IPromotionStrategy {
    private static final Logger LOG = LoggerFactory.getLogger(ForPlusPromotionStrategy.class);

    @Override
    public BigDecimal caclulate(CartItem cartItem, Member member) {
        SKU sku = cartItem.getSku();
        BigDecimal originalSubtotal = sku.getPrice().multiply(new BigDecimal(cartItem.getQuantity()));
        BigDecimal promotionalSubtotal = new BigDecimal(0);
        //此处省略几百行代码
        if (member.isPlus()) {
            promotionalSubtotal = Utils.random(originalSubtotal);
        } else {
            promotionalSubtotal = originalSubtotal;
        }
        LOG.info("促销方式:plus会员优惠,SKU名称:{} 原小计:{},促销后小计:{}", sku.getName(), originalSubtotal, promotionalSubtotal);
        return promotionalSubtotal;
    }
}

NonePromotionStrategy:无促销策略类

package com.geekarchitect.patterns.demo103;

import com.geekarchitect.patterns.demo101.CartItem;
import com.geekarchitect.patterns.demo101.Member;
import com.geekarchitect.patterns.demo101.SKU;
import com.geekarchitect.patterns.demo101.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;

/**
 * 促销策略:满减
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
public class NonePromotionStrategy implements IPromotionStrategy {
    private static final Logger LOG = LoggerFactory.getLogger(NonePromotionStrategy.class);

    @Override
    public BigDecimal caclulate(CartItem cartItem, Member member) {
        SKU sku = cartItem.getSku();
        BigDecimal originalSubtotal = sku.getPrice().multiply(new BigDecimal(cartItem.getQuantity()));
        BigDecimal promotionalSubtotal = originalSubtotal;
        LOG.info("促销方式:plus会员优惠,SKU名称:{} 原小计:{},促销后小计:{}", sku.getName(), originalSubtotal, promotionalSubtotal);
        return promotionalSubtotal;
    }
}

SetlementServiceV3类

package com.geekarchitect.patterns.demo103;

import com.geekarchitect.patterns.demo101.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;

/**
 * 基于面向对象
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
public class SetlementServiceV3 implements ISettlementService {

    private static final Logger LOG = LoggerFactory.getLogger(SetlementServiceV3.class);

    private BigDecimal promotion(CartItem cartItem, Member member) {
        BigDecimal subtotal = new BigDecimal(0);
        switch (cartItem.getSku().getPromotionStrategy()) {
            case CACH_BACK:
                subtotal = new CachBackPromotionStrategy().caclulate(cartItem, member);
                break;
            case BUY_MORE:
                subtotal = new BuyMorePromotionStrategy().caclulate(cartItem, member);
                break;
            case FOR_PLUS:
                subtotal = new ForPlusPromotionStrategy().caclulate(cartItem, member);
                break;
            case NONE:
                subtotal = new NonePromotionStrategy().caclulate(cartItem, member);
        }
        return subtotal;
    }

    @Override
    public void settlement(Cart cart) {
        LOG.info("第三版代码:面向对象级");
        BigDecimal total = new BigDecimal(0);
        for (CartItem cartItem : cart.getCartItemList()
        ) {
            total = total.add(promotion(cartItem, cart.getMember()));
        }
        LOG.info("订单总价格:{}", total.doubleValue());
    }
}

运行结果

头脑风暴

这一版,比上版代码就好很多了,定义了统一的促销策略接口,每个促销策略都封装成了一个独立的实现类。符合面向对象的编程思想,当增加新的促销策略时,只需要增加新的促销策略实现类,就可以了,当某个促销策略发生调整时,只需要修改它自己所在的类即可,一个促销策略的调整,不影响其他促销策略。对于普通的大中型项目,大部分情况下,也已经够用了,但是还是可以升级,我们继续优化。

第四版代码,策略模式级

UML类图

新增接口和类

IPromotionService:促销服务接口

PromotionService:促销服务类

UML序列图

IPromotionService:促销服务接口

package com.geekarchitect.patterns.demo104;

import com.geekarchitect.patterns.demo101.Cart;
import com.geekarchitect.patterns.demo101.CartItem;
import com.geekarchitect.patterns.demo101.Member;
import com.geekarchitect.patterns.demo103.IPromotionStrategy;

import java.math.BigDecimal;

/**
 * 促销服务接口
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
public interface IPromotionService {
    BigDecimal doCalculate(CartItem cartItem, Member member);

    IPromotionStrategy getPromotionStrategy();

    void setPromotionStrategy(IPromotionStrategy promotionStrategy);
}

PromotionService:促销服务类

package com.geekarchitect.patterns.demo104;

import com.geekarchitect.patterns.demo101.Cart;
import com.geekarchitect.patterns.demo101.CartItem;
import com.geekarchitect.patterns.demo101.Member;
import com.geekarchitect.patterns.demo103.IPromotionStrategy;

import java.math.BigDecimal;

/**
 * 促销管理服务接口
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
public class PromotionService implements IPromotionService {
    private IPromotionStrategy promotionStrategy;

    @Override
    public BigDecimal doCalculate(CartItem cartItem, Member member) {
        return promotionStrategy.caclulate(cartItem, member);
    }

    @Override
    public IPromotionStrategy getPromotionStrategy() {
        return promotionStrategy;
    }

    @Override
    public void setPromotionStrategy(IPromotionStrategy promotionStrategy) {
        this.promotionStrategy = promotionStrategy;
    }
}

SetlementServiceV4:结算服务类

package com.geekarchitect.patterns.demo104;

import com.geekarchitect.patterns.demo101.*;
import com.geekarchitect.patterns.demo103.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigDecimal;

/**
 * 基于策略模式
 *
 * @author 极客架构师@吴念
 * @createTime 2022/3/26
 */
public class SetlementServiceV4 implements ISettlementService {

    private static final Logger LOG = LoggerFactory.getLogger(SetlementServiceV4.class);

    private BigDecimal promotion(CartItem cartItem, Member member) {
        BigDecimal subtotal = new BigDecimal(0);
        IPromotionService promotionService = new PromotionService();
        IPromotionStrategy promotionStrategy = null;
        switch (cartItem.getSku().getPromotionStrategy()) {
            case CACH_BACK:
                promotionStrategy = new CachBackPromotionStrategy();
                break;
            case BUY_MORE:
                promotionStrategy = new BuyMorePromotionStrategy();
                break;
            case FOR_PLUS:
                promotionStrategy = new ForPlusPromotionStrategy();
                break;
            default:
                promotionStrategy = new NonePromotionStrategy();
        }
        promotionService.setPromotionStrategy(promotionStrategy);
        subtotal = promotionService.doCalculate(cartItem, member);
        return subtotal;
    }

    @Override
    public void settlement(Cart cart) {
        LOG.info("第四版代码:策略模式级");
        BigDecimal total = new BigDecimal(0);
        for (CartItem cartItem : cart.getCartItemList()
        ) {
            total = total.add(promotion(cartItem, cart.getMember()));
        }
        LOG.info("订单总价格:{}", total.doubleValue());
    }
}

运行结果

头脑风暴

这一版,我们就实现了基于策略模式的代码。和上一版比较,只是多了一个接口和一个类

IPromotionService:促销服务接口

PromotionService:促销服务类

促销策略相关的接口和类,没有任何变化。结算服务类有变化,不再直接调用促销策略类的方法,而是通过促销服务类来执行促销策略。结算服务类和促销策略类之间,增加了一层,多个一个促销服务类,它就是策略模式的灵魂,下面我们详细讲解策略模式。

策略模式(Template Method Pattern)定义

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

—— Gof《Design Patterns: Elements of Reusable Object-Oriented Software》

中文解释如下:

定义一系列的算法,把他们一个个封装起来,并且使它们可以相互替换。本模式使得算法可独立于使用它的客户而变化。

这个定义里面我们要注意的关键词是算法、客户。

算法

关于算法的解释,我在前面模板方法模式的分享中,已经给大家说过,大家不要被”算法“这个词吓唬住,它不是我们常说的”数据结构和算法“里面的算法,它是广义的算法,任何方法里面的代码,都可以说是一种算法。这个定义里面对于算法的要求是两个,一个是封装起来,一个是可互换。这个应该好理解,也比较好实现,只要定义统一的接口,所有算法类实现相同的接口,就可以实现算法的封装和可相互替换。

在我们的促销业务案例中,如下所示。

IPromotionStrategy:促销策略接口,所有促销策略的统一接口。

CachBackPromotionStrategy:满减促销策略类

BuyMorePromotionStrategy:多买优惠促销策略类

ForPlusPromotionStrategy:plus会员优惠促销策略类

NonePromotionStrategy:无促销策略类

上面四个具体的促销策略类,都执行了IPromotionStrategy促销策略接口,所以从面向对象的角度看,它们是相互可替换的。

客户

客户,谁的客户,当然是算法的客户,也就是需要算法执行结果的类。客户使用算法,意味着客户依赖算法,但是这个定义里面,要求算法和客户要相互独立。

那么怎么算相互独立呢?

从我们程序开发角度,大家应该能想到,当一个对象变化时,另外一个对象完全不受影响,或者基本不受影响,就可以称得上独立,用专业一点的术语说,就是这两个对象是解耦合的。说的再直白一些,当算法相关的类修改时,客户相关的类不需要修改,或者基本不需要修改,两者就可以称得上相互独立了。

那么如何独立呢?

在架构设计中,有一种道,或者是思想,就是要把两个对象(注意,这里的对象不是类,对象中的对象,它可以是系统,项目,模块,层,类,对象等)独立,或者叫解耦合,那就在他们之间再加个对象。这就好比儿子长大了,要和父亲独立,儿子还得生活啊,怎么办呢,要么依靠公司,要么依赖社会,中间总得加个第三方,才能独立。

总结一下,策略模式,就是通过定义接口,将一系列算法可以相互替换。然后在算法和算法的客户之间,增加一个第三者,让算法和算法的客户相互独立就可以了。

下面我们用我自创的设计模式分析模型REIS模式,对策略模式进行分析,大家就会理解的更透彻一些。

REIS模型分析策略模式

REIS模型是我总结的分析设计模式的一种方法论,主要包括场景(scene),角色(role),交互(interaction),效果(effect)四个要素,后面我会专门分享一下这套方法论。

场景(Scene)

场景,也就是我们在什么情况下,遇到了什么问题,需要使用某个设计模式。

对于策略模式,当出现以下情况时,可能需要使用策略模式。

  1. 从业务角度看: 当一个业务依赖一组相似的算法或者策略时,如果这些算法或者策略的变化比较大,需要提高扩展性,或者调用这些算法、策略比较复杂时,就有可能需要使用策略模式。

  2. 从技术角度看: (1)当条件语句(if else或者switch)分支比较多,分支里面的代码量比较大时,可以通过策略模式进行优化。在条件语句中,同一级别的分支,地位是相等的,如果代码量比较大,或者经常会增加新的分支,则使用策略模式,可以轻松实现扩展。(2)当一个类(策略客户方)依赖另外一个接口(策略服务方接口),而这个接口有多个实现类(策略服务方实现类),调用这些实现类对象比较复杂时,就可以增加一个类(策略代理方),将这个类和这些对象隔离开来,也就是策略模式。

角色(Role)

角色,一般为设计模式出现的类,或者对象。每种角色有自己的职责。

在策略模式中,包含三种角色,策略服务方角色,策略客户方角色,策略代理方角色。

策略服务方角色(strategy server role):策略服务方,在定义里面就是算法,也就是真正干事情的类,实现方式通常是一个策略接口,多个策略实现类,这些策略实现类因为执行了同样的接口,所以实现了可相互替换。它的职责只有一个,就是执行具体的策略,或者叫实现具体的算法。

策略客户方角色(strategy client role):策略客户方,在定义里面就是算法的客户,也就是真正需要使用算法执行结果的角色。在策略模式中,非常重要的一点,就是策略客户方,虽然依赖策略服务方,但是不会直接调用策略服务方对象的方法,而是通过策略代理方进行调用。它的职责如下:

  1. 封装业务数据,发送给策略代理方

  2. 选择具体的策略服务方,并告知策略代理方

大家要注意一下,这个模式的一个非常重要的一点,就是谁来决定使用哪个策略,一般大部分人会想当然的认为,应该是策略代理方来做这个决定,但是从原著里面看,而且作者反复强调,是策略客户方角色做这个决定,这一点大家要非常注意。

策略代理方角色(strategy delegate role):策略代理方,说实话,给这个角色起这个名字,我犹豫了很久,想了很多备选名称,如策略上下文(strategy context,按照原著里面的讲解,这个最合适、最贴切,但是上下文这个名词,太技术,不形象,不符合我的行文风格和命名习惯),策略执行环境(context,有时候也可以翻译为环境),策略委托方(delegate这个单词,它的中文翻译有代表,委托,代理的含义),经过再三考虑,还是觉得用策略代理方,比较形象,大家生活中也比较熟悉这个词,但它的缺点是容易让人联想到代理模式。

策略代理方,在定义里面并没有直接体现,但它是这个模式的灵魂,在这个模式中,它具有非常重要的地位,属于这个模式的核心类。

它的职责如下所示:

  1. 接收策略客户方的业务请求

  2. 接收策略客户指定的具体策略服务方对象

  3. 将客户方的请求转发给策略服务方对象

  4. 在转发请求的时候,还可以扩展请求数据,提供策略服务自身所需要的其他数据。

在促销业务案例中的第四版代码中。

策略服务方角色对应的接口和类如下:

IPromotionStrategy:促销策略接口,所有促销策略的统一接口。

CachBackPromotionStrategy:满减促销策略类

BuyMorePromotionStrategy:多买优惠促销策略类

ForPlusPromotionStrategy:plus会员优惠促销策略类

NonePromotionStrategy:无促销策略类

每种促销策略实现类,都实现了一种促销策略的算法。

策略客户方角色对应的接口和类如下:

ISettlementService :结算服务接口

SetlementServiceV4:结算服务实现类

如上图所示:

SetlementServiceV4结算服务实现类,它作为策略客户方,完成了它的两个职责:

1,通过switch语句,选择具体的促销策略。

2,调用策略代理方对象,执行策略,并且传递了执行策略所需的业务数据(CartItem,Member)

策略代理方角色对应的接口和类如下:

IPromotionService:促销服务接口

PromotionService:促销服务实现类。

PromotionService,促销服务实现类,作为策略代理方,从上面的代码中看,它确实挺简单的,就是做了一下请求转发,直接调用了促销策略对象的方法。那是因为我们的案例比较简单。这个对象里面其实还可以有很多事情可以干,比较常见的就是,执行具体的策略算法,除了上游传过来的业务数据,可能还需要该算法需要的特定数据,这些都需要策略代理方自己去封装。如果没有策略代理类,那么这些事情,就只能由策略客户方来解决了。这也是这个模式之所以产生的根本原因。

交互(interaction)

交互,是指设计模式中,各种角色是如何交互的,一般用UML中的序列图,活动图来表示。简单的说就是角色之间是如何配合,完成设计模式的使命的。

策略模式,交互还是比较复杂的,我们看这个交互图。

基本流程如下:

1,策略客户方角色(SetlementServiceV4)根据业务需求,选择需要的策略服务对象,封装相应的业务数据(cartItem, member),将请求发送给策略代理方。

2,策略代理方角色(PromotionService),在接到策略客户方角色的请求之后,将客户的请求直接转发,或者根据策略服务的需求,对数据进行扩展,调用具体的策略执行对象(IPromotionStrategy,CachBackPromotionStrategy等促销策略实现类),执行策略。

3,策略代理方,将策略执行结果,返回给策略客户方。

综上所述,策略模式,包含三种角色,分别是策略服务方角色,策略客户方角色,策略代理方角色,策略模式的宗旨是通过策略代理方,将策略客户方和策略服务方解耦合。策略代理方是整个模式的核心,它向上接收策略客户方的业务请求和指派的具体策略服务方对象,向下转发策略客户方的业务请求,执行具体的策略服务方方法。

我再举一个生活的案例进行阐述。

假如你刚刚大学毕业,来北京找工作,人生地不熟,需要先找个地方住。你走进了一家房屋中介公司,把自己需要的房屋信息,告诉给中介公司的管理人员,中介公司的管理人员,把你的需求,转述给一个具体的业务员,业务员给你找具体的房子。在这个过程中,你就是策略客户方角色,中介管理人员就是策略代理方角色,具体的业务员,就是策略服务方角色。

效果(effect)

效果,使用该设计模式之后,达到了什么效果,有何意义,当然,也可以说说它的缺点,或者风险。

从我们前面的案例可以看出,策略模式达到了以下效果。

  1. 算法或者叫策略的可替代性。

  2. 算法与算法客户的解耦合。

  3. 明确了职责,决策的客户方,服务方,代理方,各司其职。

  4. 通过新增策略服务方角色的类,实现策略的扩展性。

策略模式的重点:如何传递数据

策略模式中,一个非常重要的地方就是数据传递问题。在整个交互中,策略客户提出需求,将业务数据传递给策略代理方,策略代理方调用具体的策略服务方对象,执行具体的策略。

在原著里面,提供了一种传递数据的方式,就是策略代理方把他自己传递给策略服务方,我在策略模式的通用代码中,也是这样做的,大家可以参考一下。

策略模式的疑问点:谁在做决策?

在策略模式中,到底是由谁来做决策,也就是由谁来决定使用哪个具体的策略服务对象,是策略客户方,还是策略代理方。

根据原著里面的说明:

The client of Composition specifies which Compositor should be used by installing the Compositor it desires into the Composition.

应该是由策略客户方做决定,应该执行哪个具体的策略服务对象。

但是根据我的理解,以及项目中的实际情况看,这个模式,也可以让策略代理方做决策,也可以两者(策略客户方和策略代理方)同时做决策,或者可以这样说,先由策略客户做决策,如果客户没有做决策,策略代理方可以根据配置,选择一个默认的策略服务对象执行。总结如下:

  1. 策略客户方做决策(默认方式)

  2. 策略代理方做决策(客户撒手不管了,由代理方全权负责)

  3. 策略客户方和策略代理方都可以做决策,策略客户优先级高些,当客户方没有做决策或者在出现意外情况时,策略代理方可以做决策。

通用类图和代码

下面我们看看策略模式的通用UML类图和代码。大家注意,我讲的和普通设计模式书籍上的,是不太一样的,看仔细了,原著和普通的设计模式书籍,没有体现策略客户方的重要性。

UML类图

接口及类:

StrategyClientRole

IStrategyDelegateRole

StrategyDelegateRole

IStrategyServerRole

StrategyServerRole1

StrategyServerRole2

UML序列图

StrategyClientRole

package com.geekarchitect.patterns.demo105;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 策略客户方
 * @author 极客架构师@吴念
 * @createTime 2022/3/30
 */
public class StrategyClientRole {
    private static final Logger LOG = LoggerFactory.getLogger(StrategyClientRole.class);
    /**
     * 根据业务需要选择合适的策略服务对象
     * @author: 极客架构师@吴念
     * @date: 2022/4/4
     * @param: []
     * @return: com.geekarchitect.patterns.demo105.IStrategyServerRole
     */
    public IStrategyServerRole chooseStrategyServer() {
        LOG.info("根据业务需要选择合适的策略服务对象");
        return new StrategyServerRole1();
    }
    /**
     * 客户业务方法
     * @author: 极客架构师@吴念
     * @date: 2022/4/4
     * @param: []
     * @return: void
     */
    public void clientMethod() {
        LOG.info("策略模式通用代码");
        IStrategyDelegateRole strategyDelegateRole = new StrategyDelegateRole();
        IStrategyServerRole strategyServerRole = chooseStrategyServer();
        strategyDelegateRole.setStrategyDelegate(strategyServerRole);
        strategyDelegateRole.doServer();
    }
}

IStrategyDelegateRole

package com.geekarchitect.patterns.demo105;

/**
 * 策略代理方接口
 * @author 极客架构师@吴念
 * @createTime 2022/3/30
 */
public interface IStrategyDelegateRole {
    /**
     *
     * @author: 极客架构师@吴念
     * @date: 2022/4/4
     * @param: [strategyServerRole]
     * @return: void
     */
    void setStrategyDelegate(IStrategyServerRole strategyServerRole);
    /**
     *
     * @author: 极客架构师@吴念
     * @date: 2022/4/4
     * @param: []
     * @return: void
     */
    void doServer();
}

StrategyDelegateRole

package com.geekarchitect.patterns.demo105;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author 极客架构师@吴念
 * @createTime 2022/3/30
 */
public class StrategyDelegateRole implements IStrategyDelegateRole {
    private static final Logger LOG = LoggerFactory.getLogger(StrategyDelegateRole.class);
    private IStrategyServerRole strategyProviderRole;

    @Override
    public void setStrategyDelegate(IStrategyServerRole strategyProviderRole) {
        this.strategyProviderRole = strategyProviderRole;
    }

    @Override
    public void doServer() {
        LOG.info("策略代理方:StrategyDelegateRole,调用策略服务对象,执行策略");
        strategyProviderRole.server(this);
    }
}

IStrategyServerRole

package com.geekarchitect.patterns.demo105;

/**
 * 策略服务方接口
 * @author 极客架构师@吴念
 * @createTime 2022/3/30
 */
public interface IStrategyServerRole {
    void server(IStrategyDelegateRole strategyDelegateRole);
}

StrategyServerRole1

package com.geekarchitect.patterns.demo105;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 策略服务方实现类
 * @author 极客架构师@吴念
 * @createTime 2022/3/30
 */
public class StrategyServerRole1 implements IStrategyServerRole {
    private static final Logger LOG = LoggerFactory.getLogger(StrategyServerRole1.class);
    @Override
    public void server(IStrategyDelegateRole strategyDelegateRole) {
        //策略1
        LOG.info("策略服务方:StrategyServerRole1,执行策略1");
    }
}

StrategyServerRole2

package com.geekarchitect.patterns.demo105;

import com.geekarchitect.patterns.demo103.CachBackPromotionStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 策略服务方类
 * @author 极客架构师@吴念
 * @createTime 2022/3/30
 */
public class StrategyServerRole2 implements IStrategyServerRole {
    private static final Logger LOG = LoggerFactory.getLogger(StrategyServerRole2.class);
    @Override
    public void server(IStrategyDelegateRole strategyDelegateRole) {
        //策略2
        LOG.info("策略服务方:StrategyServerRole2,执行策略2");
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值