策略模式

策略模式(Strategy Pattern)是指定义了算法家族、分别封装起来,让它们之间可以互 相替换,此模式让算法的变化不会影响到使用算法的用户。

策略模式的应用场景

1、假如系统中有很多类,而他们的区别仅仅在于他们的行为不同。
2、一个系统需要动态地在几种算法中选择一种。

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

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

/**
 * 促销策略抽象
 * Created by Tom
 */
public interface PromotionStrategy {
    void doPromotion();
}

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

public class CouponStrategy implements PromotionStrategy {

    public void doPromotion() {
        System.out.println("领取优惠券,课程的价格直接减优惠券面值抵扣");
    }
}

CashbackStrategy类:

public class CashbackStrategy implements PromotionStrategy {

    public void doPromotion() {
        System.out.println("返现促销,返回的金额转到支付宝账号");
    }
}

GroupbuyStrategy类:

public class GroupbuyStrategy implements PromotionStrategy{

    public void doPromotion() {
        System.out.println("拼团,满20人成团,全团享受团购价");
    }
}

EmptyStrategy类:

public class EmptyStrategy implements PromotionStrategy {
    public void doPromotion() {
        System.out.println("无促销活动");
    }
}

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

public class PromotionActivity {
    private PromotionStrategy promotionStrategy;

    public PromotionActivity(PromotionStrategy promotionStrategy) {
        this.promotionStrategy = promotionStrategy;
    }

    public void execute(){
        promotionStrategy.doPromotion();
    }

}

编写客户端测试类:

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

        activity618.execute();
        activity1111.execute();
    }
}

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

public class PromotionActivityTest {
    public static void main(String[] args) {
        PromotionActivity promotionActivity = null;

        String promotionKey = "COUPON";

        if(StringUtils.equals(promotionKey,"COUPON")){
            promotionActivity = new PromotionActivity(new CouponStrategy());
        }else if(StringUtils.equals(promotionKey,"CASHBACK")){
            promotionActivity = new PromotionActivity(new CashbackStrategy());
        }//......
        promotionActivity.execute();
    }
}

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

/**
 * 促销策略工厂
 */
public class PromotionStrategyFactory {
    private static Map<String,PromotionStrategy> PROMOTION_STRATEGY_MAP = new HashMap<String, PromotionStrategy>();
    static {
        PROMOTION_STRATEGY_MAP.put(PromotionKey.COUPON,new CouponStrategy());
        PROMOTION_STRATEGY_MAP.put(PromotionKey.CASHBACK,new CashbackStrategy());
        PROMOTION_STRATEGY_MAP.put(PromotionKey.GROUPBUY,new GroupbuyStrategy());
    }

    private static final PromotionStrategy NON_PROMOTION = new EmptyStrategy();

    private PromotionStrategyFactory(){}

    public static PromotionStrategy getPromotionStrategy(String promotionKey){
        PromotionStrategy promotionStrategy = PROMOTION_STRATEGY_MAP.get(promotionKey);
        return promotionStrategy == null ? NON_PROMOTION : promotionStrategy;
    }

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


}

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

    public static void main(String[] args) {
        String promotionKey = "GROUPBUY";
        PromotionActivity promotionActivity = new PromotionActivity(PromotionStrategyFactory.getPromotionStrategy(promotionKey));
        promotionActivity.execute();
    }

代码优化之后,是不是我们程序猿小哥哥的维护工作就轻松了?每次上新活动,不影响原来的代码逻辑。为了加深对策略模式的理解,我们再来举一个案例。相信小伙伴们都用过支付宝、微信支付、银联支付以及京东白条。一个常见的应用场景就是大家在下单支付时会提示选择支付方式,如果用户未选,系统也会默认好推荐的支付方式进行结算。来看一下类图,下面我们用策略模式来模拟此业务场景:



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

public abstract class Payment {

    //支付类型
    public abstract String getName();

    //查询余额
    protected abstract double queryBalance(String uid);

    //扣款支付
    public MsgResult pay(String uid, double amount) {
        if(queryBalance(uid) < amount){
            return new MsgResult(500,"支付失败","余额不足");
        }
        return new MsgResult(200,"支付成功","支付金额:" + amount);
    }


}

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

public class AliPay extends Payment {

    public String getName() {
        return "支付宝";
    }

    protected double queryBalance(String uid) {
        return 900;
    }

}

京东白条JDPay类:

public class JDPay extends Payment {

    public String getName() {
        return "京东白条";
    }

    protected double queryBalance(String uid) {
        return 500;
    }
}

微信支付WechatPay类:

public class WechatPay extends Payment {

    public String getName() {
        return "微信支付";
    }

    protected double queryBalance(String uid) {
        return 256;
    }

}

银联支付UnionPay类:

public class UnionPay extends Payment {

    public String getName() {
        return "银联支付";
    }

    protected double queryBalance(String uid) {
        return 120;
    }

}

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

public class MsgResult {
    private int code;
    private Object data;
    private String msg;

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

    public String toString(){
        return ("支付状态:[" + code + "]," + msg + ",交易详情:" + data);
    }
}

创建支付策略管理类:

public class PayStrategy {
    public static final String ALI_PAY = "AliPay";
    public static final String JD_PAY = "JdPay";
    public static final String UNION_PAY = "UnionPay";
    public static final String WECHAT_PAY = "WechatPay";
    public static final String DEFAULT_PAY = ALI_PAY;

    private static Map<String,Payment> payStrategy = new HashMap<String,Payment>();
    static {
        payStrategy.put(ALI_PAY,new AliPay());
        payStrategy.put(WECHAT_PAY,new WechatPay());
        payStrategy.put(UNION_PAY,new UnionPay());
        payStrategy.put(JD_PAY,new JDPay());
    }

    public static Payment get(String payKey){
        if(!payStrategy.containsKey(payKey)){
            return payStrategy.get(DEFAULT_PAY);
        }
        return payStrategy.get(payKey);
    }
}

创建订单Order类:

public class Order {
    private String uid;
    private String orderId;
    private double amount;

    public Order(String uid,String orderId,double amount){
        this.uid = uid;
        this.orderId = orderId;
        this.amount = amount;
    }

    //完美地解决了switch的过程,不需要在代码逻辑中写switch了
    //更不需要写if    else if
    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 PayStrategyTest {

    public static void main(String[] args) {

        //省略把商品添加到购物车,再从购物车下单
        //直接从点单开始
        Order order = new Order("1","20180311001000009",324.45);

        //开始支付,选择微信支付、支付宝、银联卡、京东白条、财付通
        //每个渠道它支付的具体算法是不一样的
        //基本算法固定的

        //这个值是在支付的时候才决定用哪个值
        System.out.println(order.pay(PayStrategy.ALI_PAY));
        

    }

}

运行结果:



希望通过大家耳熟能详的业务场景来举例,让小伙伴们更深刻地理解策略模式。希望小伙伴们在面试和工作体现出自己的优势。

策略模式在JDK源码中的体现

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

public interface Comparator<T> { int compare(T o1, T o2); ... }

Comparator抽象下面有非常多的实现类,我们经常会把Comparator 作为参数传入作为排序策略,例如Arrays类的parallelSort 方法等:

public class Arrays { 
... 
  public static <T> void parallelSort(T[] a, int fromIndex, int toIndex, Comparator<? super T> cmp) { ... } 
... 
} 

还有TreeMap的构造方法:

public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable { 
... 
public TreeMap(Comparator<? super K> comparator) { this.comparator = comparator; } 
... 
} 

这就是 Comparator 在JDK 源码中的应用。那我们来看策略模式在Spring源码中的应用,来看Resource类:

/*
 * Copyright 2002-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.core.io;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;

import org.springframework.lang.Nullable;

/**
 * Interface for a resource descriptor that abstracts from the actual
 * type of underlying resource, such as a file or class path resource.
 *
 * <p>An InputStream can be opened for every resource if it exists in
 * physical form, but a URL or File handle can just be returned for
 * certain resources. The actual behavior is implementation-specific.
 *
 * @author Juergen Hoeller
 * @since 28.12.2003
 * @see #getInputStream()
 * @see #getURL()
 * @see #getURI()
 * @see #getFile()
 * @see WritableResource
 * @see ContextResource
 * @see UrlResource
 * @see ClassPathResource
 * @see FileSystemResource
 * @see PathResource
 * @see ByteArrayResource
 * @see InputStreamResource
 */
public interface Resource extends InputStreamSource {

    /**
     * Determine whether this resource actually exists in physical form.
     * <p>This method performs a definitive existence check, whereas the
     * existence of a {@code Resource} handle only guarantees a valid
     * descriptor handle.
     */
    boolean exists();

    /**
     * Indicate whether the contents of this resource can be read via
     * {@link #getInputStream()}.
     * <p>Will be {@code true} for typical resource descriptors;
     * note that actual content reading may still fail when attempted.
     * However, a value of {@code false} is a definitive indication
     * that the resource content cannot be read.
     * @see #getInputStream()
     */
    default boolean isReadable() {
        return true;
    }

    /**
     * Indicate whether this resource represents a handle with an open stream.
     * If {@code true}, the InputStream cannot be read multiple times,
     * and must be read and closed to avoid resource leaks.
     * <p>Will be {@code false} for typical resource descriptors.
     */
    default boolean isOpen() {
        return false;
    }

    /**
     * Determine whether this resource represents a file in a file system.
     * A value of {@code true} strongly suggests (but does not guarantee)
     * that a {@link #getFile()} call will succeed.
     * <p>This is conservatively {@code false} by default.
     * @since 5.0
     * @see #getFile()
     */
    default boolean isFile() {
        return false;
    }

    /**
     * Return a URL handle for this resource.
     * @throws IOException if the resource cannot be resolved as URL,
     * i.e. if the resource is not available as descriptor
     */
    URL getURL() throws IOException;

    /**
     * Return a URI handle for this resource.
     * @throws IOException if the resource cannot be resolved as URI,
     * i.e. if the resource is not available as descriptor
     * @since 2.5
     */
    URI getURI() throws IOException;

    /**
     * Return a File handle for this resource.
     * @throws java.io.FileNotFoundException if the resource cannot be resolved as
     * absolute file path, i.e. if the resource is not available in a file system
     * @throws IOException in case of general resolution/reading failures
     * @see #getInputStream()
     */
    File getFile() throws IOException;

    /**
     * Return a {@link ReadableByteChannel}.
     * <p>It is expected that each call creates a <i>fresh</i> channel.
     * <p>The default implementation returns {@link Channels#newChannel(InputStream)}
     * with the result of {@link #getInputStream()}.
     * @return the byte channel for the underlying resource (must not be {@code null})
     * @throws java.io.FileNotFoundException if the underlying resource doesn't exist
     * @throws IOException if the content channel could not be opened
     * @since 5.0
     * @see #getInputStream()
     */
    default ReadableByteChannel readableChannel() throws IOException {
        return Channels.newChannel(getInputStream());
    }

    /**
     * Determine the content length for this resource.
     * @throws IOException if the resource cannot be resolved
     * (in the file system or as some other known physical resource type)
     */
    long contentLength() throws IOException;

    /**
     * Determine the last-modified timestamp for this resource.
     * @throws IOException if the resource cannot be resolved
     * (in the file system or as some other known physical resource type)
     */
    long lastModified() throws IOException;

    /**
     * Create a resource relative to this resource.
     * @param relativePath the relative path (relative to this resource)
     * @return the resource handle for the relative resource
     * @throws IOException if the relative resource cannot be determined
     */
    Resource createRelative(String relativePath) throws IOException;

    /**
     * Determine a filename for this resource, i.e. typically the last
     * part of the path: for example, "myfile.txt".
     * <p>Returns {@code null} if this type of resource does not
     * have a filename.
     */
    @Nullable
    String getFilename();

    /**
     * Return a description for this resource,
     * to be used for error output when working with the resource.
     * <p>Implementations are also encouraged to return this value
     * from their {@code toString} method.
     * @see Object#toString()
     */
    String getDescription();

}

我们虽然没有直接使用Resource类,但是我们经常使用它的子类,例如:



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

public interface InstantiationStrategy {

    /**
     * Return an instance of the bean with the given name in this factory.
     * @param bd the bean definition
     * @param beanName the name of the bean when it's created in this context.
     * The name can be {@code null} if we're autowiring a bean which doesn't
     * belong to the factory.
     * @param owner the owning BeanFactory
     * @return a bean instance for this bean definition
     * @throws BeansException if the instantiation attempt failed
     */
    Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner)
            throws BeansException;

    /**
     * Return an instance of the bean with the given name in this factory,
     * creating it via the given constructor.
     * @param bd the bean definition
     * @param beanName the name of the bean when it's created in this context.
     * The name can be {@code null} if we're autowiring a bean which doesn't
     * belong to the factory.
     * @param owner the owning BeanFactory
     * @param ctor the constructor to use
     * @param args the constructor arguments to apply
     * @return a bean instance for this bean definition
     * @throws BeansException if the instantiation attempt failed
     */
    Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner,
            Constructor<?> ctor, @Nullable Object... args) throws BeansException;

    /**
     * Return an instance of the bean with the given name in this factory,
     * creating it via the given factory method.
     * @param bd the bean definition
     * @param beanName the name of the bean when it's created in this context.
     * The name can be {@code null} if we're autowiring a bean which doesn't
     * belong to the factory.
     * @param owner the owning BeanFactory
     * @param factoryBean the factory bean instance to call the factory method on,
     * or {@code null} in case of a static factory method
     * @param factoryMethod the factory method to use
     * @param args the factory method arguments to apply
     * @return a bean instance for this bean definition
     * @throws BeansException if the instantiation attempt failed
     */
    Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner,
            @Nullable Object factoryBean, Method factoryMethod, @Nullable Object... args)
            throws BeansException;

}

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



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

策略模式的优缺点

优点:
1、策略模式符合开闭原则。
2、避免使用多重条件转移语句,如if...else...语句、switch语句
3、使用策略模式可以提高算法的保密性和安全性。
缺点:
1、客户端必须知道所有的策略,并且自行决定使用哪一个策略类。
2、代码中会产生非常多策略类,增加维护难度。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,640评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,254评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,011评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,755评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,774评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,610评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,352评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,257评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,717评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,894评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,021评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,735评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,354评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,936评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,054评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,224评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,974评论 2 355

推荐阅读更多精彩内容

  • 策略模式 在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模...
    javacoo阅读 317评论 0 1
  • 排序问题: 当要比较的是整数,浮点数,都需要不同的排序方法,并且当改变了要比较的东西时要重新排序,这样不好, 怎样...
    tracy_668阅读 316评论 0 2
  • 策略模式(Strategy Pattern)也叫做政策模式(Policy Pattern),是一种行为型模式。 一...
    l只为终点阅读 610评论 1 3
  • 本文的主要内容: 介绍策略模式 示例商场购物打折策略的实现 策略模式总结 源码分析策略模式的典型应用Java Co...
    小旋锋的简书阅读 1,362评论 0 1
  • 前言 最近段时间,接到一个需求:开发一个聚合支付服务,对其他内部项目提供统一的接口来实现不同支付平台的支付能力发起...
    闻人的技术博客阅读 899评论 0 5