策略模式
这是 设计模式列表 的第一种设计模式:策略模式。
什么是策略模式?
什么是策略?策略可以认为是一种方法,一种做事情的方式。由于在不同的场合,我们会用不同的方式来处理不同的事情,自然而然地,我们就选择了不同的策略。例如吃饭我们会用筷子,而在喝汤时,我们会选择用勺子,这就算是策略。而所谓的策略模式,就是将这种可变的策略抽离出来,而后采用委托(可以认为是接口或抽象类)来决定采用哪一种策略。
细说策略模式
这里直接用《Head First 设计模式》中的例子进行说明。
先上例子
假设我们要设计一个游戏,游戏里面是各种鸭子。这些鸭子呢,会飞,还会叫。自然地,我们会设计一个鸭子的抽象类,让每一种具体的鸭子继承这个鸭子抽象类。
//这是鸭子的抽象类
public abstract class Duck{
// 这是鸭子叫的方法
public abstract void quack();
// 这是鸭子飞的方法
public abstract void fly();
}
//具体的鸭子
// 黄色的鸭子
public class YellowDuck extends Duck{
//抽象父类中的叫方法
public void quack(){
System.out.println("呱呱呱");
}
//抽象父类中的飞方法
public void fly(){
System.out.println("我飞,飞,飞");
}
}
// 绿色的鸭子
public class GreenDuck extends Duck{
//抽象父类中的叫方法
public void quack(){
System.out.println("呱呱呱");
}
//抽象父类中的飞方法
public void fly(){
System.out.println("我飞,飞,飞");
}
}
如果我们想在游戏中增加一个玩具鸭子,它的叫法还是“呱呱呱”,但是它不会飞。我们该怎么做。不是容易嘛,我们在写一个ToyDuck就可以了。
//玩具鸭
public class ToyDuck extends Duck{
//抽象父类中的叫方法
public void quack(){
System.out.println("呱呱呱");
}
//抽象父类中的飞方法
public void fly(){
System.out.println("我不会飞!");
}
}
上面例子中存在的不足
虽然,上面的代码能够解决我们游戏设计鸭子的问题。但是上面的代码存在许多不足
-
代码冗余。不管是YelloDuck,GreenDuck还是ToyDuck,它们叫的方式都是“呱呱呱”。显然这样重复的代码写多次时没有必要的。如果有一百种鸭子,它们都是呱呱叫,我们就要写一百次这样的代码,这就变得繁琐了。这不简单?我们直接让父类实现“呱呱呱”叫的方法不就行了吗?这种方式是可以解决问题,但不合适。为什么呢?因为并不是所有的鸭子都是呱呱呱叫的。上面的例子刚好都是呱呱呱叫的情况,但是当我们要设计“吖吖吖”叫的鸭子时,这就显得不太合适了。
-
行为被固定死了。上面的三种鸭子都是“呱呱呱”叫,因此在游戏进行中,它们就只会呱呱呱叫了。对于YellowDuck和GreenDuck,它们都会飞。对于ToyDuck,它就不会飞。这些行为都被代码固定死了。假设我们想在游戏中增加一些新的花样,让鸭子能够根据某些情况改变叫的方式,例如在鸭子不同心情时有不同的叫法。如果还是上面的代码,就只能对代码懂手脚了。这和面向对象设计中的“对扩展开发,对修改关闭”这一“开闭原则”是相违背的。
解决方式:将可变化的行为抽离封装
对于鸭子类,叫的方式,飞的方法都可以说是一种策略,因为对于不同的鸭子,可能有不同的叫法和飞法。所以我们可以把鸭子叫和鸭子飞这两种行为抽离出来,封装成接口。
// 叫的接口
public interface QuackBehavior{
void quack();
}
// 飞的接口
public interface FlyBehavior{
void fly();
}
例如上面的鸭子呱呱呱叫,我们就可以写一个Quack类,实现QuackBehavior接口,来表示鸭子呱呱呱叫的方式。
//代表鸭子呱呱呱叫行为的类
public class Quack implements QuackBehavior{
public void quack(){
System.out.println("呱呱呱");
}
}
而鸭子飞的行为,可以设计一个CanFly类和一个CannotFly类,各自实现FlyBehavior接口,分别表示鸭子会飞和鸭子不会飞的行为。
//代表鸭子会飞的行为
public class CanFly implements FlyBehavior{
public void fly(){
System.out.println("我飞,飞,飞");
}
}
//代表鸭子不会飞的行为
public class CannotFly implements FlyBehavior{
public void fly(){
System.out.println("我不会飞!");
}
}
我们已经有了鸭子的行为类了。这时候,我们在设计鸭子的抽象类时,我们可以用组合的方式让鸭子具有会叫和会飞的行为。(所谓组合的方式,就是让鸭子的抽象类具有QuackBehavior和FlyBehavior的引用,使用这种引用的好处是,可以指向子类的实例,从而实现扩展)在面向对象程序设计时,一般我们更倾向于使用组合的方式,而少用继承的方式,因为继承本身就是一种依赖,具有一定的限制。
多用组合 少用继承
下面是我们的鸭子抽象类。
public abstract class Duck{
//可以认为是叫和飞的委托
protected QuackBehavior quackBehavior;
protected FlyBehavior flyBehavior;
public void quack(){
//直接交由委托来处理
quackBehavior.quack();
}
public void fly(){
//直接交由委托来处理
flyBehavior.fly();
}
//这里增加了动态改变叫行为的方法
public void setFly(QuackBehavior quackBehavior){
this.quackBehavior = quackBehavior;
}
//这里增加了动态改变飞行为的方法
public void setFly(FlyBehavior flyBehavior){
this.flyBehavior = flyBehavior;
}
}
对于具体的鸭子,我们只需要在类的初始化时把quackBehavior和flyBehavior指向特定的行为类就好了。
public class YellowDuck extends Duck{
//构造函数
public YellowDuck(){
//YellowDuck是呱呱呱叫的
this->quackBehavior = new Quack();
//YellowDuck会飞
this->quackBehavior = new CanFly();
}
}
public class GreenDuck extends Duck(){
//构造函数
public GreenDuck(){
//GreenDuck是呱呱呱叫的
this->quackBehavior = new Quack();
//GreenDuck会飞
this->quackBehavior = new CanFly();
}
}
public class ToyDuck extends Duck(){
//构造函数
public ToyDuck(){
//ToyDuck是呱呱呱叫的
this->quackBehavior = new Quack();
//ToyDuck不会飞
this->quackBehavior = new CanNotFly();
}
}
上面的所用的方法就是所谓的策略模式。将鸭子可变的行为(叫和飞)抽离出来,分别用QuackBehavior和FlyBehavir接口引用来指向特定的行为实例。在特定的鸭子类中我们可以将QuackBehavior和FlyBehavior引用指向特定的行为类,从而实现了行为的复用。而且在需要扩展行为时,(例如增加会吖吖吖叫的鸭子),我们只需要实现一个吖吖吖叫的行为类,例如YaYaYa类,让该类实现QuackBehavior接口,就实现了行为的扩展。使用时只需要修改相应鸭子类的构造函数,让QuackBehavior引用指向YaYaYa类就可以了。而且,通过策略模式,我们可以通过setQuack和setFly这样的方法实现动态地修改类的行为。
策略模式的UML简图
UML图和上面的例子有些许区别,是我之前画的,懒得改了。
上面的图是通过processon这个网站在线画的。如果你也想画这样的图,你可以通过下方的邀请链接进入网站进行注册,这样我可以拿到3张免费文件数量 😃 。
策略模式总结
- 将可变化的行为进行抽离封装
- 面向接口编程
- 多用组合,少用继承