策略模式

这是“每周学点设计模式”系列中的策略模式。读完之后,你应该能回答以下几个问题:

  • 了解策略模式要解决的主要问题
  • 举出常见的策略模式使用场景
  • 说出JDK中运用策略模式的实例

注意:在阅读下篇之前请自己思考习题部分的问题,练习和独立思考对掌握模式知识的效果非常重要。

(上)

0、一份程序设计需求规格说明

请编写程序对输入框中的内容进行有效性检验。例如身份证信息输入框必须输入有效的身份证号码,密码输入框必须由字符、数字和至少一个符号组成。有效 性检验通过Validator对象对输入框内容进行检查。对不同的国家,身份证和邮编遵循不同的验证规则,密码强度检验可由系统管理员进行配置。

是的,这份功能说明里最重要的两个关键词就是“规则”和“配置”。当你需要实现一组规则(算法),并且这些规则要求可配置和可扩展时,策略模式会是你的好帮手。

1、策略模式的抽象过程

简单的说,策略模式就是要应对规则变化和新规则加入对程序带来的影响。

 

对于面向对象程序设计如何实现呢?通过接口可以避免直接调用规则,从而使得业务变化不会对规则产生影响。同时,添加新规则也不会影响业务流程。

 

2、示例分析

回到开始的需求,对于Validator我们可以定义一个统一的接口,同时针对不同类型的验证实现具体的算法达到对扩展的要求。

当有输入时,Text控件会调用自己的Validator。每个Validator会根据自己的实现分别应用规则返回验证结果。

  • 控件验证逻辑只与Validator接口相关:实现了业务与规则的隔离
  • 只需实现Validator接口即可添加新规则

3、习题

#1.请说明策略模式符合哪些面向对象设计原则?

提示:面向对象的5个基本设计原则有单一职责原则、开放封闭原则、Liskov替换原则、依赖倒置原则和接口隔离原则。

#2.请说明策略模式有什么优点和缺点。

#3.请用说明下列Java API设计中是如何使用策略模式的?

  • Java.util.Collections#sort(List list, Comparator < ? super T > c)
  • java.util.Arrays#sort(T[], Comparator < ? super T > c)
  • Java正则表达式Pattern
  • ThreadPoolExecutor中的ThreadFactory和RejectedExecutionHandler
  • hashmap中对hashcode的计算
  • runnable与executor
  • recursive和forkjoin

#4.分析SWT中的RowLayout、FormLayout、GridLayout是如何运用设略模式的?

 (下)

#1.请说明策略模式符合哪些面向对象设计原则?

让我们再回顾一下策略模式:

 

  •  策略模式中,通过接口(interface)实现了业务与具体实现的隔离。所以,策略模式符合“面向接口,而不是面向实现”的编程原则。
  • 在实现具体的策略(算法)时,通过实现接口可以方便地增加多个具体实现。所以,策略模式符合“开放封闭原则,对扩展开放,对修改封闭”。
  • 业务代码(Context)只依赖规则接口(Strategy),不依赖具体实现。所以,策略模式符合“依赖倒置原则,高层(业务模块)不依赖底层模块(具体算法),只依赖抽象接口”。

#2.请说明策略模式有什么优点和缺点。

  • 优点:遵循面向对象设计原则,降低设计耦合、便于扩展。
  • 缺点:当实现算法较多时,会增加需要维护的类的数量。可以使用工厂方法来解决。

#3.请用说明下列Java API设计中是如何使用策略模式的?

Java实例1. 集合、数组排序(sort)

实际编程中最常见的莫过于排序了。在Java API中,Collections.sort是常用方法之一(JDK7代码):

1

2

3

4

5

6

7

8

9

public static <T> void sort(List<T> list, Comparator<? super T> c) {

    Object[] a = list.toArray();

    Arrays.sort(a, (Comparator)c);

    ListIterator i = list.listIterator();

    for (int j=0; j<a.length; j++) {

        i.next();

        i.set(a[j]);

    }

}

这里调用了Arrays.sort

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public static <T> void sort(T[] a, Comparator<? super T> c) {

    if (LegacyMergeSort.userRequested)

        legacyMergeSort(a, c);

    else

        TimSort.sort(a, c);

}

 

/** To be removed in a future release. */

private static <T> void legacyMergeSort(T[] a, Comparator<? super T> c) {

    T[] aux = a.clone();

    if (c==null)

        mergeSort(aux, a, 0, a.length, 0);

    else

        mergeSort(aux, a, 0, a.length, 0, c);

}

这里是如何运用策略模式的呢?

 

  • Context:是实际调用排序的代码,即Collections的类。可以看到,默认使用了TimSort进行排序,排序的算法与对象的compare具体实现无关。
  • Strategy:即具体的对象比较接口,Comparator。
  • ConcreteStrategy:具体的比较算法,通过Comparator实现。例如实现大小写敏感、大小写不敏感或任意规则的比对。

Java实例2.正则表达式

正则表达式也是日常开发常用的工具,让我们来看看Pattern, Matcher是如何运用策略模式的。下面以一段简单的正则调用代码为例:

1

2

3

4

5

6

7

8

Pattern pattern = Pattern.compile("ab", Pattern.CASE_INSENSITIVE);

Matcher matcher = pattern.matcher("ABcabdAb");

// using Matcher find(), group(), start() and end() methods

while (matcher.find()) {

    System.out.println("Found the text \"" + matcher.group()

            + "\" starting at " + matcher.start()

            + " index and ending at index " + matcher.end());

}

可以看到,上面的示例中对字符串”ABcabdAb”用”ab”进行大小写不敏感匹配。

 

  • Context:是实际调用正则表达式匹配的代码,即Matcher类。可以看到,Matcher.find()实际执行了正则匹配并返回匹配结果。
  • Strategy:即匹配的接口。这里并没有使用Java的interface,而是通过Pattern构造函数进行了传递,只要填入了Pattern(String, int),正则表达式和匹配参数,即实现了匹配需要的接口。
  • ConcreteStrategy:具体的匹配算法,pattern字符串需要符合Java正则表达式的语法规则,而匹配的参数flags由Pattern中的常量定义。

Java实例3.线程池

在Java中,线程池的使用也十分常见。让我们看下面这段简单的示例:

1

2

3

4

5

6

7

8

9

10

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 60,

        TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10),

        new ThreadPoolExecutor.CallerRunsPolicy());

for (int i = 0; i < 10; i++) {

    System.out.println("add job_" + i + " at:" + new Date());

    SimplePrintJob job = new SimplePrintJob("job_" + i);

    threadPool.execute(job);

}

System.out.println("execute done!");

threadPool.shutdown();

在构造ThreadPoolExecutor时使用了两个参数,排队策略和拒绝任务的处理策略。其中排队策略可以方便地扩展,比如实现自定义策略。而任务的拒绝处理策略。

 

  • Context:ThreadPoolExecutor实际执行的方法execute()。按照用户设定的排队策略和拒绝任务的处理策略执行。
  • Strategy:这里排队策略和拒绝任务的处理策略提供了各自的接口,分别是BlockingQueue<E>和RejectedExecutionHandler。
  • ConcreteStrategy:具体的排队策略和拒绝任务的处理策略,JDK中都提供了默认的实现方法。可以实现接口自定义策略进行扩展。

Java实例4.ForkJoin框架

Java7中引入了ForkJoin框架,对多线程开发带来了很大的便利。关于ForkJoin框架的介绍可参见这篇文章

ForkJoin框架通过线程并发实现了对可以分治的问题提供了统一的抽象(业务)。开发者只需继承RecursiveAction或RecursiveTask,实现对应的abstract方法,就可以高效地实现问题求解。

下面让我们来看看ForkJoin框架是如何应用策略模式:

 

  • Context:ForkJoinPool实际执行的方法invoke()。按照用户提供的算法实现并发执行任务。
  • Strategy:ForkJoin框架通过RecursiveTask、RecursiveAction抽象类提供策略接口。
  • ConcreteStrategy:具体的实现,比如计算斐波那契数列、排序等,可以在compute方法中实现分治拆分后的计算。

Java实例5. SWT Layout

在图形框架中,支持多种类型的布局是基本功能。对Java来说,SWT就是一个很好的示例。

 

SWT FillLayout

 

SWT RowLayout

不同的布局方式,需要提供一种方便且可扩展的设计。让我们来看看SWT中对不同Layout是如何设计的:

 

  • Context:SWT的抽象元素Composite实际执行的方法computerSize()。按照用户设定的布局计算大小进行绘制。
  • Strategy:SWT通过抽象类Layout提供策略接口。
  • ConcreteStrategy:具体的实现,比如FillLayout、RowLayout等,根据自身的计算方法计算布局。

总结

在策略模式上篇,介绍设计模式的基本结构、要解决的问题并列举了Java中策略模式使用的几个示例。下篇中,对具体的示例进行了分析,主要了解了:

  • 排序工具类Collections中sort的基本实现:通过比较器comparator实现不同的对象比较策略。
  • 正则表达式的Pattern实现:通过符合语法规则的正则表达式,以及匹配参数,实现不同的匹配策略。
  • 线程池ThreadPoolExecutor实现:通过提供不同的排队策略和拒绝策略,在线程池ThreadPoolExecutor的管理中,提供不同的管理策略。
  • ForkJoin框架的ForkJoinPool:通过提供并发任务线程池管理,对可分治解决的计算任务,开发者只需通过继承RecursiveAction或RecursiveTask即可实现不同的算法。
  • 图形框架SWT Layout:对于需要实现多种不同布局的图形元素,通过Layout接口即可满足各种需求的布局策略。

可以看到,策略模式中的Strategy在Java中既可以是interface,也可以是 abstract class,甚至可以是构造函数(比如正则表达式的Pattern)。但无论实现的方式如何,要解决的核心问题还是将业务实现与具体的算法(策略)分离。

本篇的示例只是Java框架、类、库中很少的一部分。目的希望在了解策略模式的同时,可以发现身边的策略模式实例,能够认出并说出为什么要采用策略模式。以此作为抛砖引玉,欢迎大家交流讨论。

 

posted @ 2016-01-07 13:38  marvinYu  阅读(130)  评论(0)    收藏  举报