设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的;设计模式使代码编制真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。
设计模式分为三种类型,共23种。
创建型模式(5):单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式。
结构型模式(7):适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
行为型模式(11):(父子类)策略模式、模版方法模式,(两个类)观察者模式、迭代器模式、职责链模式、命令模式,(类的状态)状态模式、备忘录模式,(中间类) 访问者模式、中介者模式、解释器模式。
一.概述
定义
Strategy Pattern(策略模式):定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。
Strategy Pattern:Define a family of algorithms, encapsulate each one, and make them interchangeable.
结构
策略模式结构并不复杂,但我们需要理解其中环境类Context的作用,其结构如图所示:
策略模式涉及到三个角色:
- 环境(Context)角色:环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。
- 抽象策略(Strategy)角色:它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。
- 具体策略(ConcreteStrategy)角色:它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。
策略模式是一个比较容易理解和使用的设计模式,策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。策略模式通常把一个系列的算法封装到一系列具体策略类里面,作为抽象策略类的子类。在策略模式中,对环境类和抽象策略类的理解非常重要,环境类是需要使用算法的类。在一个系统中可以存在多个环境类,它们可能需要重用一些相同的算法。
实现
在使用策略模式时,我们需要将算法从Context类中提取出来,首先应该创建一个抽象策略类,其典型代码如下所示:
public abstract class AbstractStrategy {
public abstract void algorithm(); //声明抽象算法
}
然后再将封装每一种具体算法的类作为该抽象策略类的子类,如下代码所示:
public class ConcreteStrategyA extends AbstractStrategy {
//算法的具体实现
public void algorithm() {
//算法A
}
}
其他具体策略类与之类似,对于Context类而言,在它与抽象策略类之间建立一个关联关系,其典型代码如下所示:
public class Context {
private AbstractStrategy strategy; //维持一个对抽象策略类的引用
public void setStrategy(AbstractStrategy strategy) {
this.strategy= strategy;
}
//调用策略类中的算法
public void algorithm() {
strategy.algorithm();
}
}
在Context类中定义一个AbstractStrategy类型的对象strategy,通过注入的方式在客户端传入一个具体策略对象,客户端代码片段如下所示:
……
Context context = new Context();
AbstractStrategy strategy;
strategy = new ConcreteStrategyA(); //可在运行时指定类型
context.setStrategy(strategy);
context.algorithm();
……
在客户端代码中只需注入一个具体策略对象,可以将具体策略类类名存储在配置文件中,通过反射来动态创建具体策略对象,从而使得用户可以灵活地更换具体策略类,增加新的具体策略类也很方便。策略模式提供了一种可插入式(Pluggable)算法的实现方案。
二.示例
下面举个排序算法的例子:
public interface SortUtil{
<T extends Comparable<?>> T[] sortList(T[] list);
}
public class BubbleSort implements SortUtil{
@Override
public <T extends Comparable<?>> T[] sortList(T[] list) {
System.out.println("采用了冒泡排序算法!");
return list;
}
}
public class HashSort implements SortUtil{
@Override
public <T extends Comparable<?>> T[] sortList(T[] list) {
System.out.println("采用了哈希排序算法!");
return list;
}
}
public class Client {
public static void main(String[] args) {
String[] a5 = new String[] {"cc","11","Dd","2","5"};
//选择并创建需要使用的策略对象
SortUtil sortUtil = new BubbleSort();//此处高度依赖具体实现类BubbleSort
sortUtil.sortList(a5);
}
}
由于比较简单,这里省略了环境类。
三.总结
策略模式的重心
策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。
算法的平等性
策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正因为这个平等性,才能实现算法之间可以相互替换。所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的。
所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现。
运行时策略的唯一性
运行期间,策略模式在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态地在不同的策略实现中切换,但是同时只能使用一个。
策略模式的优点
(1)策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码移到父类里面,从而避免代码重复。
(2)使用策略模式可以避免使用多重条件(if-else)语句。多重条件语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重条件语句里面,比使用继承的办法还要原始和落后。
策略模式的缺点
(1)客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法类。换言之,策略模式只适用于客户端知道算法或行为的情况。
(2)由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。
四.拓展
紧耦合tight coupling是指两个实体高度依赖彼此以至于改变其中某个的行为时,需要调整实际的其中一个甚至二者的代码。松耦合 loose coupling则与之相反,两个实体没有高度依赖,它们之间甚至不知道彼此的存在,但二者仍然可以互相交互。
从上面总结可以看出策略模式也有不足的,显然客户端与具体策略类是紧耦合的,即客户端需要知道所有策略类并自行决定使用其中的一个(高度依赖)。另外策略算法的替换(或删除)也会要求客户端更改相应的代码,那有没有什么方式可以让客户端不需要知道具体策略类,就可以使用策略类,甚至可以让策略的选择权交给策略端(服务端)来实现呢?有的!那就是Services!
在Java6中引入了一个类叫ServiceLoader,它是一个简单的service provider服务提供者的装载工具类。
service(服务)是一组定义清晰的接口。service provider(服务提供者)是service的具体实现。
那就用service来改造下上面的例子,让客户端只面对接口编程,使得客户端与具体实现了完全解耦。
public interface SortUtil{
SortName getName();//策略参数,按名称来采用算法
<T extends Comparable<?>> T[] sortList(T[] list);
public static SortUtil getSortInstance(SortName name) {
ServiceLoader<SortUtil> sortUtils = ServiceLoader.load(SortUtil.class);
for (SortUtil s : sortUtils) {
if (name == s.getName()) {
return s;
}
}
return null;
}
//算法名称枚举
public static enum SortName{
BUBBLE,HASH,HEAP,MERGE;
}
}
public class BubbleSort implements SortUtil{
@Override
public <T extends Comparable<?>> T[] sortList(T[] list) {
System.out.println("采用了冒泡排序算法!");
return list;
}
@Override
public SortName getName() {
return SortName.BUBBLE;
}
}
public class HashSort implements SortUtil{
@Override
public <T extends Comparable<?>> T[] sortList(T[] list) {
System.out.println("采用了哈希排序算法!");
return list;
}
@Override
public SortName getName() {
return SortName.HASH;
}
}
public class Client {
public static void main(String[] args) {
String[] a5 = new String[] {"cc","11","Dd","2","5"};
//根据策略参数获取算法实例,与实现类是完全解耦的
SortUtil sortUtil = SortUtil.getSortInstance(SortName.HASH);
sortUtil.sortList(a5);
}
}
由于ServiceLoader 实现了Iterable接口,所以方法 ServiceLoader ServiceLoader.load(Class service),返回指定的service服务的所有实现者的集合的迭代器。它是一种懒加载的方式,仅当实例化某个具体实现类后,就把它加到缓存中。再利用java8的API的新特性,可以使用接口的静态方法来获取实例。这样做的好处是客户端只需通过接口(及参数)就可获取实现类的实例,使得客户端与实现类完全解耦。
当然ServiceLoader.load需要通过配置文件来加载所有的实现类的,它通过service的全称例如strategy.api.SortUtil,然后在类路径META-INF/services/ 目录下寻找strategy.api.SortUtil文件,所以需要创建这么个文件。文件内容的每一行为一个具体实现类的全名。
但是,好像替换或删除掉某个算法实现类后,客户端还是需要更改代码(例如上面的HASH算法类删掉了,客户也要改代码),这种情况也是有可能发生的。比如某个jar包提供了上面多种算法的实现类库,客户端使用了该jar包的类A,后来jar包升级后将其中某个很差劲的算法类A给删除了(这种做法通常不可取),而客户端也升级了这个jar包,那客户端由于缺少这个类A,那客户端原来的程序将会无法编译成功,而迫使修改代码。
虽然解耦了客户端和实现类的关系,但策略选择权还是在客户端,客户需要根据接口中提供的算法名称来决定使用哪一个算法。那如果把选择权移到服务端(接口)会怎么样呢?看下面代码:
public interface SortUtil{
int getIdealMaxInputLength();// 策略参数,根据最大的数目决定是否采用此算法
<T extends Comparable<?>> T[] sortList(T[] list);
public static SortUtil getSortInstance(int listSize) {
ServiceLoader<SortUtil> sortUtils = ServiceLoader.load(SortUtil.class);
List<SortUtil> list = new ArrayList<>();
for (SortUtil sortUtil : sortUtils) {
list.add(sortUtil);
}
Collections.sort(list);
for (SortUtil s : list) {
if (listSize <= s.getIdealMaxInputLength()) {
return s;
}
}
return null;
}
}
public class BubbleSort implements SortUtil{
@Override
public <T extends Comparable<?>> T[] sortList(T[] list) {
System.out.println("采用了冒泡排序算法!");
return list;
}
@Override
public int getIdealMaxInputLength() {
return 4;
}
}
public class HashSort implements SortUtil{
@Override
public <T extends Comparable<?>> T[] sortList(T[] list) {
System.out.println("采用了哈希排序算法!");
return list;
}
@Override
public int getIdealMaxInputLength() {
return Integer.MAX_VALUE;
}
}
public class MergeSort implements SortUtil {
@Override
public int getIdealMaxInputLength() {
return 8;
}
@Override
public <T extends Comparable<?>> T[] sortList(T[] list) {
// 此处对list排序
System.out.println("采用了归并排序算法!");
return list;
}
}
public class Client {
public static void main(String[] args) {
String[] a5 = new String[] {"cc","11","Dd","2","5"};
//根据策略参数获取算法实例,与实现类是完全解耦的
SortUtil sortUtil = SortUtil.getSortInstance(a5.length);
sortUtil.sortList(a5);
}
}
这样客户端已经与接口实现类彻彻底底的解耦了,客户端完全无法感知实现类的存在。无论添加、删除、替换掉某种算法,客户端都不需要动代码。当然不完美之处是损失了ServiceLoader的懒加载特性,因为需要遍历所有实例才能决定采用哪个实例,但保留了其缓存的特点。另外也在一定程度上使得各个实现类之间有了某种联系(根据参数来选择某个实现类实例)。有时找不到实例会返回null,需要客户端作相应处理(例如自己实现个默认的实例。
客户端要使用其中的某个实现类,在Java9之前是完全可以做到的,只需import该类即可。那似乎还是没有最终解决客户会直接调用实现类(不通过接口)的方式。
试想下,如果把这些具体实现类单独封装成私有包,让客户端无法通过import方式来使用这些实现类,那作为这些类的维护者而言(例如Java平台本身)可谓真是一大好事,不用再声明@deprecated废弃了,直接删掉即可,也不会影响所有的客户端。这一想法在Java9模块化中得以实现,模块化使得在包层级上私有化成为可能。(Java9之前,Java平台中有些类是专门给平台内部使用的,但还是无法避免客户端通过Import方式来使用,造成后续平台维护升级的困难)
参考电子书下载:设计模式的艺术–软件开发人员内功修炼之道_刘伟(2013年).pdf
《道德经》第四章:
道冲,而用之有弗盈也。渊呵!似万物之宗。锉其兑,解其纷,和其光,同其尘。湛呵!似或存。吾不知其谁之子,象帝之先。
译文:大“道”空虚开形,但它的作用又是无穷无尽。深远啊!它好象万物的祖宗。消磨它的锋锐,消除它的纷扰,调和它的光辉,混同于尘垢。隐没不见啊,又好象实际存在。我不知道它是谁的后代,似乎是天帝的祖先。