【深入设计模式】策略模式—策略模式详解及策略模式在源码中的应用

生活中我们经常会遇到选择问题,比如当我们要出去旅游时,会考虑是自驾、坐飞机还是坐火车前往目的地;或者在烹饪一条鱼时,是考虑清蒸、水煮还是烧烤;又或者商家在对商品促销时,是使用会员累计积分、打折促销或者买赠的方式进行促销。这个时候就需要根据当前不同的的条件,来选择出对应的具体实现方式,这就是策略模式。在实际开发中,策略模式也是会经常使用的一种设计模式。在实现某个功能有多种方式可供选择时,策略模式就能派上用场。

1. 策略模式

1.1 策略模式简介

不知道在座的各位有没有在维护项目代码时,看到过大段大段的 if else 语句,本人曾有幸遇到过一个方法里面大量 if else 嵌套,并且每一个代码块都很长。这种代码通常是第一版开发时判断分支比较少,就是用 if else 来进行处理,随着版本迭代,功能需求的增加,后面为了快速迭代就直接在原来的 if else 语句基础上继续添加判断分支,久而久之就嵌套出了大量的判断分支。这样写法虽然开发的人写着快,但是对于后面代码维护或者新人阅读代码是非常不友好,甚至感到崩溃的。那么当我们在开发中发现判断分支开始膨胀时,这个时候就可以考虑使用策略模式来进行处理。

策略模式定义了一系列功能的实现,而这些功能实现的目的是相同的,能够使用相同的方式来调用所有的实现,只是调用时根据传入不同参数从而获取到不同的实现。使用这样的方式将方法调用和功能实现进行分割,从而达到具体策略之间相互独立,修改、新增策略实现时,不会对策略调用方和其他策略产生影响。

1.2 策略模式结构

在简单了解了策略模式之后,我们来看看他的结构。

策略模式中需要定义一个策略接口 Strategy,使用具体策略类实现该接口来封装具体的策略实现过程。同时还需要给调用方提供一个管理 Strategy 配置类 Context,调用方通过 Context 来调用具体的策略。

/**
 * 策略接口
 */
public interface Strategy {
    void strategyMethod();
}
/**
 * 具体策略 1
 */
public class SpecificStrategy1 implements Strategy {
    @Override
    public void strategyMethod() {
    }
}
/**
 * 具体策略 2
 */
public class SpecificStrategy2 implements Strategy {
    @Override
    public void strategyMethod() {

    }
}

/**
 * Strategy 配置管理类 context
 */
public class Context {
    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public void strategyMethod() {
        strategy.strategyMethod();
    }
}

调用方代码

public static void main(String[] args) {
    // 调用具体策略 1
    Context strategyContext1 = new Context(new SpecificStrategy1());
    strategyContext1.strategyMethod();

    // 调用具体策略 2
    Context strategyContext2 = new Context(new SpecificStrategy2());
    strategyContext2.strategyMethod();
}

我们可以看到在这样的结构下,调用方只需要在构建 Context 的时候传入具体的策略实现就可以了,调用方也不会在关心具体是怎么实现的。如果要新增策略实现方式,则新增实现类即可,同样修改实现也只需修改对应的实现类。

1.3 策略模式示例

前面对策略模式的概念和结构进行了介绍,可能还是会感觉有点云里雾里,下面就用具体示例来加深理解。

场景模拟:假设你现在有一条鱼准备烹饪,烹饪的方式有清蒸、烤鱼两种做法,那么我们就可以使用策略模式来得到一条烹饪完成的鱼。

首先我们定义一个烹饪策略类(CookStrategy)并定义 cookFish 方法,然后定义两个具体的策略实现类 SteamedFish (水煮鱼)和 GrillFish (烤鱼)。根据结构定义我们还需要给调用方提供一个 CookContext 类来管理具体的策略和方法调用,代码如下:

// 烹饪策略类
public interface CookStrategy {
    void cookFish();
}
// 清蒸鱼
public class SteamedFish implements CookStrategy {
    @Override
    public void cookFish() {
        System.out.println("begin to cook steamed fish.");
        System.out.println("ding! you have a steamed fish.");
    }
}
// 烤鱼
public class GrillFish implements CookStrategy {
    @Override
    public void cookFish() {
        System.out.println("begin to grill fish.");
        System.out.println("ding! you have a grill fish.");
    }
}
// 策略管理 Context
public class CookContext {

    private CookStrategy cookStrategy;

    public CookContext(CookStrategy cookStrategy) {
        this.cookStrategy = cookStrategy;
    }

    public void cookFish() {
        cookStrategy.cookFish();
    }
}

在定义好烹饪方式策略和策略管理之后,就是编写调用方的调用代码了,当我们想做一条清蒸鱼时,只需在 CookContext 的构造方法里面传入具体的清蒸鱼策略即可

public static void main(String[] args) {
    CookContext cookContext = new CookContext(new SteamedFish());
    cookContext.cookFish();
    // 控制台输出
    // begin to cook steamed fish.
    // ding! you have a steamed fish.
}

同理,当我们想做一条烤鱼时也是传入具体的烤鱼策略

public static void main(String[] args) {
    CookContext cookContext = new CookContext(new GrillFish());
    cookContext.cookFish();
    // 控制台输出
    // begin to grill fish.
    // ding! you have a grill fish.
}

那么有人可能会有疑问,如果我们添加添加了更多的烹饪方式比如酸菜鱼、水煮鱼等等,那么方式越来越多,客户端所管理的策略也会越来越多,而我们的的具体策略选择不就又回到了调用者身上了吗?这个时候就要使用策略模式的扩展——策略工厂了。

2. 策略工厂

2.1 减轻客户端的负担

当我们添加的烹饪鱼的方式越来越多的时候就需要根据条件来选择具体的烹饪方式,客户端调用代码就会变成:

public static void main(String[] args) {
    CookContext cookContext = null;
    String cook = "grill";
    if ("grill".equals(cook)) {
        cookContext = new CookContext(new GrillFish());
    } else if ("steamd".equals(cook)) {
        cookContext = new CookContext(new SteamedFish());
    } else if ("shuizhu".equals(cook)) {
        cookContext = new CookContext(new ShuizhuStrategy());
    } else {
        cookContext = new CookContext(new SuancaiStrategy());
    }
    cookContext.cookFish();
}

现在能看到客户端的判断越来越复杂,因此结合简单工厂模式(可参考博客:【深入设计模式】工厂模式—简单工厂和工厂方法),将判断语句下沉到 Context 中,调用者便不在进行条件判断,而是只用传入参数即可。

2.2 策略工厂写法

Context 的代码如下:

public class CookContext {

    private CookStrategy cookStrategy;

    public CookContext(String key) {
        if ("grill".equals(key)) {
            cookStrategy = new GrillFish();
        } else if ("steamd".equals(key)) {
            cookStrategy = new SteamedFish();
        } else if ("shuizhu".equals(key)) {
            cookStrategy = new ShuizhuStrategy();
        } else {
            cookStrategy = new SuancaiStrategy();
        }
    }

    public void cookFish() {
        cookStrategy.cookFish();
    }
}

可以看到在 CookContext 的代码中,原来的构造方法参数从 Strategy 改成了具体 Strategy 对应的 key,当我们在构造 CookContext 的时候就会根据 key 构造出对应的具体 Strategy,因此调用者的代码就变成下面这样:

public static void main(String[] args) {
    String cook = "grill";
    CookContext cookContext = new CookContext(cook);
    cookContext.cookFish();
}

还有一种写法就是在调用 cookFish 时再根据 key 选择对应具体策略方法,而在构造 CookContext 时仅仅将所用到的策略根据 key 进行缓存,代码如下:

public class CookContext {

    private Map<String, CookStrategy> strategyMap;

    public CookContext() {
        strategyMap = new HashMap<>();
        strategyMap.put("grill", new GrillFish());
        strategyMap.put("steamd", new SteamedFish());
        strategyMap.put("shuizhu", new ShuizhuStrategy());
        strategyMap.put("suancai", new SuancaiStrategy());
    }

    public void cookFish(String key) {
        strategyMap.get(key).cookFish();
    }
}

对应的调用者代码也改成如下:

public static void main(String[] args) {
    String cook = "grill";
    CookContext cookContext = new CookContext();
    cookContext.cookFish(cook);
}

以上两种写法都是没问题的,主要根据个人习惯以及实际场景选择即可。

回到开篇提出的大量 if else 问题上,当我们遇到判断分支很多,并且每个分支逻辑复杂时,我们便可以使用策略工厂,将原来每个分支里面的业务代码进行策略封装,同时使用 Context 将判断条件和封装后的策略进行关联。这样做的好处是在将来如果再次新增判断分支时,只需新增策略类即可,调用者也不再与具体策略耦合。并且代码条理和责任会更清晰,每个分支只会关心自己对应的策略,对策略的修改也不会对调用方产生任何影响。

3. 策略模式在框架源码中的应用

3.1 策略模式在 JDK 中的应用

ThreadPoolExecutor 类

在我们创建线程池时,会调用 ThreadPoolExecutor 的构造函数 new 一个对象,在构造函数中需要传入七个参数,其中有一个参数叫 RejectedExecutionHandler handler 也就是线程的拒绝策略。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

传入拒绝策略之后将对象赋给 ThreadPoolExecutor 对象的成员变量 handler,在需要对加入线程池的线程进行拒绝时,直接调用 RejectedExecutionHandler 中的 reject 方法即可,方法内部调用传入 handler 的 rejectedExecution 方法。

final void reject(Runnable command) {
    handler.rejectedExecution(command, this);
}

但是 RejectedExecutionHandler 是一个接口,也就是说我们需要传入具体的实现,这里便是使用的策略模式。RejectedExecutionHandler 接口对应 Strategy 接口,下面四种实现类对应具体策略;RejectedExecutionHandler 对应 Context 类,外部调用 RejectedExecutionHandler 的 reject 方法,再由 RejectedExecutionHandler 内部调用具体策略实现的方法。

TreeMap

在创建 TreeMap 对象的时候可以在构造方法中传入 Comparetor 对象来决定 TreeMap 中的 key 是按照怎样的顺寻进行排列。并且 TreeMap 通过提供 compare 方法调用比较器的 compare 方法进行两个参数的比较,因此该处也是使用的策略模式。

public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

final int compare(Object k1, Object k2) {
    return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
        : comparator.compare((K)k1, (K)k2);
}

4. 总结

策略模式用于在完成相同工作时有多种不同实现的选择上,这些实现都能以相同的方式进行调用,减少方法实现和方法调用上的耦合。在实际开发中使用策略模式不仅能简化代码,而且能够简化我们的单元测试。策略模式将策略的选择交给了调用者,从而让具体策略仅关注自己的实现逻辑。

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