《Head First 设计模式》模式1——策略模式

时间:2022-04-20 21:56:47

概念什么的都是正确的废话!

所以不说废话,直接上栗子:


简单的鸭子

需求:假设公司现在有个项目,要做的是一款鸭子游戏,游戏中会有各种鸭子,一边游泳戏水一边呱呱叫,而且外观不同。
通常我们的做法是直接先来个超类Duck

public abstract class Duck { 
public Duck() {
}

public void swim() {
System.out.println("All ducks float, even decoys!");
}

public void quack() {
System.out.println("quack...");
}

abstract void display();
}

再来Duck的两个子类MallardDuck(绿头鸭)和ReadheadDuck(红头鸭):
MallardDuck:

public class MallardDuck extends Duck { 
public MallardDuck() {
}

public void display() {
// 外观是绿头
}
}

ReadheadDuck:

public class RedHeadDuck extends Duck { 
public RedHeadDuck() {
}

public void display() {
// 外观是红头
}
}

让鸭子飞

设计好了“简单的鸭子”后,万恶的产品汪加了新需求——让鸭子飞!
这下必须改代码了,可怎么改好呢?

使用继承

既然要让鸭子飞,就在 Duck里加上方法fly(),这样所有鸭子都会飞啦!

public abstract class Duck { 
public Duck() {
}

public void swim() {
}

public void quack() {
}

abstract void display();

public void fly() {
// 飞行动作
}
}

but,麻烦来了:原本不会飞的RubberDuck(橡皮鸭子)也飞起来了!
原来,并非Duck的所有子类都会飞,加上fly()方法后使得某些并不适合该行为的子类也具有了该行为!

大家从这里使用继承可以体会到:

当涉及维护时,为了复用而使用继承并不理想

聪明的你可能会耍下小聪明:在RubberDuckfly()方法里可以什么也不做啊!
那我就要问你了:对于DecoyDuck(诱饵鸭,不会叫也不会飞),quack()fly()是不是都要override后什么也不做? 对于后面可能要加入各种 不叫不飞、只叫不飞、只飞不叫 的鸭子,是不是都要这样麻烦地去改代码???

使用接口

从上面我们可以看到此处使用继承的缺点

  1. 代码在多个子类中重复
  2. 运行时的行为不容易改变
  3. 很难知道鸭子的全部行为
  4. 改变会牵一发动全身,造成其他鸭子不想要的改变

既然使用继承不能让人满意,那就用接口吧!

fly()Duck中取出来,放进一个“Flyable接口”中,会飞的鸭子才实现此接口;同理可以设计“Quackable接口”,因为不是所有鸭子都会叫。

but,麻烦又来了:以前已经有很多只鸭子了,需要会飞的鸭子都必须实现Flyable接口,修改代码变多,导致代码无法复用!

抛弃继承转而使用接口,发现从一个坑跳进另一个坑。。。


重新分析

我们知道,软件开发的不变真理就是改变

不管当初软件设计得多好,一段时间后,总是需要成长与改变,不然软件就会“死亡。

把问题归零,可以发现:使用继承和接口,都留着一个坑——无论何时修改某个行为,都必须往下追踪并在每个定义了此行为的类中修改它,不小心就会造成新的错误!

这里引出第一个设计原则

把可能会变化的部分取出来并“封装”起来,以便以后可以轻易地改动或扩充此部分,而不影响其他不需要变化的部分。

这个原则很简单,几乎是每个设计模式背后的精神所在,即“让系统中的某部分改变不会影响到其他部分”。

封装变化

在第一原则的指导下,分开变化和不会变化的部分:fly()quack()会随着鸭子的不同而改变,将他们从 Duck中分开,建立新的类来表示每个行为。

定义FlyBehaviorQuackBehavior接口,特定的具体行为编写在实现了这两个接口的子类中。
FlyBehavior

public interface FlyBehavior {
public void fly();
}

QuackBehavior

public interface QuackBehavior {
public void quack();
}

FlyBehavior的实现类FlyWithWings

public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("I'm flying!!");
}
}

FlyBehavior的实现类FlyNoWay

public class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("I can't fly");
}
}

QuackBehavior的实现类Quack

public class Quack implements QuackBehavior {
public void quack() {
System.out.println("Quack");
}
}

QuackBehavior的实现类Squeak

public class Squeak implements QuackBehavior {
public void quack() {
System.out.println("Squeak");
}
}

QuackBehavior的实现类MuteQuack

public class MuteQuack implements QuackBehavior {
public void quack() {
System.out.println("<< Silence >>");
}
}

动态设定鸭子的行为

设计的时候,我们希望能更有弹性,可以指定行为到鸭子的实例,动态地改变鸭子的行为:在鸭子类中包含设定行为的方法,在运行时动态地改变鸭子行为。

这里引出第二个设计原则

针对接口编程,而不是针对实现编程

针对接口编程”不是说针对Java的interface编程,更明确的说是“针对超类型编程”,即:变量的申明类型应该是超类型,通常是一个抽象类或者是一接口,这样一来申明类时就不用理会执行时的真正对象类型。

由行为类而不是Duck类实现行为接口,Duck不需要知道行为的实现细节。如果是用前面说的子类继承Duck或子类实现某个接口,都是依赖于实现,实现细节被约束得死死的,没办法更改行为。

整合鸭子的行为

现在,需要将飞行和呱呱叫的动作委托给Duck处理,而不是在Duck类(或子类)中定义fly()quack()方法。做法是在Duck 中加入两个实例变量——flyBehaviorquackBehavior,申明为接口类型而不是具体实现类型,让每个子类自己去动态地设置运行时的具体行为。

然后,将Duckfly()quack()删除,使用performQuack()performFly()代替。为了让子类动态设置运行时的具体行为,在Duck 加入动态设定行为的方法:setFlyBehavior()setQuackBehavior()

此时Duck类变成了:

public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;

public Duck() {
}

public void setFlyBehavior (FlyBehavior fb) {
flyBehavior = fb;
}

public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}

abstract void display();

public void performFly() {
flyBehavior.fly();
}

public void performQuack() {
quackBehavior.quack();
}

public void swim() {
System.out.println("All ducks float, even decoys!");
}
}

使用MallardDuck作为Duck的子类:

public class MallardDuck extends Duck { 
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}

public void display() {
System.out.println("I'm a real Mallard duck");
}
}

测试代码为:

public class MiniDuckSimulator { 
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performQuack();
mallard.performFly();

mallard.setQuackBehavior(new Squeak());
mallard.performQuack();
}
}

控制台的打印结果是:

Quack 
I'm flying!!
Squeak

整体格局

在上面的鸭子示例中,将两个类结合在一起使用,就是组合(Composition)。和继承不一样,鸭子的行为不是继承来的,而是和适当的对象组合而来的。(比如:DuckFlyBebavior组合)

这里引出第三个设计原则

多用组合,少用继承


策略模式

我们可以将一组行为想成一簇算法,在Duck中,算法代表鸭子能做的事(飞法和叫法):这种关系是HAS-A(有一个),而不是IS-A(是一个)。

吃完栗子可以讲正确的废话了!

策略模式的定义:

策略模式定义了算法族,将它们分别封装起来,让它们之间可以互相替换,使算法的变化独立于使用算法的客户。


总结

  1. 坚持设计模式的三个原则,它们是我们做模式设计的根基。
  2. 软件开发后的维护时间比开发时间多很多,要致力于提高可维护性和可扩展性的复用程度。
  3. 使用组合使系统具有很大的弹性,不仅可以将算法封装成类,还可以在运行时动态地改变行为。