阅读 278

设计模式-策略模式学习之旅

“这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战

一、什么是策略模式?

策略模式(Strategy Pattern)也叫政策模式(Policy Pattern),它是将定义的算法家族、分别封装起来,让它们之间可以互相替换,从而让算法的变化不会影响到使用算法的用户。属于行为型模式。

策略模式使用的就是面向对象的继承和多态机制,从而实现同一行为在不同场景下具备不同实现

二、策略模式的应用场景

策略模式在生活场景中应用也非常多。比如一个人的交税比率与他的工资有关,不同的工资水平对应不同的税率。再比如我们在互联网移动支付的大背景下,每次下单后付款前,需要选择支付方式。

策略模式可以解决在有多种算法相似的情况下,使用if...else或switch...case所带来的复杂性和臃肿性。在日常业务开发中,策略模式适用于以下场景:

  1. 针对同一类型问题,有多种处理方式,每一种都能独立解决问题。
  2. 算法需要自由切换的场景。
  3. 需要屏蔽算法规则的场景。

首先来看下策略模式的通用UML类图:

image.png

从UML类图中,我们可以看到,策略模式主要包含三种角色:

  1. 上下文角色(Context):用来操作策略的上下文环境,屏蔽高层模块(客户端)策略,算法的直接访问,封装可能存在的变化。
  2. 抽象策略角色(Strategy):规定策略或算法的行为。
  3. 具体策略角色(ConcreteStrategy):具体的策略或算法实现。

注意:策略模式中的上下文环境(Context),其职责本来是隔离客户端与策略类的耦合,让客户端完全与上下文环境沟通,无需关系具体策略。

三、用策略模式实现促销优惠业务场景

大家都知道,电商平台经常会有优惠活动,优惠策略会有很多种活动,可能如:领取优惠券抵扣、返现促销、拼团优惠。下面我们用代码来模拟,首先我们创建一个促销策略的抽象PromotionStrategy:

public interface IPromotionStrategy {

    void promotion();
}
复制代码

然后分别创建优惠券抵扣策略CouponStrategy类、返现促销策略CashBackStrategy类、拼团优惠策略GroupBuyStrategy类和无优惠策略EmptyStrategy类:

CouponStratey类:

public class CouponStrategy implements IPromotionStrategy {
    @Override
    public void promotion() {
        System.out.println("使用优惠券抵扣");
    }
}
复制代码

CashBackStrategy类:

public class CashBackStrategy implements IPromotionStrategy {
    @Override
    public void promotion() {
        System.out.println("返现,直接打款到支付宝账号");
    }
}
复制代码

GroupBuyStrategy类:

public class GroupBuyStrategy implements IPromotionStrategy {
    @Override
    public void promotion() {
        System.out.println("5人拼团,可以优惠");
    }
}
复制代码

EmptyStrategy类:

public class EmptyStrategy implements IPromotionStrategy {
    @Override
    public void promotion() {
        System.out.println("无优惠");
    }
}
复制代码

然后创建促销活动方案PromotionActivity类:

public class PromotionActivity {

    private IPromotionStrategy strategy;

    public PromotionActivity(IPromotionStrategy strategy) {
        this.strategy = strategy;
    }

    public void execute() {
        strategy.promotion();
    }

}
复制代码

编写客户端测试类:

public class Test {

    public static void main(String[] args) {
        PromotionActivity activity618 = new PromotionActivity(new CouponStrategy());
        PromotionActivity activity1111 = new PromotionActivity(new CashBackStrategy());

        activity618.execute();
        activity1111.execute();
    }
}
复制代码

此时,小伙伴们会发现,如果把上面这段测试代码放到实际的业务场景下,其实并不实用。因为我们做活动时候往往是要根据不同的需求对促销策略进行动态选择的,并不会一次执行多种优惠。所以,我们的代码(简单模拟下哈!!!)通常会这样写:

public void test() {
        PromotionActivity activity = null;
        String promotionKey = "COUPON";

        if ("COUPON".equalsIgnoreCase(promotionKey)) {
            activity = new PromotionActivity(new CouponStrategy());
        } else if ("CASHBACK".equalsIgnoreCase(promotionKey)) {
            activity = new PromotionActivity(new CashBackStrategy());
        }
        //......
        activity.execute();
    }
复制代码

这样改造之后,满足了业务需求,客户可根据自己的需求选择不同的优惠策略了。但是,经过一段时间的业务积累,我们的促销活动会越来越多。于是,我们的程序猿小哥哥就忙不过来了,每次上活动之前都要通宵改代码,而且要做重复测试,判断逻辑可能也变得越来越复杂。这时候,我们是不是需要思考下代码应该重构了?回顾之前学过的设计模式,应该如何来优化这段代码呢?其实,我们可以结合单例模式和工厂模式。

简单模拟下哈,有更高级的写法!!!

创建PromotionStrategyFactory类:

public class PromotionStrategyFactory {

    private PromotionStrategyFactory() {
    }

    private static final Map<String, IPromotionStrategy> PROMOTIONS = new HashMap<>();
    private static final IPromotionStrategy EMPTY = new EmptyStrategy();

    static {
        PROMOTIONS.put(PromotionKey.COUPON, new CouponStrategy());
        PROMOTIONS.put(PromotionKey.CASHBACK, new CashBackStrategy());
        PROMOTIONS.put(PromotionKey.GROUPBUY, new GroupBuyStrategy());
    }

    public static IPromotionStrategy getPromotionStrategy(String promotionKey) {
        IPromotionStrategy strategy = PROMOTIONS.get(promotionKey);
        return strategy == null ? EMPTY : strategy;
    }

    private interface PromotionKey {
        String COUPON = "COUPON";
        String CASHBACK = "CASHBACK";
        String GROUPBUY = "GROUPBUY";
    }

    public static Set<String> getPromotionKeys() {
        return PROMOTIONS.keySet();
    }
}
复制代码

这时候我们客户端代码就应该这样写了:

public class Test {

    public static void main(String[] args) {
        String promotionKey = "COUPON";

        IPromotionStrategy strategy = PromotionStrategyFactory.getPromotionStrategy(promotionKey);
        strategy.promotion();
    }
}
复制代码

代码优化之后,是不是我们程序猿小哥哥的维护工作就轻松了?每次上新活动,不影响原来的代码逻辑。

四、用策略模式实现选择支付方式的业务场景

为了加深对策略模式的理解,我们再来举一个案例。相信小伙伴们都用过支付宝、微信支付、银联支付以及京东白条。一个常见的应用场景就是大家在下单支付时,提示选择支付方式,如果用户未选,系统也会默认选择推荐的支付方式进行结算。

创建Payment抽象类,定义支付规范和支付逻辑,代码如下:

/**
 * 支付渠道
 */
public abstract class Payment {

    public abstract String getName();

    //通用逻辑放到抽象类里面实现
    public MsgResult pay(String uid, double amount) {
        //余额是否足够
        if (queryBalance(uid) < amount) {
            return new MsgResult(500, "支付失败", "余额不足");
        }
        return new MsgResult(200, "支付成功", "支付金额" + amount);
    }

    protected abstract double queryBalance(String uid);
}
复制代码

分别创建具体的支付方式,支付宝AliPay类:

public class AliPay extends Payment {
    @Override
    public String getName() {
        return "支付宝";
    }

    @Override
    protected double queryBalance(String uid) {
        return 900;
    }
}
复制代码

京东白条JDPay类:

public class JDPay extends Payment {
    @Override
    public String getName() {
        return "京东白条";
    }

    @Override
    protected double queryBalance(String uid) {
        return 500;
    }
}
复制代码

微信支付WeChatPay类:

public class WeChatPay extends Payment {
    @Override
    public String getName() {
        return "微信支付";
    }

    @Override
    protected double queryBalance(String uid) {
        return 200;
    }
}
复制代码

银联支付UnionPay类:

public class UnionPay extends Payment {
    @Override
    public String getName() {
        return "银联支付";
    }

    @Override
    protected double queryBalance(String uid) {
        return 120;
    }
}
复制代码

创建支付状态的包装类MsgResult:

/**
 * 支付完成后的状态
 */
@Data
public class MsgResult {

    private int code;
    private Object data;
    private String msg;

    public MsgResult(int code, Object data, String msg) {
        this.code = code;
        this.data = data;
        this.msg = msg;
    }

}
复制代码

创建支付管理策略类:

/**
 * 支付策略管理
 */
public class PayStrategy {

    public static final String ALI_PAY = "AliPay";
    public static final String JD_PAY = "JdPay";
    public static final String WECHAT_PAY = "WeChatPay";
    public static final String UNION_PAY = "UnionPay";
    public static final String DEFAULT_PAY = ALI_PAY;

    private static final Map<String, Payment> STRATEGY = new HashMap<>();

    static {
        STRATEGY.put(ALI_PAY, new AliPay());
        STRATEGY.put(JD_PAY, new JDPay());
        STRATEGY.put(WECHAT_PAY, new WeChatPay());
        STRATEGY.put(UNION_PAY, new UnionPay());
    }

    public static Payment get(String payKey) {
        if (!STRATEGY.containsKey(payKey)) {
            return STRATEGY.get(DEFAULT_PAY);
        }
        return STRATEGY.get(payKey);
    }
}
复制代码

创建订单Order类:

@Data
@AllArgsConstructor
public class Order {

    private String uid;
    private String orderId;
    private double amount;

    public MsgResult pay() {
        return pay(PayStrategy.DEFAULT_PAY);
    }

    public MsgResult pay(String payKey) {
        Payment payment = PayStrategy.get(payKey);
        System.out.println("欢迎使用" + payment.getName());
        System.out.println("本次交易金额为:" + amount + ",开始扣款");
        return payment.pay(uid, amount);
    }
}
复制代码

测试代码:

public class Test {

    public static void main(String[] args) {
        Order order = new Order("1", "111", 666);
        System.out.println(order.pay(PayStrategy.DEFAULT_PAY));
    }
}
复制代码

运行结果:

image.png

此时,小伙伴们有没有更深刻地理解了策略模式怎么运用啦。

五、策略模式在框架源码中的体现

首先来看JDK中一个比较常用的比较器Comparator接口,我们看到的一个大家常用的compare()方法,就是一个策略抽象实现:

public interface Comparator<T> {

.......

    int compare(T o1, T o2);

......

}
复制代码

还要一个非常经典的场景,Spring的初始化也采用了策略模式,不同的类型的类采用不同的初始化策略。首先有一个InstantiationStrategy接口,我们来看一下源码:

public interface InstantiationStrategy {
    Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3) throws BeansException;

    Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3, Constructor<?> var4, Object... var5) throws BeansException;

    Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3, @Nullable Object var4, Method var5, Object... var6) throws BeansException;
}
复制代码

顶层的策略抽象非常简单,但是它下面有两种策略SimpleInstantiationStrategy和CglibSubclassingInstantiationStrategy,我们看一下类图:

image.png

打来类图我们还发现CglibSubclassingInstantiationStrategy策略类还继承了SimpleInstantiationStrategy类,说明在实际应用中多种策略之间还可以继续使用。小伙伴可以作为一个参考,在实际业务场景中,可以根据需要来设计。

六、策略模式的优缺点

优点:

  1. 策略模式符合开闭原则。
  2. 避免使用多重条件转移语句,如if...else...语句、switch语句。
  3. 使用策略模式可以提高算法的保密性和安全性。

缺点:

  1. 客户端必须知道所有的策略,并且自行决定使用哪一个策略类。
  2. 代码中会产生非常多策略类,增加维护难度。

七、友情链接

设计模式-工厂模式学习之旅

设计模式-单例模式学习之旅

设计模式-原型模式学习之旅

设计模式-建造者模式学习之旅

设计模式-代理模式学习之旅

设计模式-门面模式学习之旅

设计模式-装饰器模式学习之旅

设计模式-享元模式学习之旅

设计模式-组合模式学习之旅

设计模式-适配器模式学习之旅

设计模式-桥接模式学习之旅

设计模式-委派模式学习之旅

设计模式-模板方法模式学习之旅

欢迎大家关注微信公众号(MarkZoe)互相学习、互相交流。

文章分类
后端