策略模式(HeadFirst)
此文章来源于 Head First设计模式一书的策略模式。
故事背景
Job上班的公司做了一个模拟鸭子的游戏:SimUDuck。游戏中会出现各种鸭子,一边游戏戏水,一边呱呱叫。系统设计了一个鸭子超类,并让各种鸭子继承这个超类。一开始的系统设计如下图:
image.png
需求变更
现在增加了一个让鸭子会飞的需求。Job决定在Duck类中加入一个fly()的方法,然后所有的鸭子都会继承fly()
image.png
但是可怕的问题发生了
Job忽略了一件事情:并非所有的鸭子都会飞,在超类上加上新的行为,会使得某些不适合改行为的子类也具有该行为。
SimUDuck程序中出现了一个无生命的会飞的东西( “橡皮鸭子” 在屏幕上飞来飞去)。
对代码局部修改,影响的层面可不只是局部。
Joe想到了继承
为了解决问题,Joe开始了思考:我可以把橡皮鸭类中的fly()方法覆盖掉。
image.png
可是以后加入诱饵鸭(DecoyDuck)又会如何?诱饵鸭是木头假鸭不会飞也不会叫。
image.png
利用接口改进如何?
Joe认识到继承可能不是答案,Joe知道规格会常常变化,每当有新的鸭子子类出现,他就要被迫检查并可能需要覆盖fly()和quark()......这简直就是无穷无尽的噩梦。
Job想到:我可以把fly()从超类中取出来,放进一个“Flyable接口”中。这么一来只有会飞的鸭子才实现接口。同样的,设计一个“Quackable接口”,因为不是所有的鸭子都会叫。
image.png
但是这样又会造成代码无法复用的问题。每一个会飞的子类都要实现“Flyable接口”。甚至,在会飞的鸭子中,飞行的动作可能还有多种变化。如果Duck类有很多子类实现了飞行的行为,要稍微修改一下飞行的行为,那将变得很麻烦。
分开变化和不会变化的部分
设计原则: 找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要变化的代码混在一起。
我们知道Duck类内的fly()和quack()会随着鸭子的不同而改变。
为了要把这两个行为从Duck类中分开,我们将把它们从Duck类中取出来,建立一组新类来代表每个行为。
image.png
在此,我们有两个接口,FlyBehavior和QuackBehavior,还有它们对应的实现类,负责实现具体的行为:
image.png
整合鸭子的行为
整合鸭子的行为关键在于,鸭子现在会将飞行和呱呱叫的动作“委托”给别人处理,而不是使用定义在Duck类(或子类)内的呱呱叫和飞行方法。
具体做法如下:
- 在Duck类加入两个接口类型的成员变量,“flyBehavior”和“quackBehavior”
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
}
- 实现performQuack()方法,实现委托给别人处理
public void performQuack(){
quackBehavior.quack();
}
- 提供set方法可以在运行时候方便修改行为
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}
最终的设计
image.png代码实现
飞行行为:
public interface FlyBehavior {
void fly();
}
public class FlyNoWay implements FlyBehavior{
@Override
public void fly() {
System.out.println("没有飞行能力");
}
}
public class FlyWithWings implements FlyBehavior{
@Override
public void fly() {
System.out.println("我也会飞了");
}
}
public class FlyRocketPowered implements FlyBehavior{
@Override
public void fly() {
System.out.println("利用火箭动力飞行");
}
}
叫的行为:
public interface QuackBehavior {
void quack();
}
public class MuteQuack implements QuackBehavior{
@Override
public void quack() {
System.out.println("不会叫");
}
}
public class Quack implements QuackBehavior{
@Override
public void quack() {
System.out.println("我能大声叫:呱呱呱");
}
}
public class Squeak implements QuackBehavior{
@Override
public void quack() {
System.out.println("我能大声叫:吱吱吱");
}
}
鸭子超类:
public abstract class Duck {
//接口类型
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public abstract void display();
public void performFly(){
flyBehavior.fly();
}
public void performQuack(){
quackBehavior.quack();
}
public void swim(){
System.out.println("游啊游");
}
//提供set方法,可以在运行时修改飞行行为
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
//提供set方法,可以在运行时修改叫的行为
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}
}
具体的模型鸭:
public class ModelDuck extends Duck{
//在构造方法初始化默认的行为,也可以通过构造参数传递。
public ModelDuck(){
//一开始模型鸭不会飞
flyBehavior = new FlyNoWay();
//可以呱呱叫
quackBehavior = new Quack();
}
@Override
public void display() {
System.out.println("模型鸭");
}
}
测试类:
public class Client {
public static void main(String[] args) {
//模型鸭
Duck modelDuck = new ModelDuck();
//模型鸭一开始不会飞
modelDuck.performFly();//输出:没有飞行能力
modelDuck.performQuack();//输出:我能大声叫:呱呱呱
//修改飞的行为
modelDuck.setFlyBehavior(new FlyRocketPowered());
//具有飞行能力
modelDuck.performFly();//输出:利用火箭动力飞行
}
}
控制台输出:
没有飞行能力
我能大声叫:呱呱呱
利用火箭动力飞行
策略模式的定义
策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
结构
策略模式的主要角色如下:
- 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。(上面的QuackBehavior和FlyBehavior)
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。(QuackBehavior和FlyBehaviord的实现类)
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。(Duck类)
优缺点
优点:
- 策略类之间可以自由切换,由于策略类都实现同一个接口,所以使它们之间可以自由切换。
- 易于扩展增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
- 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。
缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。