4.1策略模式:
策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。
问题
一天,我们需要做一个鸭子游戏,游戏中会出现各种鸭子,一些鸭子会游泳,一些鸭子会嘎嘎叫,一些鸭子会飞,现在我们需要设计这个鸭子应用,我们首先想到的是建立一个鸭子父类,然后让其他鸭子去继承这个父类。
但是随着鸭子的种类越来越多,比如橡皮鸭,他只会叫缺不会飞,我们构造的类已经不满足他的需求了,我们也不能在游戏内让橡皮鸭在天上飞,这个时候怎么办呢?我们可以让fly()
方法被覆盖掉,可是如果以后加入了诱饵鸭怎么办呢,他是个木头假鸭,不会飞也不会叫。此时再用继承覆盖会不会以后的类会非常多?
利用接口如何?
如果几个月之后我们还要需求要更新怎么办?所以我们需要一个更清晰的方法,可以让某些(不是全部)鸭子类型可以飞或者可以叫。
那么问题来了,并非所有的子类都可以飞或者都可以叫,所以继承接口不是合适的解决方案。虽然Flyable
和Quackable
可以解决掉一部分问题,但是代码却无法复用,只能是一个噩梦跳进另一个噩梦…
把问题归零
接口可以解决一部分问题,但是却造成了代码无法复用。这意味着:无论你何时修改这个代码,你都需要往下追踪在每一个类中的定义并且去修改它,一不小心就会造成新的错误。
幸运的是,有一个设计模式,可以解决这种问题:找出应用中可能需要变化之处,把他们独立出来,不要和那些不需要变化的代码混在一起
换句话说,如果每次新的需求一来,都需要把某处的代码发生变化,那么你就可以确定,这部分的代码需要被抽取出来,和其他代码有稳定的部分。那么我们是不是可以思考一个问题:”把需要变换的抽取并封装起来,以便以后需要时可以轻易的改变这个部分,而不影响其他变化的部分。“
那么我们需要找到变化和不变化的部分,很简单,我们也知道Duck类的fly()和quack()类会随着鸭子的不同而改变,我们需要建立一组新的类来代表每个行为
那么如何设计鸭子的行为呢?我们需要运动的改变鸭子的飞行行为和呱呱叫的行为,我们在新建对象时需要指定他的各种行为。
有了这些目标,我们看第二个设计原则:针对接口编程,而不是针对实现编程
我们可以用接口代表某个行为:FlyBehavior和QuackBehavior,而行为的每个实现都将实现其中一个接口。
实现鸭子的行为:
整合鸭子的行为:
- 首先将
FlyBehavior
和QuackBehavior
抽出来进行封装,因为他们是可变的,并给他们加上对应的属性方法:
public interface QuackBehavior {
/**
鸭子呱呱叫的行为,可以不叫 也可以吱吱叫
*/
void quack();
}
public interface FlyBehavior {
/**
实现鸭子飞行 或者什么都不做 不会飞
*/
void fly();
}
- 分别实现
FlyBehavior
和QuackBehavior
对应的行为:
public class Quack implements QuackBehavior{
@Override
public void quack() {
System.out.println("Quack");
}
}
public class FlyWithWings implements FlyBehavior{
@Override
public void fly() {
System.out.println("I am fly");
}
}
- 接着在Duck类中加入两个实例变量,分别为
flyBehavior
和quackBehavior
,声明为接口类型,每个鸭子都会动态的设置这些变量在运行时引用正确的行为类型
- 现在我们来实现
performQuack()
//鸭子的种类可以有很多,所以我们将鸭子和他的外观抽象出来:
public abstract class Duck {
//鸭子的叫声行为
QuackBehavior quackBehavior;
//鸭子的飞行行为
FlyBehavior flyBehavior;
//鸭子的外观
public abstract void display();
public void performQuack(){
quackBehavior.quack();
}
public void performFly(){
flyBehavior.fly();
}
}
- 我们来关心怎么设置实例变量:绿头鸭类
public class MallardDuck extends Duck{
public MallardDuck(){
//绿头鸭类使用呱呱叫
quackBehavior = new Quack();
//绿头鸭是可以飞的
flyBehavior = new FlyWithWings();
}
@Override
public void display() {
System.out.println("I`m a real Mallard duck");
}
}
- 编写测试类
public class Main {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performQuack();
mallard.performFly();
}
}
运行结果:
Quack
I am fly
但是这样的话就写死,鸭子是可以”动态“扩展的,不然我们设定的那么多动态功能没用用到就太可惜了。我们继续给他加入动态扩展:
- 在Duck类中加入两个新的方法:
public void setFlyBehavior(FlyBehavior fb){
flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qd){
quackBehavior = qd;
}
- 我们创一个新的鸭子—模型鸭
public class ModelDuck extends Duck{
public ModelDuck(){
//模型鸭不会飞
flyBehavior = new FlyNoWay();
quackBehavior = new Quack();
}
@Override
public void display() {
System.out.println("I`m a model duck");
}
}
public class FlyNoWay implements FlyBehavior{
@Override
public void fly() {
System.out.println("i can`t fly");
}
}
- 我们给模型鸭一个火箭动力,让他飞起来
public class FlyRocketPowered implements FlyBehavior{
@Override
public void fly() {
System.out.println("I`m flying with a rocket");
}
}
- 改变测试类,看看我们的动力鸭:
public class Main {
public static void main(String[] args) {
Duck model = new ModelDuck();
model.performFly();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
}
}
运行结果:
i can`t fly
I`m flying with a rocket
所以我们就可以完全自定义。
下面是整个重新设计后的类结构,你所期望的一切都有:鸭子继承Duck,飞行行为实现FlyBehavior
接口,呱呱叫行为实现QuackBehavior
接口。也请注意,我们描述事情的方式也稍有改变。不再把鸭子的行为说成是“一组行为”,我们开始把行为想成是“一族算法”。想想看,在SimUDuck
的设计中,算法代表鸭子能做的事(不同的叫法和飞行法),这样的做法也能很容易地用于用一群类计算不同州的销售税金。
请特别注意类之间的“关系”。拿起笔,把下面图形中的每个箭头标上适当的关系,关系可以是IS-A(是一个)、HAS-A (有一个)或IMPLEMENTS (实现)。
策略模式适合应用场景
当你想使用对象中各种不同的算法变体, 并希望能在运行时切换算法时, 可使用策略模式。
策略模式让你能够将对象关联至可以不同方式执行特定子任务的不同子对象, 从而以间接方式在运行时更改对象行为。
当你有许多仅在执行某些行为时略有不同的相似类时, 可使用策略模式。
策略模式让你能将不同行为抽取到一个独立类层次结构中, 并将原始类组合成同一个, 从而减少重复代码。
如果算法在上下文的逻辑中不是特别重要, 使用该模式能将类的业务逻辑与其算法实现细节隔离开来。
策略模式让你能将各种算法的代码、 内部数据和依赖关系与其他代码隔离开来。 不同客户端可通过一个简单接口执行算法, 并能在运行时进行切换。
当类中使用了复杂条件运算符以在同一算法的不同变体中切换时, 可使用该模式。
策略模式将所有继承自同样接口的算法抽取到独立类中, 因此不再需要条件语句。 原始对象并不实现所有算法的变体, 而是将执行工作委派给其中的一个独立算法对象。
实现方式
- 从上下文类中找出修改频率较高的算法 (也可能是用于在运行时选择某个算法变体的复杂条件运算符)。
- 声明该算法所有变体的通用策略接口。
- 将算法逐一抽取到各自的类中, 它们都必须实现策略接口。
- 在上下文类中添加一个成员变量用于保存对于策略对象的引用。 然后提供设置器以修改该成员变量。 上下文仅可通过策略接口同策略对象进行交互, 如有需要还可定义一个接口来让策略访问其数据。
- 客户端必须将上下文类与相应策略进行关联, 使上下文可以预期的方式完成其主要工作。
策略模式优缺点
优点:
-
你可以在运行时切换对象内的算法。
-
你可以将算法的实现和使用算法的代码隔离开来。
-
你可以使用组合来代替继承。
-
开闭原则。 你无需对上下文进行修改就能够引入新的策略。
缺点:
- 如果你的算法极少发生改变, 那么没有任何理由引入新的类和接口。 使用该模式只会让程序过于复杂。
- 客户端必须知晓策略间的不同——它需要选择合适的策略。
- 许多现代编程语言支持函数类型功能, 允许你在一组匿名函数中实现不同版本的算法。 这样, 你使用这些函数的方式就和使用策略对象时完全相同, 无需借助额外的类和接口来保持代码简洁。
与其他模式的关系
- 桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
- 命令模式和策略看上去很像, 因为两者都能通过某些行为来参数化对象。 但是, 它们的意图有非常大的不同。
- 你可以使用命令来将任何操作转换为对象。 操作的参数将成为对象的成员变量。 你可以通过转换来延迟操作的执行、 将操作放入队列、 保存历史命令或者向远程服务发送命令等。
- 另一方面, 策略通常可用于描述完成某件事的不同方式, 让你能够在同一个上下文类中切换算法。
- 装饰模式可让你更改对象的外表, 策略则让你能够改变其本质。
- 模板方法模式基于继承机制: 它允许你通过扩展子类中的部分内容来改变部分算法。 策略基于组合机制: 你可以通过对相应行为提供不同的策略来改变对象的部分行为。 模板方法在类层次上运作, 因此它是静态的。 策略在对象层次上运作, 因此允许在运行时切换行为。
- 状态可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。
文章:First Head设计模式、[设计模式](