Strategy模式结构图如下:
图21-1 Strategy模式类图
策略模式涉及到三个角色:
1、抽象策略(Strategy)角色:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,Context使用这个接口调用不同的算法,一般使用接口或抽象类实现。
2、环境(Context)角色:需要使用ConcreteStrategy提供的算法。内部维护一个Strategy的实例。负责动态设置运行时Strategy具体的实现算法。负责跟Strategy之间的交互和数据传递。
3、具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。实现了Strategy定义的接口,提供具体的算法实现。
Strategy模式以下列几条原则为基础:
1)每个对象都是一个具有职责的个体。
2)这些职责不同的具体实现是通过多态的使用来完成的。
3)概念上相同的算法具有多个不同的实现,需要进行管理。
下面通过一个实例来说明它的具体使用,这个例子是关于数据库连接的。代码如下:
interface DatabaseStrategy { //Strategy:抽象策略 public void process(); } class MysqlDBStrategy implements DatabaseStrategy { //具体策略 @Override public void process() { System.out.println("处理Mysql数据库连接"); } } class OracleDBStrategy implements DatabaseStrategy { @Override public void process() { System.out.println("处理Oracle数据库连接"); } } class DataBaseManager { //Context角色 public void process(DatabaseStrategy dbStrategy) { dbStrategy.process(); } } public class StrategyClient { public static void main(String[] args) { MysqlDBStrategy mysql = new MysqlDBStrategy(); DataBaseManager manager = new DataBaseManager(); manager.process(mysql); OracleDBStrategy oracle = new OracleDBStrategy(); manager.process(oracle); } }在我们的实际编程中经常会遇到系统要连接的数据库可能不只一种,如果采用传统的方法,即修改连接Url的方法,这种方法确实可行,但是有一个问题要经常修改源代码,不利于以后的维护,那么有没有一种更好的方法呢?答案是有,使用Strategy模式,首先定义一个连接数据库通用的接口(在上面的例子中是DatabaseStrategy),然后再定义实现该接口的具体类(MysqlDBStrategy、OracleDBStrategy),在这些具体类,实现具体的逻辑。最后再定义一个管理数据库连接的类(DataBaseManager),它的内部有一个方法可以接受具体类实例的参数。我们可以看到这个参数是DatabaseStrategy类型的,也就是说它可以接受任何一个实现了DatabaseStrategy接口的类的具体实例(这里运用了对象替换机制,多态的一种),从而完成数据库连接的处理。如果我们还需要处理另外一种数据库如sqlserver,我们只需要建立一个SqlserverDBStrategy类实现DatabaseStrategy接口,把该类的实例传给DatabaseManager的process方法即可。
图21-2 AWT中的容器和布局管理器的关系
如果有几个很相似的类,其区别仅仅是在个别行为上的动作不同,这时候就可以考虑使用Strategy模式。这样,通过策略组合,将原来的多个类精简为一个带有多个策略的类。这很符合OO设计的原则:找到变化的部分,并将其封装起来!Strategy模式同样的为子类继承提供了一个好的替代方案,当使用继承机制的时候,行为的改变是静态的,你只能够改变一次。而策略是动态的,可以在任何时候,切换任何次数。更为重要的是,策略对象可以在不同的环境中被不同的对象所共享。以布局管理器为例,虽然每一个容器只有一个布局管理器,但是一个布局管理器可以为多个容器工作。
从结构上看,Strategy模式与State模式有几分相似,但二者所讨论的Context(情景)具有显著的差异。
State模式在于将其状态信息分离出来保存到一个独立的对象中,以便状态信息的获取或状态的转换;Strategy模式在于将可能的算法分离出来,根据需要进行适当的选择。此外,二者的区别还在于,Strategy模式中各个Strategy(算法、策略)往往用于解决相同的问题,即只是解决同一问题的不同“策略”、“途径”,而且,一次只能有一个Strategy为上次应用提供服务。而State模式中的各个State本身往往具有一定的差异,但他们之间存在明显的相互转换的关系,而且这种转换往往会在程序运行过程中经常性地发生,同时存在一个以上State也是可能的。
区别参考:二者的应用场合不同。状态模式用于处理对象有不同状态(状态机)的场合,策略模式用于随不同外部环境采取不同行为的场合。在状态模式中,状态的变迁是由对象的内部条件决定,外界只需关心其接口,不必关心其状态对象的创建和转化;而策略模式里,采取何种策略由外部条件决定。所以,有人说“状态模式是完全封装且自修改的策略模式”。至于Bridge,在结构上与前两者都不一样了。要说相似之处,就是三者都有具有对外接口统一的类,展现出多态性而已。
当存在以下情况时可考虑使用Strategy模式:
1、许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。
2、需要使用一个算法的不同变体。例如,你可能会定义一些反映不同的空间/时间权衡的算法,当这些变体实现为一个算法的类层次时,可以使用策略模式。
3、算法使用客户不应该知道的数据。可使用策略模式以避免暴露复杂的、与算法相关的数据结构。
4、一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。
具体的应用实例还可以列举一些,如:
1、以不同的格式保存文件;
2、以不同的方式对文件进行压缩或其他处理;
3、以不同的方式绘制/处理相同的图形数据。
《设计模式》一书对Template Method模式是这样描述的:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。这使得子类可以不改变算法的结构而重新定义算法的某些特定步骤。
这里所说的Template跟Generic Programming(范型编程)中讨论的C++的template不是一回事(虽然有一定的相似性),C++的template是一种逻辑复用的方式,它可以不依赖于OO的Inheritance(继承)机制独立存在,因为GP跟OO所讨论的是完全不同的两个方面,虽然二者经常被融合在一起使用。Template Method模式与template不同,它是建立在继承机制 + 虚函数基础上的,它的核心在于在基类中定义好逻辑处理的框架(或称完成一项任务所需依次执行的步骤,或一段通用的处理逻辑),将具体的处理细节交给子类具体实现,从而达到“使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤”的目的。
Template Method模式的结构如下图所示:
图22-1 Template Method模式类图
其中的参与者比较简单:
1、AbstractClass(抽象类):定义一到多个抽象方法(也可以不是抽象方法,但至少应该是virtual方法。视你的应用需要,如果你的AbstractClass负责实现一个通用版本的算法,各子类对该方法进行进一步细化,则只需定义成virtual方法即可),具体的子类将重定义它们以实现一个算法;而且还实现一个模板方法,来定义一个算法的骨架。该模板方法不仅调用前面的抽象方法,也可以调用其他的操作。各抽象方法往往被定义成protected(保护)成员,以保证它们只被模板方法调用,而TemplateMethod往往被定义成public非虚成员函数。
2、ConcreteClass(具体类):实现父类中的抽象方法以完成算法中与特定子类相关的步骤。
这里的关键是基类中的TemplateMethod方法,因为正是它定义了对各子类对象适用的通用的处理逻辑。
下面给出一个例子:
abstract class QueryTemplate { //抽象类:算法的模板 //Template Method:模板方法,一般为public的,用来执行算法的各步 public void doQuery() { formatConnect(); formatSelect(); } //算法的骨架,一般为protected的,由子类来实现它们 //可见,子类并不能改变算法的框架结构,但可以改变算法的实现步骤 protected abstract void formatConnect(); protected abstract void formatSelect(); } class OracleQT extends QueryTemplate { //实现算法的具体子类 @Override public void formatConnect() { System.out.println("格式化Qracle数据库连接"); } @Override public void formatSelect() { System.out.println("格式化Oracle数据库查询"); } } class MysqlQT extends QueryTemplate { @Override public void formatConnect() { System.out.println("格式化Mysql数据库连接"); } @Override public void formatSelect() { System.out.println("格式化Mysql数据库查询"); } } public class TemplateTestClient { public static void main(String[] args) { QueryTemplate oracleQT = new OracleQT(); oracleQT.doQuery(); //调用抽象模板的模板方法,以执行算法 QueryTemplate mysqlQT = new MysqlQT(); mysqlQT.doQuery(); } }
在这个例子中,我们定义了一个骨架QueryTemplate,在它的内部定义了一个Template Method和一些步骤(抽象方法),使用Template Method来调用这些步骤。步骤是在子类中实现的。
理解:定义一个抽象类(接口),在它的内部定义一些抽象的方法(供TemplateMethod调用的步骤)和一个TemplateMethod方法(非抽象方法),封装了这些抽象方法的抽象类(接口)就是骨架。而将它的实现延迟到子类中,也就是用子类实现它。不改变算法的结构而重新定义它的步骤,也就是改写或者实现父类的这些非TemplateMethod的抽象方法。
有时候,我们会遇到由一系列步骤构成的过程需要执行。这个过程从高层次上看是相同的,但有些步骤的实现可能不同。正如,查询SQL数据库从高层次上看过程是相同的,但某些细节比如如何连接数据库则可能因平台等细节的不同而不同。通过Template Method模式,我们可以先定义步骤序列,然后覆盖那些需要改变的步骤。
应用:
Template Method模式是一个使用频率比较高的模式,因为对于同一种类型的对象而言,他们之间一些处理流程往往是一致的,对象之间的差异仅在于具体的处理逻辑,因此,可以将通用的逻辑提取出来放到AbstractClass中实现,而将实现的具体细节交给子类完成。
从这一点上讲,Template Method与Strategy模式存在一定的相似性,但Template Method中实现的主体是ConcreteClass,AbstractClass仅定义了接口和希望子类重新定义的方法,通过继承来改变算法;而Strategy模式中Context类与Strategy类之间不存在继承关系,体现的是一种委托的关系。
Visitor模式定义:表示一个作用于某对象结构中各元素的操作。它可以使你不修改各元素类的前提下定义作用于这些元素的新操作,也就是动态的增加新的方法。
Visitor模式的结构如下图所示:
图23-1 Visitor模式类图
其中包括以下组成部分:
Visitor(访问者):为该对象结构中的每个ConcreteElement提供一个visit操作。该操作的名字和特征标识了要访问的具体元素角色,这样访问者就可以通过该元素的特定接口直接访问它。
ConcreteVisitor(具体访问者):实现每个由Visitor声明的操作。每个操作实现本算法的一部分,而该算法片断乃是对应于结构中对象的类。ConcreteVisitor为该算法提供了上下文并存储它的局部状态,这一状态常常在遍历该结构的过程中累积结果。
Element(元素):定义一个accept操作,它以一个访问者为参数,接受具体的访问者。
ConcreteElement(具体元素):实现Element的accept操作,该操作以一个访问者为参数。
ObjectStructure(对象结构,如Program):这是使用访问者模式必备的角色。能枚举它的元素;可以提供一个高层的接口以允许该访问者访问它的元素;可以是一个复合(组合模式)或是一个集合,如一个列表或一个无序集合。
下面代码按照类图的结构图来写的:
import java.util.ArrayList; import java.util.Collection; interface Visitor { //访问者 public void visitElementA(ConcreteElementA elementA);//针对具体元素A的新方法 public void visitElementB(ConcreteElementB elementB);//针对具体元素B的新方法 } interface Element { //元素 public void accept(Visitor visitor); } class ConcreteVisitor implements Visitor { //具体的访问者 @Override public void visitElementA(ConcreteElementA elementA) { //访问具体元素A System.out.println(elementA.getName() + " visited by ConcreteVisitor "); } @Override public void visitElementB(ConcreteElementB elementB) { System.out.println(elementB.getName() + " visited by ConcreteVisitor "); } } class ConcreteElementA implements Element { //具体元素A private String name; public ConcreteElementA(String name) { this.name = name; } @Override public void accept(Visitor visitor) { //接受访问者的访问:要把自己推送给访问者 visitor.visitElementA(this); } public String getName() { return name; } } class ConcreteElementB implements Element { //具体元素B private String name; public ConcreteElementB(String name) { this.name = name; } public String getName() { return name; } @Override public void accept(Visitor visitor) { //接受访问者的访问:要把自己推送给访问者 visitor.visitElementB(this); } } class ObjectStructure { //对象结构:即元素的集合 private Collection<Element> collection = new ArrayList<>(); //维护一个元素列表 public void attach(Element element) { collection.add(element); } public void detach(Element element) { collection.remove(element); } public void accept(Visitor visitor) { //让每个元素者接受访问的访问 for (Element element : collection) { element.accept(visitor); } } } public class VisitorClient { public static void main(String args[]) { Element elementA = new ConcreteElementA("ElementA"); Element elementB = new ConcreteElementB("ElementB"); Visitor visitor = new ConcreteVisitor(); ObjectStructure os = new ObjectStructure(); os.attach(elementA); os.attach(elementB); os.accept(visitor); } }在上述实现中,我们可以发现,Visitor模式虽然使得为已有的类型添加新的抽象函数的需求变得容易实现,但是,Element类型与Visitor类型之间的耦合十分严重,出现了循环依赖,Visitor需要有所有Element子类的声明,而所有Element子类也需要包含Visitor类的头文件,当需要增加新的Element类型时,由于Visitor类的改动,将造成Element继承体系和Visitor继承体系全部需要重新编译。那么有什么办法来减轻耦合呢?在C++中,我们为每一个Element类型实现一个ConcreteVisitor,并最终通过多继承来实现IntegratedConcreteVisitor以解除这种耦合关系的实现方法,但这种实现方法使得继承体系变得更加复杂,同时还存在一些其它的开销。个人认为,Visitor模式是GoF所列举的23种模式中最复杂的,同时由于其使用上的约束较多,实际的应用并不太多。
protected void processEvent(AWTEvent e) { if (e instanceof FocusEvent) { processFocusEvent((FocusEvent) e); } else if (e instanceof MouseEvent) { switch (e.getID()) { case MouseEvent.MOUSE_PRESSED: case MouseEvent.MOUSE_RELEASED: case MouseEvent.MOUSE_CLICKED: case MouseEvent.MOUSE_ENTERED: case MouseEvent.MOUSE_EXITED: processMouseEvent((MouseEvent) e); break; case MouseEvent.MOUSE_MOVED: case MouseEvent.MOUSE_DRAGGED: processMouseMotionEvent((MouseEvent) e); break; case MouseEvent.MOUSE_WHEEL: processMouseWheelEvent((MouseWheelEvent) e); break; } } else if (e instanceof KeyEvent) { processKeyEvent((KeyEvent) e); } else if (e instanceof ComponentEvent) { processComponentEvent((ComponentEvent) e); } else if (e instanceof InputMethodEvent) { processInputMethodEvent((InputMethodEvent) e); } else if (e instanceof HierarchyEvent) { switch (e.getID()) { case HierarchyEvent.HIERARCHY_CHANGED: processHierarchyEvent((HierarchyEvent) e); break; case HierarchyEvent.ANCESTOR_MOVED: case HierarchyEvent.ANCESTOR_RESIZED: processHierarchyBoundsEvent((HierarchyEvent) e); break; } } }这种方式通过一堆的if-else,switch-case检查b的类型信息进行Re-Dispatch,虽然type-switch在设计上比较简单,但type-switch是OOD中应当尽量避免使用的技术,因为它可能给我们的代码引入一些难以察觉的Bug,以下面的代码为例(Java Code):
class A { } class B extends A { } public class DispatchTest { static public void main(String[] args) { B b = new B(); if (b instanceof A) { System.out.println("b is an instanceof A"); } else if (b instanceof B) { System.out.println("b is an instanceof B"); } } }程序运行的结果是:
public class Integer { Number add(Number b) { //接受Number b的加动作,并把自己推送给b return b.add(this); } //... }则不管b是什么类型,只要它实现了add(Integer a)这个方法,就可以准确完成add操作。这里的Number相当于Visitor,Integer相当于ConcreteElement,Integer是由Number来加(访问)的,它自己并没有去主动加b,而被b加了。
public class Float { Number add(Number b) { return b.add(this); } //... }则不管b是什么类型,只要它实现了add(Float a)这个方法,就可以准确完成add操作。
那么这种复杂的Double-Dispatch技术有什么好处呢?它的好处之一在于可以使我们在不改变a的同时,通过对b进行扩充,达到为a提供新的功能的目的。以上面的add为例,我们可以从Number派生出一种新的数值类型,在其中实现各种add操作,则可以在不改变已有数值类型的基础上与之协同工作。当然,由于在使用上存在一些限制,限制了Double-Dispatch的应用。
关于双重分派,还可以参考我之前写的一篇文章,关于在C++中实现多态的双重分派:http://blog.csdn.net/zhoudaxia/article/details/4580438
abstract class Parts { //Element角色 abstract void accept(Visitor visitor); } // component class: Wheel class Wheel extends Parts { //ConcreteElement角色 private String name; Wheel(String name) { this.name = name; } String getName() { return this.name; } @Override void accept(Visitor visitor) { // function to support double-dispatch visitor.visit(this); } } // component class: Engine class Engine extends Parts { @Override void accept(Visitor visitor) { visitor.visit(this); } } // component class: Body class Body extends Parts { @Override void accept(Visitor visitor) { visitor.visit(this); } } // class to demonstrate visitor pattern and double-dispatch. //If we don't use double-dispatch, we will lost all class info when we //put all components into an array. class Car { //ObjectStructure角色:管理对各元素的访问 private Parts[] parts = { new Engine(), new Body(), new Wheel("front left"), new Wheel("front right"), new Wheel("back left"), new Wheel("back right") }; //把对各元素的访问委托给Visitor void accept(Visitor visitor) { visitor.visit(this); for (int i = 0; i < parts.length; ++i) { parts[i].accept(visitor); } } } // visitor interface, all concrete visitor class must implement it. // need a access-function for each element class in the class-hierachy interface Visitor { void visit(Wheel wheel); void visit(Engine engine); void visit(Body body); void visit(Car car); } // concrete visitor: PrintVisitor class PrintVisitor implements Visitor { //具体的Visitor:完成对每个元素的访问 @Override public void visit(Wheel wheel) { System.out.println("Visiting " + wheel.getName() + " wheel"); } @Override public void visit(Engine engine) { System.out.println("Visiting engine"); } @Override public void visit(Body body) { System.out.println("Visiting body"); } @Override public void visit(Car car) { System.out.println("Visiting car"); } } // more concrete visitor class, omitted... // entry class public class VisitorDemo { static public void main(String[] args) { Car car = new Car(); Visitor visitor = new PrintVisitor(); car.accept(visitor); } }基本思想: 把访问对象中各元素(或者一个复合对象中的各原子对象)的工作委托给Visitor来完成。把各元素抽象一个元素类,它有一个接受访问请求的accept方法,此方法里面把真正的访问工作转发给Visitor来完成,并把自己的引用传过去。Visitor里面有各元素的访问方法,根据传来的引用访问该元素。由于一个对象有很多属性元素,故要有一个管理者ObjectStructure维护一个元素集合,并串起对各元素的访问。