1. 基本概念
策略模式定义了算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户
看不懂没关系,看个例子就明白了,下面是head first设计模式
上的例子
2. 一个例子
假设我现在要定义一个鸭子类,有的鸭子会叫会飞(对应着fly()
和quack()
方法),有的鸭子不会飞(橡皮鸭),有的鸭子不会叫,你会怎么设计?
2.1 能不能使用继承?
首先给想到的肯定是抽象出来一个鸭子的父类,在里面实现鸭子的各种方法,具体实现鸭子的时候去继承他,然后重写需要改变的方法
但是这肯定是不行的,你会发现:
- 当我们每次都要去实现一个具体的鸭子子类的时候,都可能会要去重写需要改变的方法,这样不仅麻烦,而且容易出错(万一忘记重写橡皮鸭的
fly()
方法可能出现橡皮鸭会飞的情况) - 溢出效应:当对超类的局部改动会影响到其他部分
2.2 能不能使用接口
那能不能使用接口?把fly()
和quack()
分别放入Flyable
接口和Quackable
接口,需要使用这些方法的鸭子子类再去实现相应的接口
这样看似合理,但是实际也不行;因为这样一来重复的代码就变多了,多个子类鸭子可能都去实现Flyable
接口,实现fly()
方法,但是他们的实现是一样的,完全无法进行代码的重用
2.3 策略模式
怎么使用策略模式解决这个问题呢?将继承换成组合或者聚合来解决
看文章开头那一段拗口的定义,其实策略模式说的就是:
- 把上面鸭子的会变化的行为部分放在分开的类中,为此类专门提供某行为接口的实现;
- 再把这样的类(具体策略)作为鸭子类的一个成员变量,组合或聚合到鸭子里,这样鸭子类只需要在使用需要的方法的时候去调用相应的具体策略的相应的方法,而不用知道行为的具体实现
- 对于具体使用到的策略可以在构造方法中进行设定或者使用
set
方法设定
以上体现了几个设计原则:
- 把变化的代码从不变的代码中分离出来
- 针对接口编程(策略接口)
- 多用组合/聚合,少用继承(客户通过组合方式使用策略)
代码实现
① Dark
抽象类
public abstract class Duck {
//为行为接口类型的两个引用变量 多态
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck(){
}
public void performFly(){
flyBehavior.fly();
}
public void performQuack(){
quackBehavior.quack();
}
}
② 行为接口FlyBehavior
public interface FlyBehavior {
public void fly();
}
③ FlyBehavior
的实现类(应该有多个,这里只写了一个)
public class FlyWithWings implements FlyBehavior{
@Override
public void fly() {
//fly with wings
}
}
④ 行为接口QuackBehavior
public interface QuackBehavior {
public void quack();
}
⑤ QuackBehavior
的实现类(应该有多个,这里只写了一个)
public class Quack implements QuackBehavior {
@Override
public void quack() {
//quack
}
}
⑥ MallardDuck
具体的鸭子类
public class MallardDuck extends Duck{
public MallardDuck(){
this.flyBehavior = new FlyWithWings();
this.quackBehavior = new Quack();
}
public void setFlyBehavior(FlyBehavior flyBehavior){
this.flyBehavior = flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior){
this.quackBehavior = quackBehavior;
}
}
3. 再看定义
通过上面的例子我们大概就能理解所谓的策略模式了,我们来看看策略模式的结构:
策略模式的主要角色如下
-
抽象策略(
Strategy
)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现,也就是上面的FlyBehavior
接口和QuackBehavior
接口 -
具体策略(
Concrete Strategy
)类:实现了抽象策略定义的接口,提供具体的算法实现,也就是上面的FlyWithWings
-
环境(
Context
)类:持有一个策略类的引用,最终给客户端调用,也就是上面的Duck
除了上面的例子,如果有下面的代码,也可以用策略信息改进
methodA(condition){
if(condition == conditionA){
strategyA();
}else if(condition == conditionB){
strategyB();
}else if(condition == conditionC){
strategyC();
}else{
strategyD();
}
}
那么说到底什么时候去使用策略模式呢
① 针对同一类型问题,有多种处理方式,每一种都能独立解决问题;
② 算法需要自由切换的场景;
③ 需要屏蔽算法规则的场景
优点
- 算法多样性,且具备自由切换功能;
- 有效避免多重条件判断,增强了封装性,简化了操作,降低出错概率;
- 扩展性良好,策略类遵顼里氏替换原则,可以很方便地进行策略扩展
缺点
- 策略类数量增多,且所有策略类都必须对外暴露,以便客户端能进行选择
4. Arrays
中的策略模式
JDK
的Arrays.sort()
的排序比较器Comparator
就使用了策略模式
Integer[] a = {2,3,5,4,1};
Arrays.sort(a, new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
Comparator
就是抽象策略,具体策略采用匿名内部类实现
5. 使用Lambda表达式避免啰嗦
如上所说策略模式的缺点是策略实现类的数量很多且并不会在别的地方复用,为了解决这个问题可以使用lambda表达式来简化代码
public interface ValidationStrategy {
boolean execute(String s);
}
public class Validator{
private final ValidationStrategy strategy;
public Validator(ValidationStrategy v){
this.strategy = v;
}
public boolean validate(String s){
return strategy.execute(s);
}
}
ValidationStrategy是一个函数接口了(除此之外,它 还与Predicate具有同样的函数描述)。这意味着我们不需要声明新的类来实现不同 的策略,通过直接传递Lambda表达式就能达到同样的目的,并且还更简洁
Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+"));
boolean b1 = numericValidator.validate("aaaa");
Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+"));
boolean b2 = lowerCaseValidator.validate("bbbb");
Lambda表达式避免了采用策略设计模式时僵化的模板代码