前言
在开发一个系统的过程中,我们可能会时不时的问自己,我们这么设计这块逻辑到底好不好,如果以后需要扩展这部分会不会很困难。有了这么多疑问,我们自然而然的就会想到去寻找问题的答案。幸运的是,Head First设计模式这本书为我们提供了很多问题的解答。我们要善于利用其它开发人员的经验与智慧,他们也曾遇到过相似的问题,也顺利解决过这些问题。这周,读了一下策略模式,读完之后,犹如醍醐灌顶。
鸭子的故事
策略模式是从简单的鸭子游戏开始讲起的。
众所周知,游戏中会出现各种鸭子,有的会游泳,有的会呱呱叫,有的会咕咕叫,有的会飞,有的不会飞。
我们最先想到的设计就是有一个鸭子的基类,这个基类中有quack()方法,swim()方法,display()(外观)方法。我们其他所有的鸭子都会继承这个基类。如下图:
注意1:因为每种鸭子的外观都不同,所以dispaly()方法是抽象的。
注意2:所有的鸭子都会呱呱叫(Quack),也会游泳(swim),所以由超类负责处理这部分的实现代码。
注意3:每个鸭子的子类负责实现自己的display()行为。
但是呢,公司想要为鸭子加上“会飞”这个方法。
我们首先想到的就是在鸭子的基类Duck上面再增加一个方法fly(),如下图所示:
然而,这样做出现了问题,我们忽略了一个事实就是“并不是所有的鸭子都会飞”,但是我们却在超类中增加了鸭子的“飞”的行为,这样会使得某也不适合该行为的子类也具有该行为。
当涉及“维护”时,为了使用“复用”(reuse)目的而使用继承,结局并不完美。
橡皮鸭不是呱呱叫,而是吱吱叫,所以我们在RubberDuck中讲quack()方法覆盖为吱吱叫,但是橡皮鸭不会飞,可能有人会想到,我们可以把橡皮鸭(RubberDuck)中的fly()方法覆盖掉,这样不就可以了?
可是,如果我们以后加入木头假鸭,这个又不会飞,也不会叫,这该怎么办呢?
我们有橡皮鸭,会叫但不会飞,我们还有木头假鸭,不会飞也不会叫,我们还有别的种类的鸭子,会飞也会叫。
利用接口如何
在这种情况下,有人可能会想到了使用接口,我们把fly()从超类中取出来,放进一个Flyable接口,只有会飞的鸭子才实现这个接口,同样的方式,我们也可以设计一个Quackable接口,只有会叫的鸭子才实现这个接口。
问题随之而来,虽然Flyable和Quackable可以解决一部分问题,但是却造成代码无法复用,这就像是从一个噩梦跳到另一个噩梦。甚至,在会飞的鸭子中,飞行的动作可能还有多种变化。
在这种情况下,我们接触到了第一个设计原则:
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
分离变化和不会变化的部分
为了要分开“变化和不会变化的部分”,我们准备建立两组类(完全远离Duck类),一个是fly相关的,一个是quack相关的,每一组类将实现各自的动作。比方说,我们可能有一个类实现“呱呱叫”,另一个类实现“吱吱叫”,还有一个类实现“安静”。
设计鸭子的行为
我们希望一切能有弹性,毕竟,正式因为一开始鸭子行为没有弹性,才让我们走向现在这条路。我们还想能够“指定”行为到鸭子的实例。并指定特定“类型”的飞行行为给他。干脆顺便让鸭子的行为可以动态改变好了。
第二个设计原则:
针对接口变成,而不是针对实现编程
我们利用接口代表每个行为,比方说,FlyBehavior 与QuackBehavior,而行为的每个实现都将实现其中的一个接口。
FlyBehavior是一个接口,所有的飞行类都实现它。
QuackBehavir是一个接口,代表叫的行为。
FlyWithWings是一个类,该类实现了FlyBehavior这个接口,代表所有有翅膀的鸭子飞行动作。
FlyNoWay是一个类,该类实现了FlyBehavior这个接口,代表所有不会飞的鸭子的动作。
同样,Quack Squeak MuteQuack也是三个类,分别实现了QuackBehavior这个接口,分别代表了鸭子呱呱叫、吱吱叫、安静行为。
这样的设计,可以让飞行和呱呱叫的动作被其他的对象复用,因为这些行为已经与鸭子类无关了。
而我们也可以新增一些行为,不会影响到既有的行为类,也不会影响“使用”到飞行行为的鸭子类。
整合鸭子的行为
关键在于,鸭子现在会将飞行和呱呱叫的动作“委托”(delegate)别人处理,而不是使用定义在Duck类(或子类)内的呱呱叫和飞行方法。
Duck.java
public abstract class Duck {
public void setFlyBehavior(FlyBehavior mFlyBehavior) {
this.mFlyBehavior = mFlyBehavior;
}
FlyBehavior mFlyBehavior;
public void setQuackBehavior(QuackBehavior mQuackBehavior) {
this.mQuackBehavior = mQuackBehavior;
}
QuackBehavior mQuackBehavior;
public Duck(){
}
public void performFly(){
mFlyBehavior.fly();
}
public void performQuack(){
mQuackBehavior.quack();
}
public abstract void display();
public void swim(){
System.out.println("All Ducks can swim");
}
}
FlyBehavior.java
public interface FlyBehavior {
void fly();
}
FlyWithWings.java
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System.out.println("I am Flying");
}
}
FlyNoWay.java
public class FlyNoWay implements FlyBehavior {
@Override
public void fly() {
System.out.println("I cannot fly");
}
}
QuackBehavior.java
public interface QuackBehavior {
void quack();
}
Quack.java
public class Quack implements QuackBehavior {
@Override
public void quack() {
System.out.println("Quack");
}
}
Squeak.java
public class Squeak implements QuackBehavior {
@Override
public void quack() {
System.out.println("Squeak");
}
}
MuteQuack.java
public class MuteQuack implements QuackBehavior{
@Override
public void quack() {
System.out.println("<<Silence>>");
}
}
定义好鸭子的基类和行为类之后,我们来创建我们的鸭子MallardDuck
MallardDuck.java
public class MallardDuck extends Duck {
public MallardDuck(){
}
@Override
public void display() {
System.out.println("I am MallardDuck");
}
}
以及我们的测试类MiniDuckSimulator.java
public class MiniDuckSimulator {
public static void main(String [] args){
Duck mallard = new MallardDuck();
mallard.setFlyBehavior(new FlyWithWings());
mallard.setQuackBehavior(new Quack());
mallard.performFly();
mallard.performQuack();
}
}
运行代码:
总结
每一鸭子都有一个FlyBehavior和一个QuackBehavior,好将飞行和呱呱叫委托给他们代为处理。
当你将两个类结合起来使用,这就是组合(conposition),这种做法和继承的不同的地方在于,鸭子的行为不是继承而来的,而是组合而来的。
第三个设计原则:
多用组合,少用继承
经过很长的一段路,终于引出了我们的第一个设计模式:策略模式。
策略模式定义了算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
良好的OO设计必须具备可服用、可扩充、可维护三个特性。我们常常把系统中会变化的部分抽出来封装。有了策略模式,我们的系统不会担心遇到任何改变。