当我们掌握了Java的语法,当我们了解了面向对象的封装、继承、多态等特性,当我们可以用Swing、Servlet、JSP技术构建桌面以及Web应用,不意味着我们可以写出面向对象的程序,不意味着我们可以很好的实现代码复用,弹性维护,不意味着我们可以实现在维护、扩展基础上的代码复用。一把刀,可以使你制敌于无形而于江湖扬名,也可以只是一把利刃而使你切菜平静。Java,就是这把刀,它的威力取决于你使用的方式。当我们陷入无尽无止重复代码的泥沼,当我们面临牵一发而动全身的维护恶梦, 你应该想起“设计模式”这个行动秘笈。面向对象的精义,看似平淡,其实要经过艰苦实践才能成功。而构造OO系统的隐含经验于是被前人搜集而成并冠以“设计模式”之名。我们应该在编码行动初始就携带以它。接下来,让我们步“四人组”先行者之后,用中国文字、用实际案例领略模式于我们代码焕然一新的改变:
设计模式解读之一: 策略模式
1. 模式定义
把会变化的内容取出并封装起来,以便以后可以轻易地改动或扩充部分,而不影响不需要变化的其他部分;
2. 问题缘起
当涉及至代码维护时,为了复用目的而使用继承,结局并不完美。对父类的修改,会影响到子类型。在超类中增加的方法,会导致子类型有该方法,甚至连那些不该具备该方法的子类型也无法免除。
示例,一个鸭子类型:(示例来源于head first.设计模式 )
- public abstract class Duck {
- //所有的鸭子均会叫以及游泳,所以父类中处理这部分代码
- public void quack() {
- System.out.println("Quack");
- }
- public void swim() {
- System.out.println("All ducks float, even decoys.");
- }
- //因为每种鸭子的外观是不同的,所以父类中该方法是抽象的,由子类型自己完成。
- public abstract void display();
- }
- public class MallardDuck extends Duck {
- //野鸭外观显示为绿头
- public void display() {
- System.out.println("Green head.");
- }
- }
- public class RedHeadDuck extends Duck {
- //红头鸭显示为红头
- public void display() {
- System.out.println("Red head.");
- }
- }
- public class RubberDuck extends Duck {
- //橡皮鸭叫声为吱吱叫,所以重写父类以改写行为
- public void quack() {
- System.out.println("Squeak");
- }
- //橡皮鸭显示为黄头
- public void display() {
- System.out.println("Yellow head.");
- }
- }
public abstract class Duck {
//所有的鸭子均会叫以及游泳,所以父类中处理这部分代码
public void quack() {
System.out.println("Quack");
}
public void swim() {
System.out.println("All ducks float, even decoys.");
}
//因为每种鸭子的外观是不同的,所以父类中该方法是抽象的,由子类型自己完成。
public abstract void display();
}
public class MallardDuck extends Duck {
//野鸭外观显示为绿头
public void display() {
System.out.println("Green head.");
}
}
public class RedHeadDuck extends Duck {
//红头鸭显示为红头
public void display() {
System.out.println("Red head.");
}
}
public class RubberDuck extends Duck {
//橡皮鸭叫声为吱吱叫,所以重写父类以改写行为
public void quack() {
System.out.println("Squeak");
}
//橡皮鸭显示为黄头
public void display() {
System.out.println("Yellow head.");
}
}
上述代码,初始实现得非常好。现在我们如果给Duck.java中加入fly()方法的话,那么在子类型中均有了该方法,于是我们看到了 会飞的橡皮鸭子,你看过吗?当然,我们可以在子类中通过空实现重写该方法以解决该方法对于子类型的影响。但是父类中再增加其它的方法呢?
通过继承在父类中提供行为,会导致以下缺点:
a. 代码在多个子类中重复;
b. 运行时的行为不容易改变;
c. 改变会牵一发动全身,造成部分子类型不想要的改变;
好啦,还是刚才鸭子的例子,你也许想到使用接口,将飞的行为、叫的行为定义为接口,然后让Duck的各种子类型实现这些接口。这时侯代码类似于:
- public abstract class Duck {
- //将变化的行为 fly() 以及quake()从Duck类中分离出去定义形成接口,有需求的子类中自行去实现
- public void swim() {
- System.out.println("All ducks float, even decoys.");
- }
- public abstract void display();
- }
- //变化的 fly() 行为定义形成的接口
- public interface FlyBehavior {
- void fly();
- }
- //变化的 quack() 行为定义形成的接口
- public interface QuackBehavior {
- void quack();
- }
- //野鸭子会飞以及叫,所以实现接口FlyBehavior, QuackBehavior
- public class MallardDuck extends Duck implements FlyBehavior, QuackBehavior{
- public void display() {
- System.out.println("Green head.");
- }
- public void fly() {
- System.out.println("Fly.");
- }
- public void quack() {
- System.out.println("Quack.");
- }
- }
- //红头鸭子会飞以及叫,所以也实现接口FlyBehavior, QuackBehavior
- public class RedHeadDuck extends Duck implements FlyBehavior, QuackBehavior{
- public void display() {
- System.out.println("Red head.");
- }
- public void fly() {
- System.out.println("Fly.");
- }
- public void quack() {
- System.out.println("Quack.");
- }
- }
- //橡皮鸭不会飞,但会吱吱叫,所以只实现接口QuackBehavior
- public class RubberDuck extends Duck implements QuackBehavior{
- //橡皮鸭叫声为吱吱叫
- public void quack() {
- System.out.println("Squeak");
- }
- //橡皮鸭显示为黄头
- public void display() {
- System.out.println("Yellow head.");
- }
- }
public abstract class Duck {
//将变化的行为 fly() 以及quake()从Duck类中分离出去定义形成接口,有需求的子类中自行去实现
public void swim() {
System.out.println("All ducks float, even decoys.");
}
public abstract void display();
}
//变化的 fly() 行为定义形成的接口
public interface FlyBehavior {
void fly();
}
//变化的 quack() 行为定义形成的接口
public interface QuackBehavior {
void quack();
}
//野鸭子会飞以及叫,所以实现接口FlyBehavior, QuackBehavior
public class MallardDuck extends Duck implements FlyBehavior, QuackBehavior{
public void display() {
System.out.println("Green head.");
}
public void fly() {
System.out.println("Fly.");
}
public void quack() {
System.out.println("Quack.");
}
}
//红头鸭子会飞以及叫,所以也实现接口FlyBehavior, QuackBehavior
public class RedHeadDuck extends Duck implements FlyBehavior, QuackBehavior{
public void display() {
System.out.println("Red head.");
}
public void fly() {
System.out.println("Fly.");
}
public void quack() {
System.out.println("Quack.");
}
}
//橡皮鸭不会飞,但会吱吱叫,所以只实现接口QuackBehavior
public class RubberDuck extends Duck implements QuackBehavior{
//橡皮鸭叫声为吱吱叫
public void quack() {
System.out.println("Squeak");
}
//橡皮鸭显示为黄头
public void display() {
System.out.println("Yellow head.");
}
}
上述代码虽然解决了一部分问题,让子类型可以有选择地提供一些行为(例如 fly() 方法将不会出现在橡皮鸭中).但我们也看到,野鸭子MallardDuck.java和红头鸭子RedHeadDuck.java的一些相同行为代码不能得到重复使用。很大程度上这是从一个火坑跳到另一个火坑。
在一段程序之后,让我们从细节中跳出来,关注一些共性问题。不管使用什么语言,构建什么应用,在软件开发上,一直伴随着的不变的真理是:需要一直在变化。不管当初软件设计得多好,一段时间之后,总是需要成长与改变,否则软件就会死亡。
我们知道,继承在某种程度上可以实现代码重用,但是父类(例如鸭子类Duck)的行为在子类型中是不断变化的,让所有子类型都有这些行为是不恰当的。我们可以将这些行为定义为接口,让Duck的各种子类型去实现,但接口不具有实现代码,所以实现接口无法达到代码复用。这意味着,当我们需要修改某个行为,必须往下追踪并在每一个定义此行为的类中修改它,一不小心,会造成新的错误。
设计原则:把应用中变化的地方独立出来,不要和那些不需要变化的代码混在一起。这样代码变化引起的不经意后果变少,系统变得更有弹性。
按照上述设计原则,我们重新审视之前的Duck代码。
1) 分开变化的内容和不变的内容
Duck类中的行为 fly(), quack(), 每个子类型可能有自己特有的表现,这就是所谓的变化的内容。
Duck类中的行为 swim() 每个子类型的表现均相同,这就是所谓不变的内容。
我们将变化的内容从Duck()类中剥离出来单独定义形成接口以及一系列的实现类型。将变化的内容定义形成接口可实现变化内容和不变内容的剥离。其实现类型可实现变化内容的重用。这些实现类并非Duck.java的子类型,而是专门的一组实现类,称之为"行为类"。由行为类而不是Duck.java的子类型来实现接口。这样,才能保证变化的行为独立于不变的内容。于是我们有:
变化的内容:
- //变化的 fly() 行为定义形成的接口
- public interface FlyBehavior {
- void fly();
- }
- //变化的 fly() 行为的实现类之一
- public class FlyWithWings implements FlyBehavior {
- public void fly() {
- System.out.println("I'm flying.");
- }
- }
- //变化的 fly() 行为的实现类之二
- public class FlyNoWay implements FlyBehavior {
- public void fly() {
- System.out.println("I can't fly.");
- }
- }
//变化的 fly() 行为定义形成的接口
public interface FlyBehavior {
void fly();
}
//变化的 fly() 行为的实现类之一
public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("I'm flying.");
}
}
//变化的 fly() 行为的实现类之二
public class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("I can't fly.");
}
}
- //变化的 quack() 行为定义形成的接口
- public interface QuackBehavior {
- void quack();
- }
- //变化的 quack() 行为实现类之一
- public class Quack implements QuackBehavior {
- public void quack() {
- System.out.println("Quack");
- }
- }
- //变化的 quack() 行为实现类之二
- public class Squeak implements QuackBehavior {
- public void quack() {
- System.out.println("Squeak.");
- }
- }
- //变化的 quack() 行为实现类之三
- public class MuteQuack implements QuackBehavior {
- public void quack() {
- System.out.println("<< Slience >>");
- }
- }
//变化的 quack() 行为定义形成的接口
public interface QuackBehavior {
void quack();
}
//变化的 quack() 行为实现类之一
public class Quack implements QuackBehavior {
public void quack() {
System.out.println("Quack");
}
}
//变化的 quack() 行为实现类之二
public class Squeak implements QuackBehavior {
public void quack() {
System.out.println("Squeak.");
}
}
//变化的 quack() 行为实现类之三
public class MuteQuack implements QuackBehavior {
public void quack() {
System.out.println("<< Slience >>");
}
}
通过以上设计,fly()行为以及quack()行为已经和Duck.java没有什么关系,可以充分得到复用。而且我们很容易增加新的行为, 既不影响现有的行为,也不影响Duck.java。但是,大家可能有个疑问,就是在面向对象中行为不是体现为方法吗?为什么现在被定义形成类(例如Squeak.java)?在OO中,类代表的"东西"一般是既有状态(实例变量)又有方法。只是在本例中碰巧"东西"是个行为。既使是行为,也有属性及方法,例如飞行行为,也需要一些属性记录飞行的状态,如飞行高度、速度等。
2) 整合变化的内容和不变的内容
- public abstract class Duck {
- //将行为类声明为接口类型,降低对行为实现类型的依赖
- FlyBehavior flyBehavior;
- QuackBehavior quackBehavior;
- public void performFly() {
- //不自行处理fly()行为,而是委拖给引用flyBehavior所指向的行为对象
- flyBehavior.fly();
- }
- public void performQuack() {
- quackBehavior.quack();
- }
- public void swim() {
- System.out.println("All ducks float, even decoys.");
- }
- public abstract void display();
- }
public abstract class Duck {
//将行为类声明为接口类型,降低对行为实现类型的依赖
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public void performFly() {
//不自行处理fly()行为,而是委拖给引用flyBehavior所指向的行为对象
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void swim() {
System.out.println("All ducks float, even decoys.");
}
public abstract void display();
}
Duck.java不关心如何进行 fly()以及quack(), 这些细节交由具体的行为类完成。
- public class MallardDuck extends Duck{
- public MallardDuck() {
- flyBehavior=new FlyWithWings();
- quackBehavior=new Quack();
- }
- public void display() {
- System.out.println("Green head.");
- }
- }
public class MallardDuck extends Duck{
public MallardDuck() {
flyBehavior=new FlyWithWings();
quackBehavior=new Quack();
}
public void display() {
System.out.println("Green head.");
}
}
- public class DuckTest {
- public static void main(String[] args) {
- Duck duck=new MallardDuck();
- duck.performFly();
- duck.performQuack();
- }
- }
public class DuckTest {
public static void main(String[] args) {
Duck duck=new MallardDuck();
duck.performFly();
duck.performQuack();
}
}
在Duck.java子类型MallardDuck.java的构造方法中,直接实例化行为类型,在编译的时侯便指定具体行为类型。当然,我们可以:
1) 我们可以通过工厂模式或其它模式进一步解藕(可参考后续模式讲解);
2) 或做到在运行时动态地改变行为。
3) 动态设定行为
在父类Duck.java中增加设定行为类型的setter方法,接受行为类型对象的参数传入。为了降藕,行为参数被声明为接口类型。这样,既便在运行时,也可以通过调用这二个方法以改变行为。
- public abstract class Duck {
- //在刚才Duck.java中加入以下二个方法。
- public void setFlyBehavior(FlyBehavior flyBehavior) {
- this.flyBehavior=flyBehavior;
- }
- public void setQuackBehavior(QuackBehavior quackBehavior) {
- this.quackBehavior=quackBehavior;
- }
- //其它方法同,省略...
- }
public abstract class Duck {
//在刚才Duck.java中加入以下二个方法。
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior=flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior=quackBehavior;
}
//其它方法同,省略...
}
- public class DuckTest {
- public static void main(String[] args) {
- Duck duck=new MallardDuck();
- duck.performFly();
- duck.performQuack();
- duck.setFlyBehavior(new FlyNoWay());
- duck.performFly();
- }
- }
public class DuckTest {
public static void main(String[] args) {
Duck duck=new MallardDuck();
duck.performFly();
duck.performQuack();
duck.setFlyBehavior(new FlyNoWay());
duck.performFly();
}
}
如果,我们要加上火箭助力的飞行行为,只需再新建FlyBehavior.java接口的实现类型。而子类型可通过调用setQuackBehavior(...)方法动态改变。至此,在Duck.java增加新的行为给我们代码所带来的困绕已不复存在。
该是总结的时侯了,让我们从代码的水中浮出来,做一只在水面上自由游动的鸭子吧:
3.解决方案
MallardDuck 继承Duck抽象类; -> 不变的内容
FlyWithWings 实现 FlyBehavior接口; -> 变化的内容,行为或算法
在Duck.java提供setter方法以装配关系; -> 动态设定行为
以上就是策略模式的实现三步曲。接下来,让我们透过步骤看本质:
1) 初始,我们通过继承实现行为的重用,导致了代码的维护问题。 -> 继承, is a
2) 接着,我们将行为剥离成单独的类型并声明为不变内容的实例变量并通过-> 组合, has a
setter方法以装配关系;
继承,可以实现静态代码的复用;组合,可以实现代码的弹性维护;使用组合代替继承,可以使代码更好地适应软件开发完后的需求变化。
策略模式的本质:少用继承,多用组合
Strategy(策略模式):是对对象的策略的封装,可以选择并使用需要的策略
优点:可以动态的选择并使用策略
缺点:客户必须知道所有的策略,并自行选择使用那一种策略
代码模型:有一个抽象策略接口,有若干个继承了这个抽象策略接口的具体策略,有一个包含了抽象策略接口变量的对象。那么在客户调用该对象时可以使用某个具体的策略来向上转型成抽象策略,这也是一种多态的表现。这样就实现了策略的动态选择,不过这里讨论的是方法的动态实现而已。
- /**
- * 抽象策略
- * @author wly
- *
- */
- public interface AbstractStrategy {
- void someMethod();
- }
/**
* 抽象策略
* @author wly
*
*/
public interface AbstractStrategy {
void someMethod();
}
- /**
- * 一个具体的策略类
- * @author wly
- *
- */
- public class A_Strategy implements AbstractStrategy {
- @Override
- public void someMethod() {
- // 具体的策略方法一
- System.out.println("This is A_Strategy.someMethod");
- }
- }
/**
* 一个具体的策略类
* @author wly
*
*/
public class A_Strategy implements AbstractStrategy {
@Override
public void someMethod() {
// 具体的策略方法一
System.out.println("This is A_Strategy.someMethod");
}
}
- public class B_Strategy implements AbstractStrategy {
- @Override
- public void someMethod() {
- // 这又是一个具体的策略方法
- System.out.println("This is B_Startegy.someMethod");
- }
- }
public class B_Strategy implements AbstractStrategy {
@Override
public void someMethod() {
// 这又是一个具体的策略方法
System.out.println("This is B_Startegy.someMethod");
}
}
- /**
- * 环境对象,包含了抽象策略的引用变量
- * @author wly
- *
- */
- public class SomeObject {
- private AbstractStrategy abstractStrategy;
- public SomeObject(AbstractStrategy abstractStrategy) {
- this.abstractStrategy = abstractStrategy;
- }
- public void someMethod() {
- abstractStrategy.someMethod();
- }
- }
/**
* 环境对象,包含了抽象策略的引用变量
* @author wly
*
*/
public class SomeObject {
private AbstractStrategy abstractStrategy;
public SomeObject(AbstractStrategy abstractStrategy) {
this.abstractStrategy = abstractStrategy;
}
public void someMethod() {
abstractStrategy.someMethod();
}
}
- /**
- * 客户类
- * @author wly
- *
- */
- public class Test {
- public static void main(String[] args){
- SomeObject _so = new SomeObject(new A_Strategy());
- _so.someMethod();
- _so = new SomeObject(new B_Strategy());
- _so.someMethod();
- }
- }
- //输出:
- //This is A_Strategy.someMethod
- //This is B_Startegy.someMethod
对Object数组中的Person对象,按某属性进行排序。具体的按某属性排序被抽象出来,形成策略。
Person类:
- package com.flg.Strategy;
- import java.util.Comparator;
- /**
- * Created with IntelliJ IDEA.
- * User: fuliguo
- * Date: 12-8-12
- * Time: 下午4:02
- * 待排序类实现了Comparable接口
- * 排序条件由具体策略决定,排序策略实现了Comparator接口
- */
- public class Person implements Comparable<Person>{
- private String name;//姓名
- private int age;//年龄
- private int height;//身高
- //请关注工厂模式博文,此处会换成工厂方法实现
- //private Comparator comparator = new PersonAgeComparator();//按年龄排序
- //private Comparator comparator = new PersonHeigthComparator();//按身高排序
- private Comparator comparator = new PersonNameComparator();//按姓名排序
- //请关注工厂模式博文,此处会换成工厂方法实现
- public Person(String name, int age,int height) {
- this.name = name;
- this.age = age;
- this.height = height;
- }
- public Person(){}
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public int getAge() {
- return age;
- }
- public void setAge(int age) {
- this.age = age;
- }
- public int getHeight() {
- return height;
- }
- public void setHeight(int height) {
- this.height = height;
- }
- @Override
- /**
- * 实现Comparable接口中的comparaTo方法
- * 具体排序条件由对应策略实现
- */
- public int compareTo(Person o) {
- return comparator.compare(this,o);//根据什么排序,由具体策略实现决定
- }
- }
package com.flg.Strategy;
import java.util.Comparator;
/**
* Created with IntelliJ IDEA.
* User: fuliguo
* Date: 12-8-12
* Time: 下午4:02
* 待排序类实现了Comparable接口
* 排序条件由具体策略决定,排序策略实现了Comparator接口
*/
public class Person implements Comparable<Person>{
private String name;//姓名
private int age;//年龄
private int height;//身高
//请关注工厂模式博文,此处会换成工厂方法实现
//private Comparator comparator = new PersonAgeComparator();//按年龄排序
//private Comparator comparator = new PersonHeigthComparator();//按身高排序
private Comparator comparator = new PersonNameComparator();//按姓名排序
//请关注工厂模式博文,此处会换成工厂方法实现
public Person(String name, int age,int height) {
this.name = name;
this.age = age;
this.height = height;
}
public Person(){}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
@Override
/**
* 实现Comparable接口中的comparaTo方法
* 具体排序条件由对应策略实现
*/
public int compareTo(Person o) {
return comparator.compare(this,o);//根据什么排序,由具体策略实现决定
}
}
按Person的年龄进行排序的策略,PersonAgeComparator类:
- package com.flg.Strategy;
- import java.util.Comparator;
- /**
- * Created with IntelliJ IDEA.
- * User: fuliguo
- * Date: 12-8-12
- * Time: 下午4:07
- * 按Person的年龄进行排序的策略
- */
- public class PersonAgeComparator implements Comparator<Person> {
- @Override
- public int compare(Person o1, Person o2) {
- return o1.getAge()-o2.getAge();//按年龄比较
- }
- }
package com.flg.Strategy;
import java.util.Comparator;
/**
* Created with IntelliJ IDEA.
* User: fuliguo
* Date: 12-8-12
* Time: 下午4:07
* 按Person的年龄进行排序的策略
*/
public class PersonAgeComparator implements Comparator<Person> {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge()-o2.getAge();//按年龄比较
}
}
按Person的身高进行排序的策略,PersonHeightComparator类:
- package com.flg.Strategy;
- import java.util.Comparator;
- /**
- * Created with IntelliJ IDEA.
- * User: fuliguo
- * Date: 12-8-12
- * Time: 下午4:13
- * 按Person的高度进行排序的策略
- */
- public class PersonHeigthComparator implements Comparator<Person> {
- @Override
- public int compare(Person o1, Person o2) {
- return o1.getHeight()-o2.getHeight();//按高度比较
- }
- }
package com.flg.Strategy;
import java.util.Comparator;
/**
* Created with IntelliJ IDEA.
* User: fuliguo
* Date: 12-8-12
* Time: 下午4:13
* 按Person的高度进行排序的策略
*/
public class PersonHeigthComparator implements Comparator<Person> {
@Override
public int compare(Person o1, Person o2) {
return o1.getHeight()-o2.getHeight();//按高度比较
}
}
按Person的名字进行排序的策略,PersonNameComparator类:
- package com.flg.Strategy;
- import java.util.Comparator;
- /**
- * Created with IntelliJ IDEA.
- * User: fuliguo
- * Date: 12-8-12
- * Time: 下午4:05
- * 按Person的姓名进行排序的策略
- */
- public class PersonNameComparator implements Comparator<Person> {
- @Override
- public int compare(Person o1, Person o2) {
- return o1.getName().compareToIgnoreCase(o2.getName());//按姓名排序
- }
- }
package com.flg.Strategy;
import java.util.Comparator;
/**
* Created with IntelliJ IDEA.
* User: fuliguo
* Date: 12-8-12
* Time: 下午4:05
* 按Person的姓名进行排序的策略
*/
public class PersonNameComparator implements Comparator<Person> {
@Override
public int compare(Person o1, Person o2) {
return o1.getName().compareToIgnoreCase(o2.getName());//按姓名排序
}
}
测试类 Test类:
- package com.flg.Strategy;
- import com.flg.*;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.List;
- /**
- * Created with IntelliJ IDEA.
- * User: fuliguo
- * Date: 12-8-12
- * Time: 下午5:07
- * 测试类
- */
- public class Test {
- public Object[] getObjects() {
- Object [] objects = new Object[]{
- new Person("张三",22,188),
- new Person("赵六",19,210),
- new Person("王五",30,175),
- new Person("李四",10,158)
- } ;
- return objects;
- }
- /**
- * 控制台输出遍历Object数组对像顺序
- * @param
- */
- public void printObjects(Object [] objects) {
- Person p;
- for(int i=0;i<objects.length;i++){
- p = (Person) objects[i];
- System.out.println(p.getName()+"---"+p.getAge()+"---"+p.getHeight());
- }
- }
- public static void main(String [] arg){
- Test t = new Test();
- Object [] objects = t.getObjects();
- t.printObjects(objects);
- Arrays.sort(objects);
- System.out.println("******排序后******");
- t.printObjects(objects);
- }
- }
--------------------------------------------------------------------------------------------------------------
本文探讨初学使用策略模式时遇到的一些疑惑,以及在工作中慢慢解决之前遇到的疑惑,借此与大家分享。比如说本文谈到策略模式中环境角色Context的用处,为什么一定要用,可不可以将此取消。这些都是在学习和工作的实践总结中慢慢体会到的。
首先,我们来看下策略模式的概念。一般的解释如下:
策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。(原文:The Strategy Pattern defines a family of algorithms,encapsulates each one,and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.)
一般的,策略模式主要分为以下三个角色:
1. 环境角色(Context):持有一个策略类引用
2. 抽象策略(Strategy):定义了多个具体策略的公共接口,具体策略类中各种不同的算法以不同的方式实现这个接口;Context使用这些接口调用不同实现的算法。一般的,我们使用接口或抽象类实现。
3. 具体策略(ConcreteStrategy):实现抽象策略类中的相关的算法或操作。
我们首先来写一个简单的策略模式,然后再结合实际应用进行扩展,进而思考其在实际开发中的使用方法。
(给大家推荐前辈写的一篇不错的博文,研磨设计模式之 策略模式http://www.uml.org.cn/sjms/201009092.asp)
我们这个简单的策略假设就是想让不同的策略来实现某个算法(algorithm),抽象类如下:
- package com.icecode.demo.strategy;
- /**
- * 抽象策略
- * @author zhanche
- *
- */
- public abstract class AbstractStrategy {
- /**
- * 某个希望有不同策略实现的算法
- */
- public abstract void algorithm();
- }
package com.icecode.demo.strategy;
/**
* 抽象策略
* @author zhanche
*
*/
public abstract class AbstractStrategy {
/**
* 某个希望有不同策略实现的算法
*/
public abstract void algorithm();
}
算法algorithm的具体实现策略类ConcreteStrategy1和ConcreteStrategy2如下:
- package com.icecode.demo.strategy.impl;
- import com.icecode.demo.strategy.AbstractStrategy;
- /**
- * 对算法的第一种具体实现策略
- * @author zhanche
- *
- */
- public class ConcreteStrategy1 extends AbstractStrategy {
- @Override
- public void algorithm() {
- System.out.println("----------------我是策略一算法----------------");
- }
- }
- package com.icecode.demo.strategy.impl;
- import com.icecode.demo.strategy.AbstractStrategy;
- /**
- * 对算法的第二种具体实现策略
- * @author zhanche
- *
- */
- public class ConcreteStrategy2 extends AbstractStrategy {
- @Override
- public void algorithm() {
- System.out.println("----------------我是策略二算法----------------");
- }
- }
package com.icecode.demo.strategy.impl;
import com.icecode.demo.strategy.AbstractStrategy;
/**
* 对算法的第一种具体实现策略
* @author zhanche
*
*/
public class ConcreteStrategy1 extends AbstractStrategy {
@Override
public void algorithm() {
System.out.println("----------------我是策略一算法----------------");
}
}
package com.icecode.demo.strategy.impl;
import com.icecode.demo.strategy.AbstractStrategy;
/**
* 对算法的第二种具体实现策略
* @author zhanche
*
*/
public class ConcreteStrategy2 extends AbstractStrategy {
@Override
public void algorithm() {
System.out.println("----------------我是策略二算法----------------");
}
}
环境角色的实现如下:
- package com.icecode.demo.context;
- import com.icecode.demo.strategy.AbstractStrategy;
- /**
- * 环境角色,主要完成对特定策略的调用
- * @author zhanche
- *
- */
- public class Context {
- private AbstractStrategy strategy;
- public Context(AbstractStrategy strategy) {
- this.strategy = strategy;
- }
- public void algorithm() {
- this.strategy.algorithm();
- }
- }
package com.icecode.demo.context;
import com.icecode.demo.strategy.AbstractStrategy;
/**
* 环境角色,主要完成对特定策略的调用
* @author zhanche
*
*/
public class Context {
private AbstractStrategy strategy;
public Context(AbstractStrategy strategy) {
this.strategy = strategy;
}
public void algorithm() {
this.strategy.algorithm();
}
}
下面简单写一个客户端测试的代码:
- package com.icecode.demo;
- import com.icecode.demo.context.Context;
- import com.icecode.demo.strategy.impl.ConcreteStrategy1;
- import com.icecode.demo.strategy.impl.ConcreteStrategy2;
- /**
- * 策略模式测试类
- * @author zhanche
- *
- */
- public class Client {
- /**
- * @param args
- */
- public static void main(String[] args) {
- Context context = new Context(new ConcreteStrategy1());
- context.algorithm();
- context = new Context(new ConcreteStrategy2());
- context.algorithm();
- }
- }
package com.icecode.demo;
import com.icecode.demo.context.Context;
import com.icecode.demo.strategy.impl.ConcreteStrategy1;
import com.icecode.demo.strategy.impl.ConcreteStrategy2;
/**
* 策略模式测试类
* @author zhanche
*
*/
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
Context context = new Context(new ConcreteStrategy1());
context.algorithm();
context = new Context(new ConcreteStrategy2());
context.algorithm();
}
}
输出结果如下:
----------------我是策略一算法----------------
----------------我是策略二算法----------------
好吧,到此为止,一个简单的策略模式就写完了。但是,大家肯定有所疑惑,Context完全可以没有嘛,既然抽象策略AbstractStrategy已经持有algorithm这个接口,我们完全可以如下去写代码,让系统根据不同的实现执行不同的策略不就完了。代码可以如下:
- package com.icecode.demo;
- import com.icecode.demo.strategy.AbstractStrategy;
- import com.icecode.demo.strategy.impl.ConcreteStrategy1;
- import com.icecode.demo.strategy.impl.ConcreteStrategy2;
- /**
- * 策略模式测试类
- * @author zhanche
- *
- */
- public class Client {
- /**
- * @param args
- */
- public static void main(String[] args) {
- AbstractStrategy strategy = new ConcreteStrategy1();
- strategy.algorithm();
- strategy = new ConcreteStrategy2();
- strategy.algorithm();
- }
- }
package com.icecode.demo;
import com.icecode.demo.strategy.AbstractStrategy;
import com.icecode.demo.strategy.impl.ConcreteStrategy1;
import com.icecode.demo.strategy.impl.ConcreteStrategy2;
/**
* 策略模式测试类
* @author zhanche
*
*/
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
AbstractStrategy strategy = new ConcreteStrategy1();
strategy.algorithm();
strategy = new ConcreteStrategy2();
strategy.algorithm();
}
}
输出结果如下:
----------------我是策略一算法----------------
----------------我是策略二算法----------------
可见,2种方案都实现了同一个引用根据不同的实现执行特定的算法。是的,分析发现,在上面简单的应用中,Context的确可以取消。那么,Context这个角色为什么还要存在呢?
让我们考虑以下几种情况:
1、如果我们需要对不同策略中相同算法的参数,执行相同的安全性检查,我们如果没有环境角色Context,则只能在每个实现的开始部分,调用安全性检查代码;而有了Context这个角色,我们可以在调用Context的构造器时,统一进行安全性检查。这在我们的实现策略比较多的时候,比如说7、8个的时候,特别有用,可以大量减少冗余的代码量。
2、如果我们需要改变原有算法时,需要引进新的参数,如果没有Context,我们怎么办?一种办法是重载该算法,增加新的函数接口;另外一种办法是完全废弃原有的函数接口,重新写新的函数接口。毋庸置疑,这2种办法的代价都很大,尤其是如果这个新的参数只有部分实现策略中的该算法实现用到的时候。而我们使用Context就可以完全解决这个问题。
下面我们改造下上面那个基本策略模式,我们让策略模式也持有对Context的引用,这样的优点是可以在策略类里回调的Context里的所有可用的变量或函数等信息。此外,我们也增加一个新的实现策略类ConcreteStrategy3,具体代码如下所示:
- package com.icecode.demo.strategy;
- import com.icecode.demo.context.Context;
- /**
- * 抽象策略
- * @author zhanche
- *
- */
- public abstract class AbstractStrategy {
- /**
- * 某个希望有不同策略实现的算法
- */
- public abstract void algorithm(Context context);
- }
- package com.icecode.demo.strategy.impl;
- import com.icecode.demo.context.Context;
- import com.icecode.demo.strategy.AbstractStrategy;
- /**
- * 对算法的第一种具体实现策略
- * @author zhanche
- *
- */
- public class ConcreteStrategy1 extends AbstractStrategy {
- @Override
- public void algorithm(Context context) {
- System.out.println("----------------我是策略一算法----------------");
- }
- }
- package com.icecode.demo.strategy.impl;
- import com.icecode.demo.context.Context;
- import com.icecode.demo.strategy.AbstractStrategy;
- /**
- * 对算法的第二种具体实现策略
- * @author zhanche
- *
- */
- public class ConcreteStrategy2 extends AbstractStrategy {
- @Override
- public void algorithm(Context context) {
- System.out.println("----------------我是策略二算法----------------");
- }
- }
- package com.icecode.demo.strategy.impl;
- import com.icecode.demo.context.Context;
- import com.icecode.demo.strategy.AbstractStrategy;
- /**
- * 对算法的第三种具体实现策略
- * @author zhanche
- *
- */
- public class ConcreteStrategy3 extends AbstractStrategy {
- @Override
- public void algorithm(Context context) {
- System.out.println("----------------我是策略三算法----------------");
- }
- }
- package com.icecode.demo.context;
- import com.icecode.demo.strategy.AbstractStrategy;
- /**
- * 环境角色,主要完成对特定策略的调用
- * @author zhanche
- *
- */
- public class Context {
- /**
- * 持有对策略的引用
- */
- private AbstractStrategy strategy;
- /**
- * 算法入口
- */
- public void algorithm() {
- this.strategy.algorithm(this);
- }
- }
package com.icecode.demo.strategy;
import com.icecode.demo.context.Context;
/**
* 抽象策略
* @author zhanche
*
*/
public abstract class AbstractStrategy {
/**
* 某个希望有不同策略实现的算法
*/
public abstract void algorithm(Context context);
}
package com.icecode.demo.strategy.impl;
import com.icecode.demo.context.Context;
import com.icecode.demo.strategy.AbstractStrategy;
/**
* 对算法的第一种具体实现策略
* @author zhanche
*
*/
public class ConcreteStrategy1 extends AbstractStrategy {
@Override
public void algorithm(Context context) {
System.out.println("----------------我是策略一算法----------------");
}
}
package com.icecode.demo.strategy.impl;
import com.icecode.demo.context.Context;
import com.icecode.demo.strategy.AbstractStrategy;
/**
* 对算法的第二种具体实现策略
* @author zhanche
*
*/
public class ConcreteStrategy2 extends AbstractStrategy {
@Override
public void algorithm(Context context) {
System.out.println("----------------我是策略二算法----------------");
}
}
package com.icecode.demo.strategy.impl;
import com.icecode.demo.context.Context;
import com.icecode.demo.strategy.AbstractStrategy;
/**
* 对算法的第三种具体实现策略
* @author zhanche
*
*/
public class ConcreteStrategy3 extends AbstractStrategy {
@Override
public void algorithm(Context context) {
System.out.println("----------------我是策略三算法----------------");
}
}
package com.icecode.demo.context;
import com.icecode.demo.strategy.AbstractStrategy;
/**
* 环境角色,主要完成对特定策略的调用
* @author zhanche
*
*/
public class Context {
/**
* 持有对策略的引用
*/
private AbstractStrategy strategy;
/**
* 算法入口
*/
public void algorithm() {
this.strategy.algorithm(this);
}
}
好了,现在我们想这样改变需求,ConcreteStrategy2和ConcreteStrategy3里对algorithm算法的实现,需要统计两个新的信息,分别用parameter1和parameter2来表示,同时要统计所有实现策略类里,对algorithm算法调用的次数。
如果没有Context这个角色,又需要做到客户端调用的时候代码改动尽量少,相信大家的做法只好是改抽象策略类和实现策略类里的algorithm算法。但是这样实现策略ConcreteStrategy1可能不愿意了,因为他并不需要新增加的参数;此外,对所有实现类里algorithm算法调用的统计也没有一个统一的入口,需要在每个algorithm实现中,插入一个计数代码。但是如果有了环境角色Context,一切就变得很简单了,我们不需要改动抽象策略类,和实现策略类ConcreteStrategy1,只需要改需求发生变化相关的类,且看下面的代码:
- package com.icecode.demo.context;
- import com.icecode.demo.strategy.AbstractStrategy;
- /**
- * 环境角色,主要完成对特定策略的调用
- * @author zhanche
- *
- */
- public class Context {
- /**
- * 持有对策略的引用
- */
- private AbstractStrategy strategy;
- /**
- * parameter1、parameter2只是ConcreteStrategy2ConcreteStrategy3需要使用的参数,
- * 而ConcreteStrategy1不使用
- */
- private int parameter1;
- private int parameter2;
- //count用来统计所有策略的算法algorithm调用的总次数
- public static int count = 0;
- public Context(AbstractStrategy strategy) {
- this.strategy = strategy;
- }
- public Context(AbstractStrategy strategy, int parameter1, int parameter2) {
- super();
- this.strategy = strategy;
- this.parameter1 = parameter1;
- this.parameter2 = parameter2;
- }
- public int getParameter1() {
- return parameter1;
- }
- public int getParameter2() {
- return parameter2;
- }
- /**
- * 算法入口
- */
- public void algorithm() {
- count++;
- System.out.println("------------这是第"+count+"次调用algorithm算法--------");
- this.strategy.algorithm(this);
- }
- }
- package com.icecode.demo.strategy.impl;
- import com.icecode.demo.context.Context;
- import com.icecode.demo.strategy.AbstractStrategy;
- /**
- * 对算法的第一种具体实现策略
- * @author zhanche
- *
- */
- public class ConcreteStrategy1 extends AbstractStrategy {
- @Override
- public void algorithm(Context context) {
- System.out.println("----------------我是策略一算法----------------");
- }
- }
- package com.icecode.demo.strategy.impl;
- import com.icecode.demo.context.Context;
- import com.icecode.demo.strategy.AbstractStrategy;
- /**
- * 对算法的第二种具体实现策略
- * @author zhanche
- *
- */
- public class ConcreteStrategy2 extends AbstractStrategy {
- @Override
- public void algorithm(Context context) {
- System.out.println("----------------我是策略二算法----------------");
- System.out.println("------------------我需要的参数parameter1="+context.getParameter1());
- System.out.println("------------------我需要的参数parameter2="+context.getParameter2());
- }
- }
- package com.icecode.demo.strategy.impl;
- import com.icecode.demo.context.Context;
- import com.icecode.demo.strategy.AbstractStrategy;
- /**
- * 对算法的第三种具体实现策略
- * @author zhanche
- *
- */
- public class ConcreteStrategy3 extends AbstractStrategy {
- @Override
- public void algorithm(Context context) {
- System.out.println("----------------我是策略二算法----------------");
- System.out.println("------------------我需要的参数parameter1="+context.getParameter1());
- System.out.println("------------------我需要的参数parameter2="+context.getParameter2());
- }
- }
package com.icecode.demo.context;
import com.icecode.demo.strategy.AbstractStrategy;
/**
* 环境角色,主要完成对特定策略的调用
* @author zhanche
*
*/
public class Context {
/**
* 持有对策略的引用
*/
private AbstractStrategy strategy;
/**
* parameter1、parameter2只是ConcreteStrategy2ConcreteStrategy3需要使用的参数,
* 而ConcreteStrategy1不使用
*/
private int parameter1;
private int parameter2;
//count用来统计所有策略的算法algorithm调用的总次数
public static int count = 0;
public Context(AbstractStrategy strategy) {
this.strategy = strategy;
}
public Context(AbstractStrategy strategy, int parameter1, int parameter2) {
super();
this.strategy = strategy;
this.parameter1 = parameter1;
this.parameter2 = parameter2;
}
public int getParameter1() {
return parameter1;
}
public int getParameter2() {
return parameter2;
}
/**
* 算法入口
*/
public void algorithm() {
count++;
System.out.println("------------这是第"+count+"次调用algorithm算法--------");
this.strategy.algorithm(this);
}
}
package com.icecode.demo.strategy.impl;
import com.icecode.demo.context.Context;
import com.icecode.demo.strategy.AbstractStrategy;
/**
* 对算法的第一种具体实现策略
* @author zhanche
*
*/
public class ConcreteStrategy1 extends AbstractStrategy {
@Override
public void algorithm(Context context) {
System.out.println("----------------我是策略一算法----------------");
}
}
package com.icecode.demo.strategy.impl;
import com.icecode.demo.context.Context;
import com.icecode.demo.strategy.AbstractStrategy;
/**
* 对算法的第二种具体实现策略
* @author zhanche
*
*/
public class ConcreteStrategy2 extends AbstractStrategy {
@Override
public void algorithm(Context context) {
System.out.println("----------------我是策略二算法----------------");
System.out.println("------------------我需要的参数parameter1="+context.getParameter1());
System.out.println("------------------我需要的参数parameter2="+context.getParameter2());
}
}
package com.icecode.demo.strategy.impl;
import com.icecode.demo.context.Context;
import com.icecode.demo.strategy.AbstractStrategy;
/**
* 对算法的第三种具体实现策略
* @author zhanche
*
*/
public class ConcreteStrategy3 extends AbstractStrategy {
@Override
public void algorithm(Context context) {
System.out.println("----------------我是策略二算法----------------");
System.out.println("------------------我需要的参数parameter1="+context.getParameter1());
System.out.println("------------------我需要的参数parameter2="+context.getParameter2());
}
}
客户端测试的代码如下:
- package com.icecode.demo;
- import com.icecode.demo.context.Context;
- import com.icecode.demo.strategy.impl.ConcreteStrategy1;
- import com.icecode.demo.strategy.impl.ConcreteStrategy2;
- import com.icecode.demo.strategy.impl.ConcreteStrategy3;
- /**
- * 策略模式测试类
- * @author zhanche
- *
- */
- public class Client {
- /**
- * @param args
- */
- public static void main(String[] args) {
- Context context = new Context(new ConcreteStrategy1());
- context.algorithm();
- context = new Context(new ConcreteStrategy2(),100, 200);
- context.algorithm();
- context = new Context(new ConcreteStrategy3(), 100, 200);
- context.algorithm();
- }
- }
package com.icecode.demo;
import com.icecode.demo.context.Context;
import com.icecode.demo.strategy.impl.ConcreteStrategy1;
import com.icecode.demo.strategy.impl.ConcreteStrategy2;
import com.icecode.demo.strategy.impl.ConcreteStrategy3;
/**
* 策略模式测试类
* @author zhanche
*
*/
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
Context context = new Context(new ConcreteStrategy1());
context.algorithm();
context = new Context(new ConcreteStrategy2(),100, 200);
context.algorithm();
context = new Context(new ConcreteStrategy3(), 100, 200);
context.algorithm();
}
}
测试输出结果如下:
------------这是第1次调用algorithm算法--------
----------------我是策略一算法----------------
------------这是第2次调用algorithm算法--------
----------------我是策略二算法----------------
------------------我需要的参数parameter1=100
------------------我需要的参数parameter2=200
------------这是第3次调用algorithm算法--------
----------------我是策略三算法----------------
------------------我需要的参数parameter1=100
------------------我需要的参数parameter2=200
由以上分析可见,策略模式中,各个角色的功能都非常重要,虽然环境角色Context可以在某些简单的策略模式中不去使用,但是如果无法预测到各个实现策略功能和需求的变化,以及实现灵活性更好的策略模式,在使用策略模式进行架构时,一定要充分利用所有角色的功能。
策略模式(Strategy)
1 场景问题
1.1 报价管理
向客户报价,对于销售部门的人来讲,这是一个非常重大、非常复杂的问题,对不同的客户要报不同的价格,比如:
(1)对普通客户或者是新客户报的是全价
(2)对老客户报的价格,根据客户年限,给予一定的折扣
(3)对大客户报的价格,根据大客户的累计消费金额,给予一定的折扣
(4)还要考虑客户购买的数量和金额,比如:虽然是新用户,但是一次购买的数量非常大,或者是总金额非常高,也会有一定的折扣
(5)还有,报价人员的职务高低,也决定了他是否有权限对价格进行一定的浮动折扣
甚至在不同的阶段,对客户的报价也不同,一般情况是刚开始比较高,越接近成交阶段,报价越趋于合理。
总之,向客户报价是非常复杂的,因此在一些CRM(客户关系管理)的系统中,会有一个单独的报价管理模块,来处理复杂的报价功能。
为了演示的简洁性,假定现在需要实现一个简化的报价管理,实现如下的功能:
(1)对普通客户或者是新客户报全价
(2)对老客户报的价格,统一折扣5%
(3)对大客户报的价格,统一折扣10%
该怎么实现呢?
1.2 不用模式的解决方案
要实现对不同的人员报不同的价格的功能,无外乎就是判断起来麻烦点,也不多难,很快就有朋友能写出如下的实现代码,示例代码如下:
/** * 价格管理,主要完成计算向客户所报价格的功能 */ public class Price { /** * 报价,对不同类型的,计算不同的价格 * @param goodsPrice 商品销售原价 * @param customerType 客户类型 * @return 计算出来的,应该给客户报的价格 */ public double quote(double goodsPrice,String customerType){ if(customerType.equals("普通客户")){ System.out.println("对于新客户或者是普通客户,没有折扣"); return goodsPrice; }else if(customerType.equals("老客户")){ System.out.println("对于老客户,统一折扣5%"); return goodsPrice*(1-0.05); }else if(customerType.equals("大客户")){ System.out.println("对于大客户,统一折扣10%"); return goodsPrice*(1-0.1); } //其余人员都是报原价 return goodsPrice; } } |
1.3 有何问题
上面的写法是很简单的,也很容易想,但是仔细想想,这样实现,问题可不小,比如:
(1)第一个问题:价格类包含了所有计算报价的算法,使得价格类,尤其是报价这个方法比较庞杂,难以维护。
有朋友可能会想,这很简单嘛,把这些算法从报价方法里面拿出去,形成独立的方法不就可以解决这个问题了吗?据此写出如下的实现代码,示例代码如下:
/** * 价格管理,主要完成计算向客户所报价格的功能 */ public class Price { /** * 报价,对不同类型的,计算不同的价格 * @param goodsPrice 商品销售原价 * @param customerType 客户类型 * @return 计算出来的,应该给客户报的价格 */ public double quote(double goodsPrice,String customerType){ if(customerType.equals("普通客户")){ return this.calcPriceForNormal(goodsPrice); }else if(customerType.equals("老客户")){ return this.calcPriceForOld(goodsPrice); }else if(customerType.equals("大客户")){ return this.calcPriceForLarge(goodsPrice); } //其余人员都是报原价 return goodsPrice; } /** * 为新客户或者是普通客户计算应报的价格 * @param goodsPrice 商品销售原价 * @return 计算出来的,应该给客户报的价格 */ private double calcPriceForNormal(double goodsPrice){ System.out.println("对于新客户或者是普通客户,没有折扣"); return goodsPrice; } /** * 为老客户计算应报的价格 * @param goodsPrice 商品销售原价 * @return 计算出来的,应该给客户报的价格 */ private double calcPriceForOld(double goodsPrice){ System.out.println("对于老客户,统一折扣5%"); return goodsPrice*(1-0.05); } /** * 为大客户计算应报的价格 * @param goodsPrice 商品销售原价 * @return 计算出来的,应该给客户报的价格 */ private double calcPriceForLarge(double goodsPrice){ System.out.println("对于大客户,统一折扣10%"); return goodsPrice*(1-0.1); } } |
这样看起来,比刚开始稍稍好点,计算报价的方法会稍稍简单一点,这样维护起来也稍好一些,某个算法发生了变化,直接修改相应的私有方法就可以了。扩展起来也容易一点,比如要增加一个“战略合作客户”的类型,报价为直接8折,就只需要在价格类里面新增加一个私有的方法来计算新的价格,然后在计算报价的方法里面新添一个else-if即可。看起来似乎很不错了。
真的很不错了吗?
再想想,问题还是存在,只不过从计算报价的方法挪动到价格类里面了,假如有100个或者更多这样的计算方式,这会让这个价格类非常庞大,难以维护。而且,维护和扩展都需要去修改已有的代码,这是很不好的,违反了开-闭原则。
(2)第二个问题:经常会有这样的需要,在不同的时候,要使用不同的计算方式。
比如:在公司周年庆的时候,所有的客户额外增加3%的折扣;在换季促销的时候,普通客户是额外增加折扣2%,老客户是额外增加折扣3%,大客户是额外增加折扣5%。这意味着计算报价的方式会经常被修改,或者被切换。
通常情况下应该是被切换,因为过了促销时间,又还回到正常的价格体系上来了。而现在的价格类中计算报价的方法,是固定调用各种计算方式,这使得切换调用不同的计算方式很麻烦,每次都需要修改if-else里面的调用代码。
看到这里,可能有朋友会想,那么到底应该如何实现,才能够让价格类中的计算报价的算法,能很容易的实现可维护、可扩展,又能动态的切换变化呢?
2 解决方案
2.1 策略模式来解决
用来解决上述问题的一个合理的解决方案就是策略模式。那么什么是策略模式呢?
(1)策略模式定义
定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
(2)应用策略模式来解决的思路
仔细分析上面的问题,先来把它抽象一下,各种计算报价的计算方式就好比是具体的算法,而使用这些计算方式来计算报价的程序,就相当于是使用算法的客户。
再分析上面的实现方式,为什么会造成那些问题,根本原因,就在于算法和使用算法的客户是耦合的,甚至是密不可分的,在上面实现中,具体的算法和使用算法的客户是同一个类里面的不同方法。
现在要解决那些问题,按照策略模式的方式,应该先把所有的计算方式独立出来,每个计算方式做成一个单独的算法类,从而形成一系列的算法,并且为这一系列算法定义一个公共的接口,这些算法实现是同一接口的不同实现,地位是平等的,可以相互替换。这样一来,要扩展新的算法就变成了增加一个新的算法实现类,要维护某个算法,也只是修改某个具体的算法实现即可,不会对其它代码造成影响。也就是说这样就解决了可维护、可扩展的问题。
为了实现让算法能独立于使用它的客户,策略模式引入了一个上下文的对象,这个对象负责持有算法,但是不负责决定具体选用哪个算法,把选择算法的功能交给了客户,由客户选择好具体的算法后,设置到上下文对象里面,让上下文对象持有客户选择的算法,当客户通知上下文对象执行功能的时候,上下文对象会去转调具体的算法。这样一来,具体的算法和直接使用算法的客户是分离的。
具体的算法和使用它的客户分离过后,使得算法可独立于使用它的客户而变化,并且能够动态的切换需要使用的算法,只要客户端动态的选择使用不同的算法,然后设置到上下文对象中去,实际调用的时候,就可以调用到不同的算法。
2.2 模式结构和说明
策略模式的结构示意图如图1所示:
图1 策略模式结构示意图
Strategy:
策略接口,用来约束一系列具体的策略算法。Context使用这个接口来调用具体的策略实现定义的算法。
ConcreteStrategy:
具体的策略实现,也就是具体的算法实现。
Context:
上下文,负责和具体的策略类交互,通常上下文会持有一个真正的策略实现,上下文还可以让具体的策略类来获取上下文的数据,甚至让具体的策略类来回调上下文的方法。
2.3 策略模式示例代码
(1)首先来看策略,也就是定义算法的接口,示例代码如下:
/** * 策略,定义算法的接口 */ public interface Strategy { /** * 某个算法的接口,可以有传入参数,也可以有返回值 */ public void algorithmInterface(); } |
(2)该来看看具体的算法实现了,定义了三个,分别是ConcreteStrategyA、ConcreteStrategyB、ConcreteStrategyC,示例非常简单,由于没有具体算法的实现,三者也就是名称不同,示例代码如下:
/** * 实现具体的算法 */ public class ConcreteStrategyA implements Strategy { public void algorithmInterface() { //具体的算法实现 } } |
/** * 实现具体的算法 */ public class ConcreteStrategyB implements Strategy { public void algorithmInterface() { //具体的算法实现 } } |
/** * 实现具体的算法 */ public class ConcreteStrategyC implements Strategy { public void algorithmInterface() { //具体的算法实现 } } |
(3)再来看看上下文的实现,示例代码如下:
/** * 上下文对象,通常会持有一个具体的策略对象 */ public class Context { /** * 持有一个具体的策略对象 */ private Strategy strategy; /** * 构造方法,传入一个具体的策略对象 * @param aStrategy 具体的策略对象 */ public Context(Strategy aStrategy) { this.strategy = aStrategy; } /** * 上下文对客户端提供的操作接口,可以有参数和返回值 */ public void contextInterface() { //通常会转调具体的策略对象进行算法运算 strategy.algorithmInterface(); } } |
2.4 使用策略模式重写示例
要使用策略模式来重写前面报价的示例,大致有如下改变:
- 首先需要定义出算法的接口。
- 然后把各种报价的计算方式单独出来,形成算法类。
- 对于Price这个类,把它当做上下文,在计算报价的时候,不再需要判断,直接使用持有的具体算法进行运算即可。选择使用哪一个算法的功能挪出去,放到外部使用的客户端去。
这个时候,程序的结构如图2所示:
图2 使用策略模式实现示例的结构示意图
(1)先看策略接口,示例代码如下:
/** * 策略,定义计算报价算法的接口 */ public interface Strategy { /** * 计算应报的价格 * @param goodsPrice 商品销售原价 * @return 计算出来的,应该给客户报的价格 */ public double calcPrice(double goodsPrice); } |
(2)接下来看看具体的算法实现,不同的算法,实现也不一样,先看为新客户或者是普通客户计算应报的价格的实现,示例代码如下:
/** * 具体算法实现,为新客户或者是普通客户计算应报的价格 */ public class NormalCustomerStrategy implements Strategy{ public double calcPrice(double goodsPrice) { System.out.println("对于新客户或者是普通客户,没有折扣"); return goodsPrice; } } |
再看看为老客户计算应报的价格的实现,示例代码如下:
/** * 具体算法实现,为老客户计算应报的价格 */ public class OldCustomerStrategy implements Strategy{ public double calcPrice(double goodsPrice) { System.out.println("对于老客户,统一折扣5%"); return goodsPrice*(1-0.05); } } |
再看看为大客户计算应报的价格的实现,示例代码如下:
/** * 具体算法实现,为大客户计算应报的价格 */ public class LargeCustomerStrategy implements Strategy{ public double calcPrice(double goodsPrice) { System.out.println("对于大客户,统一折扣10%"); return goodsPrice*(1-0.1); } } |
(3)接下来看看上下文的实现,也就是原来的价格类,它的变化比较大,主要有:
- 原来那些私有的,用来做不同计算的方法,已经去掉了,独立出去做成了算法类
- 原来报价方法里面,对具体计算方式的判断,去掉了,让客户端来完成选择具体算法的功能
- 新添加持有一个具体的算法实现,通过构造方法传入
- 原来报价方法的实现,变化成了转调具体算法来实现
示例代码如下:
/** * 价格管理,主要完成计算向客户所报价格的功能 */ public class Price { /** * 持有一个具体的策略对象 */ private Strategy strategy = null; /** * 构造方法,传入一个具体的策略对象 * @param aStrategy 具体的策略对象 */ public Price(Strategy aStrategy){ this.strategy = aStrategy; } /** * 报价,计算对客户的报价 * @param goodsPrice 商品销售原价 * @return 计算出来的,应该给客户报的价格 */ public double quote(double goodsPrice){ return this.strategy.calcPrice(goodsPrice); } } |
(4)写个客户端来测试运行一下,好加深体会,示例代码如下:
public class Client { public static void main(String[] args) { //1:选择并创建需要使用的策略对象 Strategy strategy = new LargeCustomerStrategy (); //2:创建上下文 Price ctx = new Price(strategy); //3:计算报价 double quote = ctx.quote(1000); System.out.println("向客户报价:"+quote); } } |
运行一下,看看效果。
你可以修改使用不同的策略算法具体实现,现在用的是LargeCustomerStrategy,你可以尝试修改成其它两种实现,试试看,体会一下切换算法的容易性。
3 模式讲解
3.1 认识策略模式
(1)策略模式的功能
策略模式的功能是把具体的算法实现,从具体的业务处理里面独立出来,把它们实现成为单独的算法类,从而形成一系列的算法,并让这些算法可以相互替换。
策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活、具有更好的维护性和扩展性。
(2)策略模式和if-else语句
看了前面的示例,很多朋友会发现,每个策略算法具体实现的功能,就是原来在if-else结构中的具体实现。
没错,其实多个if-elseif语句表达的就是一个平等的功能结构,你要么执行if,要不你就执行else,或者是elseif,这个时候,if块里面的实现和else块里面的实现从运行地位上来讲就是平等的。
而策略模式就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下文来与具体的策略类进行交互。
因此多个if-else语句可以考虑使用策略模式。
(3)算法的平等性
策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正是因为这个平等性,才能实现算法之间可以相互替换。
所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的。
所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现。
(4)谁来选择具体的策略算法
在策略模式中,可以在两个地方来进行具体策略的选择。
一个是在客户端,在使用上下文的时候,由客户端来选择具体的策略算法,然后把这个策略算法设置给上下文。前面的示例就是这种情况。
还有一个是客户端不管,由上下文来选择具体的策略算法,这个在后面讲容错恢复的时候给大家演示一下。
(5)Strategy的实现方式
在前面的示例中,Strategy都是使用的接口来定义的,这也是常见的实现方式。但是如果多个算法具有公共功能的话,可以把Strategy实现成为抽象类,然后把多个算法的公共功能实现到Strategy里面。
(6)运行时策略的唯一性
运行期间,策略模式在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态的在不同的策略实现中切换,但是同时只能使用一个。
(7)增加新的策略
在前面的示例里面,体会到了策略模式中切换算法的方便,但是增加一个新的算法会怎样呢?比如现在要实现如下的功能:对于公司的“战略合作客户”,统一8折。
其实很简单,策略模式可以让你很灵活的扩展新的算法。具体的做法是:先写一个策略算法类来实现新的要求,然后在客户端使用的时候指定使用新的策略算法类就可以了。
还是通过示例来说明。先添加一个实现要求的策略类,示例代码如下:
/** * 具体算法实现,为战略合作客户客户计算应报的价格 */ public class CooperateCustomerStrategy implements Strategy{ public double calcPrice(double goodsPrice) { System.out.println("对于战略合作客户,统一8折"); return goodsPrice*0.8; } } |
然后在客户端指定使用策略的时候指定新的策略算法实现,示例如下:
public class Client2 { public static void main(String[] args) { //1:选择并创建需要使用的策略对象 Strategy strategy = new CooperateCustomerStrategy (); //2:创建上下文 Price ctx = new Price(strategy);
//3:计算报价 double quote = ctx.quote(1000); System.out.println("向客户报价:"+quote); } } |
除了加粗部分变动外,客户端没有其他的变化。
运行客户端,测试看看,好好体会一下。
除了客户端发生变化外,已有的上下文、策略接口定义和策略的已有实现,都不需要做任何的修改,可见能很方便的扩展新的策略算法。
(8)策略模式调用顺序示意图
策略模式的调用顺序,有两种常见的情况,一种如同前面的示例,具体如下:
a:先是客户端来选择并创建具体的策略对象
b:然后客户端创建上下文
c:接下来客户端就可以调用上下文的方法来执行功能了,在调用的时候,从客户端传入算法需要的参数
d:上下文接到客户的调用请求,会把这个请求转发给它持有的Strategy
这种情况的调用顺序示意图如图3所示:
图3 策略模式调用顺序示意图一
策略模式调用还有一种情况,就是把Context当做参数来传递给Strategy,这种方式的调用顺序图,在讲具体的Context和Strategy的关系时再给出。
3.2 容错恢复机制
容错恢复机制是应用程序开发中非常常见的功能。那么什么是容错恢复呢?简单点说就是:程序运行的时候,正常情况下应该按照某种方式来做,如果按照某种方式来做发生错误的话,系统并不会崩溃,也不会就此不能继续向下运行了,而是有容忍出错的能力,不但能容忍程序运行出现错误,还提供出现错误后的备用方案,也就是恢复机制,来代替正常执行的功能,使程序继续向下运行。
举个实际点的例子吧,比如在一个系统中,所有对系统的操作都要有日志记录,而且这个日志还需要有管理界面,这种情况下通常会把日志记录在数据库里面,方便后续的管理,但是在记录日志到数据库的时候,可能会发生错误,比如暂时连不上数据库了,那就先记录在文件里面,然后在合适的时候把文件中的记录再转录到数据库中。
对于这样的功能的设计,就可以采用策略模式,把日志记录到数据库和日志记录到文件当作两种记录日志的策略,然后在运行期间根据需要进行动态的切换。
在这个例子的实现中,要示范由上下文来选择具体的策略算法,前面的例子都是由客户端选择好具体的算法,然后设置到上下文中。
下面还是通过代码来示例一下。
(1)先定义日志策略接口,很简单,就是一个记录日志的方法,示例代码如下:
/** * 日志记录策略的接口 */ public interface LogStrategy { /** * 记录日志 * @param msg 需记录的日志信息 */ public void log(String msg); } |
(2)实现日志策略接口,先实现默认的数据库实现,假设如果日志的长度超过长度就出错,制造错误的是一个最常见的运行期错误,示例代码如下:
/** * 把日志记录到数据库 */ public class DbLog implements LogStrategy{ public void log(String msg) { //制造错误 if(msg!=null && msg.trim().length()>5){ int a = 5/0; } System.out.println("现在把 '"+msg+"' 记录到数据库中"); } } |
接下来实现记录日志到文件中去,示例代码如下:
/** * 把日志记录到文件 */ public class FileLog implements LogStrategy{ public void log(String msg) { System.out.println("现在把 '"+msg+"' 记录到文件中"); } } |
(3)接下来定义使用这些策略的上下文,注意这次是在上下文里面实现具体策略算法的选择,所以不需要客户端来指定具体的策略算法了,示例代码如下:
(4)看看现在的客户端,没有了选择具体实现策略算法的工作,变得非常简单,故意多调用一次,可以看出不同的效果,示例代码如下:
(5)小结一下,通过上面的示例,会看到策略模式的一种简单应用,也顺便了解一下基本的容错恢复机制的设计和实现。在实际的应用中,需要设计容错恢复的系统一般要求都比较高,应用也会比较复杂,但是基本的思路是差不多的。
3.3 Context和Strategy的关系
在策略模式中,通常是上下文使用具体的策略实现对象,反过来,策略实现对象也可以从上下文获取所需要的数据,因此可以将上下文当参数传递给策略实现对象,这种情况下上下文和策略实现对象是紧密耦合的。
在这种情况下,上下文封装着具体策略对象进行算法运算所需要的数据,具体策略对象通过回调上下文的方法来获取这些数据。
甚至在某些情况下,策略实现对象还可以回调上下文的方法来实现一定的功能,这种使用场景下,上下文变相充当了多个策略算法实现的公共接口,在上下文定义的方法可以当做是所有或者是部分策略算法使用的公共功能。
但是请注意,由于所有的策略实现对象都实现同一个策略接口,传入同一个上下文,可能会造成传入的上下文数据的浪费,因为有的算法会使用这些数据,而有的算法不会使用,但是上下文和策略对象之间交互的开销是存在的了。
还是通过例子来说明。
1:工资支付的实现思路
考虑这样一个功能:工资支付方式的问题,很多企业的工资支付方式是很灵活的,可支付方式是比较多的,比如:人民币现金支付、美元现金支付、银行转账到工资帐户、银行转账到工资卡;一些创业型的企业为了留住骨干员工,还可能有:工资转股权等等方式。总之一句话,工资支付方式很多。
随着公司的发展,会不断有新的工资支付方式出现,这就要求能方便的扩展;另外工资支付方式不是固定的,是由公司和员工协商确定的,也就是说可能不同的员工采用的是不同的支付方式,甚至同一个员工,不同时间采用的支付方式也可能会不同,这就要求能很方便的切换具体的支付方式。
要实现这样的功能,策略模式是一个很好的选择。在实现这个功能的时候,不同的策略算法需要的数据是不一样,比如:现金支付就不需要银行帐号,而银行转账就需要帐号。这就导致在设计策略接口中的方法时,不太好确定参数的个数,而且,就算现在把所有的参数都列上了,今后扩展呢?难道再来修改策略接口吗?如果这样做,那无异于一场灾难,加入一个新策略,就需要修改接口,然后修改所有已有的实现,不疯掉才怪!那么到底如何实现,在今后扩展的时候才最方便呢?
解决方案之一,就是把上下文当做参数传递给策略对象,这样一来,如果要扩展新的策略实现,只需要扩展上下文就可以了,已有的实现不需要做任何的修改。
这样是不是能很好的实现功能,并具有很好的扩展性呢?还是通过代码示例来具体的看。假设先实现人民币现金支付和美元现金支付这两种支付方式,然后就进行使用测试,然后再来添加银行转账到工资卡的支付方式,看看是不是能很容易的与已有的实现结合上。
2:实现代码示例
(1)先定义工资支付的策略接口,就是定义一个支付工资的方法,示例代码如下:
/** * 支付工资的策略的接口,公司有多种支付工资的算法 * 比如:现金、银行卡、现金加股票、现金加期权、美元支付等等 */ public interface PaymentStrategy { /** * 公司给某人真正支付工资 * @param ctx 支付工资的上下文,里面包含算法需要的数据 */ public void pay(PaymentContext ctx); } |
(2)定义好了工资支付的策略接口,该来考虑如何实现这多种支付策略了。
为了演示的简单,这里先简单实现人民币现金支付和美元现金支付方式,当然并不真的去实现跟银行的交互,只是示意一下。
人民币现金支付的策略实现,示例代码如下:
/** * 人民币现金支付 */ public class RMBCash implements PaymentStrategy{ public void pay(PaymentContext ctx) { System.out.println("现在给"+ctx.getUserName() +"人民币现金支付"+ctx.getMoney()+"元"); } } |
同样的实现美元现金支付的策略,示例代码如下:
/** * 美元现金支付 */ public class DollarCash implements PaymentStrategy{ public void pay(PaymentContext ctx) { System.out.println("现在给"+ctx.getUserName() +"美元现金支付"+ctx.getMoney()+"元"); } } |
(3)该来看支付上下文的实现了,当然这个使用支付策略的上下文,是需要知道具体使用哪一个支付策略的,一般由客户端来确定具体使用哪一个具体的策略,然后上下文负责去真正执行。因此,这个上下文需要持有一个支付策略,而且是由客户端来配置它。示例代码如下:
/** * 支付工资的上下文,每个人的工资不同,支付方式也不同 */ public class PaymentContext { /** * 应被支付工资的人员,简单点,用姓名来代替 */ private String userName = null; /** * 应被支付的工资的金额 */ private double money = 0.0; /** * 支付工资的方式策略的接口 */ private PaymentStrategy strategy = null; /** * 构造方法,传入被支付工资的人员,应支付的金额和具体的支付策略 * @param userName 被支付工资的人员 * @param money 应支付的金额 * @param strategy 具体的支付策略 */ public PaymentContext(String userName,double money, PaymentStrategy strategy){ this.userName = userName; this.money = money; this.strategy = strategy; } public String getUserName() { return userName; } public double getMoney() { return money; } /** * 立即支付工资 */ public void payNow(){ //使用客户希望的支付策略来支付工资 this.strategy.pay(this); } } |
(4)准备好了支付工资的各种策略,下面看看如何使用这些策略来真正支付工资,很简单,客户端是使用上下文来使用具体的策略的,而且是客户端来确定具体的策略,就是客户端创建哪个策略,最终就运行哪一个策略,各个策略之间是可以动态切换的,示例代码如下:
public class Client { public static void main(String[] args) { //创建相应的支付策略 PaymentStrategy strategyRMB = new RMBCash(); PaymentStrategy strategyDollar = new DollarCash(); //准备小李的支付工资上下文 PaymentContext ctx1 = new PaymentContext("小李",5000,strategyRMB); //向小李支付工资 ctx1.payNow(); //切换一个人,给petter支付工资 PaymentContext ctx2 = new PaymentContext("Petter",8000,strategyDollar); ctx2.payNow(); } } |
运行一下,看看效果,运行结果如下:
现在给小李人民币现金支付5000.0元 现在给Petter美元现金支付8000.0元 |
3:扩展示例,实现方式一
经过上面的测试可以看出,通过使用策略模式,已经实现好了两种支付方式了。如果现在要增加一种支付方式,要求能支付到银行卡,该怎么扩展最简单呢?
应该新增加一种支付到银行卡的策略实现,然后通过继承来扩展支付上下文,在里面添加新的支付方式需要的新的数据,比如银行卡账户,然后在客户端使用新的上下文和新的策略实现就可以了,这样已有的实现都不需要改变,完全遵循开-闭原则。
先看看扩展的支付上下文对象的实现,示例代码如下:
/** * 扩展的支付上下文对象 */ public class PaymentContext2 extends PaymentContext { /** * 银行帐号 */ private String account = null; /** * 构造方法,传入被支付工资的人员,应支付的金额和具体的支付策略 * @param userName 被支付工资的人员 * @param money 应支付的金额 * @param account 支付到的银行帐号 * @param strategy 具体的支付策略 */ public PaymentContext2(String userName,double money, String account,PaymentStrategy strategy){ super(userName,money,strategy); this.account = account; } public String getAccount() { return account; } } |
然后看看新的策略算法的实现,示例代码如下:
/** * 支付到银行卡 */ public class Card implements PaymentStrategy{ public void pay(PaymentContext ctx) { //这个新的算法自己知道要使用扩展的支付上下文,所以强制造型一下 PaymentContext2 ctx2 = (PaymentContext2)ctx; System.out.println("现在给"+ctx2.getUserName()+"的" +ctx2.getAccount()+"帐号支付了"+ctx2.getMoney()+"元"); //连接银行,进行转帐,就不去管了 } } |
最后看看客户端怎么使用这个新的策略呢?原有的代码不变,直接添加新的测试就可以了,示例代码如下:
public class Client { public static void main(String[] args) { //创建相应的支付策略 PaymentStrategy strategyRMB = new RMBCash(); PaymentStrategy strategyDollar = new DollarCash(); //准备小李的支付工资上下文 PaymentContext ctx1 = new PaymentContext("小李",5000,strategyRMB); //向小李支付工资 ctx1.payNow(); //切换一个人,给petter支付工资 PaymentContext ctx2 = new PaymentContext("Petter",8000,strategyDollar); ctx2.payNow(); //测试新添加的支付方式 PaymentStrategy strategyCard = new Card(); PaymentContext ctx3 = new PaymentContext2( "小王",9000,"010998877656",strategyCard); ctx3.payNow(); } } |
再次测试,体会一下,运行结果如下:
现在给小李人民币现金支付5000.0元 现在给Petter美元现金支付8000.0元 现在给小王的010998877656帐号支付了9000.0元 |
4:扩展示例,实现方式二
同样还是实现上面这个功能:现在要增加一种支付方式,要求能支付到银行卡。
(1)上面这种实现方式,是通过扩展上下文对象来准备新的算法需要的数据。还有另外一种方式,那就是通过策略的构造方法来传入新算法需要的数据。这样实现的话,就不需要扩展上下文了,直接添加新的策略算法实现就好了。示例代码如下:
/** * 支付到银行卡 */ public class Card2 implements PaymentStrategy{ /** * 帐号信息 */ private String account = ""; /** * 构造方法,传入帐号信息 * @param account 帐号信息 */ public Card2(String account){ this.account = account; } public void pay(PaymentContext ctx) { System.out.println("现在给"+ctx.getUserName()+"的" +this.account+"帐号支付了"+ctx.getMoney()+"元"); //连接银行,进行转帐,就不去管了 } } |
(2)直接在客户端测试就可以了,测试示例代码如下:
public class Client { public static void main(String[] args) { //测试新添加的支付方式 PaymentStrategy strategyCard2 = new Card2("010998877656"); PaymentContext ctx4 = new PaymentContext("小张",9000,strategyCard2); ctx4.payNow(); } } |
运行看看,好好体会一下。
(3)现在有这么两种扩展的实现方式,到底使用哪一种呢?或者是哪种实现更好呢?下面来比较一下:
对于扩展上下文的方式:这样实现,所有策略的实现风格更统一,策略需要的数据都统一从上下文来获取,这样在使用方法上也很统一;另外,在上下文中添加新的数据,别的相应算法也可以用得上,可以视为公共的数据。但缺点也很明显,如果这些数据只有一个特定的算法来使用,那么这些数据有些浪费;另外每次添加新的算法都去扩展上下文,容易形成复杂的上下文对象层次,也未见得有必要。
对于在策略算法的实现上添加自己需要的数据的方式:这样实现,比较好想,实现简单。但是缺点也很明显,跟其它策略实现的风格不一致,其它策略都是从上下文中来获取数据,而这个策略的实现一部分数据来自上下文,一部分数据来自自己,有些不统一;另外,这样一来,外部使用这些策略算法的时候也不一样了,不太好以一个统一的方式来动态切换策略算法。
两种实现各有优劣,至于如何选择,那就具体问题,具体的分析了。
5:另一种策略模式调用顺序示意图
策略模式调用还有一种情况,就是把Context当做参数来传递给Strategy,也就是本例示范的这种方式,这个时候策略模式的调用顺序如图4所示:
图4 策略模式调用顺序示意图二
3.4 策略模式结合模板方法模式
在实际应用策略模式的过程中,经常会出现这样一种情况,就是发现这一系列算法的实现上存在公共功能,甚至这一系列算法的实现步骤都是一样的,只是在某些局部步骤上有所不同,这个时候,就需要对策略模式进行些许的变化使用了。
对于一系列算法的实现上存在公共功能的情况,策略模式可以有如下三种实现方式:
- 一个是在上下文当中实现公共功能,让所有具体的策略算法回调这些方法。
- 另外一种情况就是把策略的接口改成抽象类,然后在里面实现具体算法的公共功能。
- 还有一种情况是给所有的策略算法定义一个抽象的父类,让这个父类去实现策略的接口,然后在这个父类里面去实现公共的功能。
更进一步,如果这个时候发现“一系列算法的实现步骤都是一样的,只是在某些局部步骤上有所不同”的情况,那就可以在这个抽象类里面定义算法实现的骨架,然后让具体的策略算法去实现变化的部分。这样的一个结构自然就变成了策略模式来结合模板方法模式了,那个抽象类就成了模板方法模式的模板类。
在上一章我们讨论过模板方法模式来结合策略模式的方式,也就是主要的结构是模板方法模式,局部采用策略模式。而这里讨论的是策略模式来结合模板方法模式,也就是主要的结构是策略模式,局部实现上采用模板方法模式。通过这个示例也可以看出来,模式之间的结合是没有定势的,要具体问题具体分析。
此时策略模式结合模板方法模式的系统结构如下图5所示:
图5 策略模式结合模板方法模式的结构示意图
还是用实际的例子来说吧,比如上面那个记录日志的例子,如果现在需要在所有的消息前面都添加上日志时间,也就是说现在记录日志的步骤变成了:第一步为日志消息添加日志时间;第二步具体记录日志。
那么该怎么实现呢?
(1)记录日志的策略接口没有变化,为了看起来方便,还是示例一下,示例代码如下:
/** * 日志记录策略的接口 */ public interface LogStrategy { /** * 记录日志 * @param msg 需记录的日志信息 */ public void log(String msg); } |
(2)增加一个实现这个策略接口的抽象类,在里面定义记录日志的算法骨架,相当于模板方法模式的模板,示例代码如下:
/** * 实现日志策略的抽象模板,实现给消息添加时间 */ public abstract class LogStrategyTemplate implements LogStrategy{ public final void log(String msg) { //第一步:给消息添加记录日志的时间 DateFormat df = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss SSS"); msg = df.format(new java.util.Date())+" 内容是:"+ msg; //第二步:真正执行日志记录 doLog(msg); } /** * 真正执行日志记录,让子类去具体实现 * @param msg 需记录的日志信息 */ protected abstract void doLog(String msg); } |
(3)这个时候那两个具体的日志算法实现也需要做些改变,不再直接实现策略接口了,而是继承模板,实现模板方法了。这个时候记录日志到数据库的类,示例代码如下:
/** * 把日志记录到数据库 */ public class DbLog extends LogStrategyTemplate{ //除了定义上发生了改变外,具体的实现没变 public void doLog(String msg) { //制造错误 if(msg!=null && msg.trim().length()>5){ int a = 5/0; } System.out.println("现在把 '"+msg+"' 记录到数据库中"); } } |
同理实现记录日志到文件的类如下:
/** * 把日志记录到数据库 */ public class FileLog extends LogStrategyTemplate{ public void doLog(String msg) { System.out.println("现在把 '"+msg+"' 记录到文件中"); } } |
(4)算法实现的改变不影响使用算法的上下文,上下文跟前面一样,示例代码如下:
/** * 日志记录的上下文 */ public class LogContext { /** * 记录日志的方法,提供给客户端使用 * @param msg 需记录的日志信息 */ public void log(String msg){ //在上下文里面,自行实现对具体策略的选择 //优先选用策略:记录到数据库 LogStrategy strategy = new DbLog(); try{ strategy.log(msg); }catch(Exception err){ //出错了,那就记录到文件中 strategy = new FileLog(); strategy.log(msg); } } } |
(5)客户端跟以前也一样,示例代码如下:
public class Client { public static void main(String[] args) { LogContext log = new LogContext(); log.log("记录日志"); log.log("再次记录日志"); } } |
运行一下客户端再次测试看看,体会一下,看看结果是否带上了时间。
通过这个示例,好好体会一下策略模式和模板方法模式的组合使用,在实用开发中是很常见的方式。
3.5 策略模式的优缺点
- 定义一系列算法
策略模式的功能就是定义一系列算法,实现让这些算法可以相互替换。所以会为这一系列算法定义公共的接口,以约束一系列算法要实现的功能。如果这一系列算法具有公共功能,可以把策略接口实现成为抽象类,把这些公共功能实现到父类里面,对于这个问题,前面讲了三种处理方法,这里就不罗嗦了。 - 避免多重条件语句
根据前面的示例会发现,策略模式的一系列策略算法是平等的,可以互换的,写在一起就是通过if-else结构来组织,如果此时具体的算法实现里面又有条件语句,就构成了多重条件语句,使用策略模式能避免这样的多重条件语句。
如下示例来演示了不使用策略模式的多重条件语句,示例代码如下:
public class OneClass { /** * 示范多重条件语句 * @param type 某个用于判断的类型 */ public void oneMethod(int type){ //使用策略模式的时候,这些算法的处理代码就被拿出去, //放到单独的算法实现类去了,这里就不再是多重条件了
if(type==1){ //算法一示范 //从某个地方获取这个s的值 String s = ""; //然后判断进行相应处理 if(s.indexOf("a") > 0){ //处理 }else{ //处理 } }else if(type==2){ //算法二示范 //从某个地方获取这个a的值 int a = 3; //然后判断进行相应处理 if(a > 10){ //处理 }else{ //处理 } } } } |
- 更好的扩展性
在策略模式中扩展新的策略实现非常容易,只要增加新的策略实现类,然后在选择使用策略的地方选择使用这个新的策略实现就好了。 - 客户必须了解每种策略的不同
策略模式也有缺点,比如让客户端来选择具体使用哪一个策略,这就可能会让客户需要了解所有的策略,还要了解各种策略的功能和不同,这样才能做出正确的选择,而且这样也暴露了策略的具体实现。 - 增加了对象数目
由于策略模式把每个具体的策略实现都单独封装成为类,如果备选的策略很多的话,那么对象的数目就会很可观。 - 只适合扁平的算法结构
策略模式的一系列算法地位是平等的,是可以相互替换的,事实上构成了一个扁平的算法结构,也就是在一个策略接口下,有多个平等的策略算法,就相当于兄弟算法。而且在运行时刻只有一个算法被使用,这就限制了算法使用的层级,使用的时候不能嵌套使用。
对于出现需要嵌套使用多个算法的情况,比如折上折、折后返卷等业务的实现,需要组合或者是嵌套使用多个算法的情况,可以考虑使用装饰模式、或是变形的职责链、或是AOP等方式来实现。
3.6 思考策略模式
1:策略模式的本质
策略模式的本质:分离算法,选择实现。
仔细思考策略模式的结构和实现的功能,会发现,如果没有上下文,策略模式就回到了最基本的接口和实现了,只要是面向接口编程的,那么就能够享受到接口的封装隔离带来的好处。也就是通过一个统一的策略接口来封装和隔离具体的策略算法,面向接口编程的话,自然不需要关心具体的策略实现,也可以通过使用不同的实现类来实例化接口,从而实现切换具体的策略。
看起来好像没有上下文什么事情,但是如果没有上下文,那么就需要客户端来直接与具体的策略交互,尤其是当需要提供一些公共功能,或者是相关状态存储的时候,会大大增加客户端使用的难度。因此,引入上下文还是很必要的,有了上下文,这些工作就由上下文来完成了,客户端只需要与上下文交互就可以了,这样会让整个设计模式更独立、更有整体性,也让客户端更简单。
但纵观整个策略模式实现的功能和设计,它的本质还是“分离算法,选择实现”,因为分离并封装了算法,才能够很容易的修改和添加算法;也能很容易的动态切换使用不同的算法,也就是动态选择一个算法来实现需要的功能了。
2:对设计原则的体现
从设计原则上来看,策略模式很好的体现了开-闭原则。策略模式通过把一系列可变的算法进行封装,并定义出合理的使用结构,使得在系统出现新算法的时候,能很容易的把新的算法加入到已有的系统中,而已有的实现不需要做任何修改。这在前面的示例中已经体现出来了,好好体会一下。
从设计原则上来看,策略模式还很好的体现了里氏替换原则。策略模式是一个扁平结构,一系列的实现算法其实是兄弟关系,都是实现同一个接口或者继承的同一个父类。这样只要使用策略的客户保持面向抽象类型编程,就能够使用不同的策略的具体实现对象来配置它,从而实现一系列算法可以相互替换。
3:何时选用策略模式
建议在如下情况中,选用策略模式:
- 出现有许多相关的类,仅仅是行为有差别的情况,可以使用策略模式来使用多个行为中的一个来配置一个类的方法,实现算法动态切换
- 出现同一个算法,有很多不同的实现的情况,可以使用策略模式来把这些“不同的实现”实现成为一个算法的类层次
- 需要封装算法中,与算法相关的数据的情况,可以使用策略模式来避免暴露这些跟算法相关的数据结构
- 出现抽象一个定义了很多行为的类,并且是通过多个if-else语句来选择这些行为的情况,可以使用策略模式来代替这些条件语句
3.7 相关模式
- 策略模式和状态模式
这两个模式从模式结构上看是一样的,但是实现的功能是不一样的。
状态模式是根据状态的变化来选择相应的行为,不同的状态对应不同的类,每个状态对应的类实现了该状态对应的功能,在实现功能的同时,还会维护状态数据的变化。这些实现状态对应的功能的类之间是不能相互替换的。
策略模式是根据需要或者是客户端的要求来选择相应的实现类,各个实现类是平等的,是可以相互替换的。
另外策略模式可以让客户端来选择需要使用的策略算法,而状态模式一般是由上下文,或者是在状态实现类里面来维护具体的状态数据,通常不由客户端来指定状态。 - 策略模式和模板方法模式
这两个模式可组合使用,如同前面示例的那样。
模板方法重在封装算法骨架,而策略模式重在分离并封装算法实现。 - 策略模式和享元模式
这两个模式可组合使用。
策略模式分离并封装出一系列的策略算法对象,这些对象的功能通常都比较单一,很多时候就是为了实现某个算法的功能而存在,因此,针对这一系列的、多个细粒度的对象,可以应用享元模式来节省资源,但前提是这些算法对象要被频繁的使用,如果偶尔用一次,就没有必要做成享元了。