设计模式解读之一: 策略模式——鸭子游戏
当我们掌握了Java的语法,当我们了解了面向对象的封装、继承、多态等特性,当我们可以用Swing、Servlet、JSP技术构建桌面以及Web应用,不意味着我们可以写出面向对象的程序,不意味着我们可以很好的实现代码复用,弹性维护,不意味着我们可以实现在维护、扩展基础上的代码复用。一把刀,可以使你制敌于无形而于江湖扬名,也可以只是一把利刃而使你切菜平静。Java,就是这把刀,它的威力取决于你使用的方式。当我们陷入无尽无止重复代码的泥沼,当我们面临牵一发而动全身的维护恶梦, 你应该想起“设计模式”这个行动秘笈。面向对象的精义,看似平淡,其实要经过艰苦实践才能成功。而构造OO系统的隐含经验于是被前人搜集而成并冠以“设计模式”之名。我们应该在编码行动初始就携带以它。接下来,让我们步“四人组”先行者之后,用中国文字、用实际案例领略模式于我们代码焕然一新的改变:
1. 模式定义
把会变化的内容取出并封装起来,以便以后可以轻易地改动或扩充部分,而不影响不需要变化的其他部分;
2. 问题缘起
当涉及至代码维护时,为了复用目的而使用继承,结局并不完美。对父类的修改,会影响到子类型。在超类中增加的方法,会导致子类型有该方法,甚至连那些不该具备该方法的子类型也无法免除。
模拟鸭子的简单应用
Joe(乔)上班的公司做了一套相当成功的模拟鸭子游戏SimUDuck,游戏中出现各种鸭子, 一边游泳戏水, 一边呱呱叫。此系统的内部设计使用了标准的OO技术,设计了一个鸭子超类(Superclass),并让各种鸭子继承此超类。
示例,一个鸭子类型:
public abstract class Duck {
//所有的鸭子均会叫以及游泳,所以父类中处理这部分代码
public void quack() {
System.out.println("Quack");
}
public void swim() {
System.out.println("All ducks float, even decoys.");
}
//因为每种鸭子的外观是不同的,所以父类中该方法是抽象的,由子类型自己完成。
public abstract void display();
}
}
public class MallardDuck extends Duck {
//野鸭外观显示为绿头
public void display() {
System.out.println("Green head.");
}
}
public class RedHeadDuck extends Duck {
//红头鸭显示为红头
public void display() {
System.out.println("Red head.");
}
}
public class RubberDuck extends Duck {
//橡皮鸭叫声为吱吱叫,所以重写父类以改写行为
public void quack() {
System.out.println("Squeak");
}
//橡皮鸭显示为黄头
public void display() {
System.out.println("Yellow head.");
}
}
还是上面的鸭子游戏,如果我们想让鸭子飞起来呢?我们来看看使用继承方法在Duck中加入飞行动作之后鸭子的代码
public abstract class Duck {
//所有的鸭子均会叫以及游泳,所以父类中处理这部分代码
public void quack() {
System.out.println("Quack");
}
public void swim() {
System.out.println("All ducks float, even decoys.");
}
//因为每种鸭子的外观是不同的,所以父类中该方法是抽象的,由子类型自己完成。
public abstract void display();
}
public void fly() {
System.out.println("flying.");//加入的飞行动作
}
}
//其他鸭子子类代码如上,略。
上述代码,初始实现得非常好。现在我们如果给Duck.java中加入fly()方法的话,那么在子类型中均有了该方法,于是我们看到了会飞的橡皮鸭子,你看过吗?当然,我们可以在子类中通过空实现重写该方法以解决该方法对于子类型的影响。但是父类中再增加其它的方法呢?
(你也许想到了另一种方法,在会飞的鸭子类里才添加该方法不就可以了?)
代码修改如下:
public class MallardDuck extend Duck{
public void display(){
System.out.println("Green head.");
// 外观是绿色的
}
}
public class RubberDuck extends Duck {
//橡皮鸭叫声为吱吱叫,所以重写父类以改写行为
public void quack() {
System.out.println("Squeak");
}
//橡皮鸭显示为黄头
public void display() {
System.out.println("Yellow head.");
public void fly{
//什么也不做
}
}
这样我们真实现了确实能飞的鸭子才可以飞起来了,看起来主意不错!问题到这儿似乎得到了解决
但我们现在有了一种新的鸭子,诱铒鸭(不会飞也不会叫),看来需要这样来写
public class DecoyDuck extend Duck{
public void quack(){
//覆盖,变成什么也不做
}
public void display(){
//诱饵鸭
System.out.println("DecoyDuck.");
}
public void fly(){
//覆盖,变成什么也不做
}
}
我们来想一下,每当有新的鸭子子类出现或者鸭子新的特性出现,你就不得不被迫在Duck类里添加并在所有子类里检查可能需要覆盖fly()和quark()...这简直是无穷尽的恶梦。
通过继承在父类中提供行为,会导致以下缺点:
a. 代码在多个子类中重复;
b. 运行时的行为不容易改变;
c. 改变会牵一发动全身,造成部分子类型不想要的改变;
所以,我们需要一个更清晰的方法,让某些(而不是全部)鸭子类型可飞或可叫。让鸭子的特性能有更好的扩展性。
我们试着用一下接口的方式怎么样?把fly()取出来,放进一个Flyable接口中,这样只有会飞的鸭子才实现这个接口,当然我们也可以照此来设计一个Quackbable接口,因为不是所有的鸭子都会叫,也只让会叫的鸭子才去实现这个接口。
public abstract class Duck {
//将变化的行为 fly() 以及quake()从Duck类中分离出去定义形成接口,有需求的子类中自行去实现
public void swim() {
System.out.println("All ducks float, even decoys.");
}
public abstract void display();
}
}
//变化的 fly() 行为定义形成的接口
public interface FlyBehavior {
void fly();
}
//变化的 quack() 行为定义形成的接口
public interface QuackBehavior {
void quack();
}
//野鸭子会飞以及叫,所以实现接口FlyBehavior, QuackBehavior
public class MallardDuck extends Duck implements FlyBehavior, QuackBehavior{
public void display() {
System.out.println("Green head.");
}
public void fly() {
System.out.println("Fly.");
}
public void quack() {
System.out.println("Quack.");
}
}
//红头鸭子会飞以及叫,所以也实现接口FlyBehavior, QuackBehavior
public class RedHeadDuck extends Duck implements FlyBehavior, QuackBehavior{
public void display() {
System.out.println("Red head.");
}
public void fly() {
System.out.println("Fly.");
}
public void quack() {
System.out.println("Quack.");
}
}
//橡皮鸭不会飞,但会吱吱叫,所以只实现接口QuackBehavior
public class RubberDuck extends Duck implements QuackBehavior{
//橡皮鸭叫声为吱吱叫
public void quack() {
System.out.println("Squeak");
}
//橡皮鸭显示为黄头
public void display() {
System.out.println("Yellow head.");
}
}
但这个方法和上面提到的在子类里去实现fly一样笨,如果几十种都可以飞,你得在几十个鸭子里去写上一样的fly(),如果一旦这个fly有所变更,你将不得不找到这几十个鸭子去一个一个改它们的fly()方法。
你可能有些气恼了,这样也不太正确,那样也不太正确,谁来告诉我怎么样弄呢?还有没有更好的办法了呢?
让我们先来总结一下:我们知道了使用继承有一些缺失, 因为改变鸭子的行为会影响所有种类的鸭子,而这并不恰当。Flyable与Quackable接口一开始似乎还挺不错, 解决了问题( 只有会飞的鸭子才继承Flyable) , 但是Java的接口不具有实现代码, 所以继承接口无法达到代码的复用。这意味着:无论何时你需要修改某个行为,你必须得往下追踪并修改每一个定义此行为的类。
让我们来想一下策略模式的第一原则:
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
按照上述设计原则,我们重新审视之前的Duck代码。
1) 分开变化的内容和不变的内容
Duck类中的行为 fly(), quack(), 每个子类型可能有自己特有的表现,这就是所谓的变化的内容。
Duck类中的行为 swim() 每个子类型的表现均相同,这就是所谓不变的内容。
我们将变化的内容从Duck()类中剥离出来单独定义形成接口以及一系列的实现类型。将变化的内容定义形成接口可实现变化内容和不变内容的剥离。其实现类型可实现变化内容的重用。这些实现类并非Duck.java的子类型,而是专门的一组实现类,称之为"行为类"。由行为类而不是Duck.java的子类型来实现接口。这样,才能保证变化的行为独立于不变的内容。于是我们有:
我们利用接口代表每个行为,比方说, FlyBehavior与QuackBehavior,而行为的每个实现都必须实现这些接口之一。所以这次鸭子类不会负责实现Flying与Quacking接口,而是由其它类专门实现FlyBehavior与QuackBehavior, 这就称为“行为”类。由行为类实现行为接口,而不是由Duck类实现行为接口。
这样的作法迥异于以往,以前的作法是:行为是继承
自Duck超类的具体实现而来,或是继承某个接口并由子类
自行实现而来。这两种作法都是依赖于“实现”,我们被实现
绑得死死的,没办法更改行为(除非写更多代码)。
变化的内容:
//变化的 fly() 行为定义形成的接口
public interface FlyBehavior {
void fly();
}
//变化的 fly() 行为的实现类之一
public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("I'm flying.");
}
}
//变化的 fly() 行为的实现类之二
public class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("I can't fly.");
}
}
//变化的 quack() 行为定义形成的接口
public interface QuackBehavior {
void quack();
}
//变化的 quack() 行为实现类之一
public class Quack implements QuackBehavior {
public void quack() {
System.out.println("Quack");
}
}
//变化的 quack() 行为实现类之二
public class Squeak implements QuackBehavior {
public void quack() {
System.out.println("Squeak.");
}
}
//变化的 quack() 行为实现类之三
public class MuteQuack implements QuackBehavior {
public void quack() {
System.out.println("<< Slience >>");
}
}
通过以上设计,fly()行为以及quack()行为已经和Duck.java没有什么关系,可以充分得到复用。而且我们很容易增加新的行为, 既不影响现有的行为,也不影响Duck.java。但是,大家可能有个疑问,就是在面向对象中行为不是体现为方法吗?为什么现在被定义形成类(例如Squeak.java)?在OO中,类代表的"东西"一般是既有状态(实例变量)又有方法。只是在本例中碰巧"东西"是个行为。既使是行为,也有属性及方法,例如飞行行为,也需要一些属性记录飞行的状态,如飞行高度、速度等。
2) 整合变化的内容和不变的内容
public abstract class Duck {
//将行为类声明为接口类型,降低对行为实现类型的依赖
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public void performFly() {
//不自行处理fly()行为,而是委拖给引用flyBehavior所指向的行为对象
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void swim() {
System.out.println("All ducks float, even decoys.");
}
public abstract void display();
}
Duck.java不关心如何进行 fly()以及quack(), 这些细节交由具体的行为类完成。
public class MallardDuck extends Duck{
public MallardDuck() {
flyBehavior=new FlyWithWings();
quackBehavior=new Quack();
}
public void display() {
System.out.println("Green head.");
}
}
测试类:
public class DuckTest {
public static void main(String[] args) {
Duck duck=new MallardDuck();
duck.performFly();
duck.performQuack();
}
}
测试结果略!
类图结构如下:
如同本例一般,当你将两个类结合起来使用,这就是组合(composition)。这种作法和『继承』不同的地方在于,鸭子的行为不是继承而来,而是和适当的行为对象『组合』而来。
这是一个很重要的技巧。其实是使用了策略模式中的第三个设计原则:
多用组合,少用继承。
当然我们也可以动态设定行为:在父类Duck.java中增加设定行为类型的setter方法,接受行为类型对象的参数传入。为降低耦合,行为参数被声明为接口类型。这样,即便在运行时,也可以通过调用这两种方法以改变行为。
public abstract class Duck {
//在刚才Duck.java中加入以下二个方法。
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior=flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior=quackBehavior;
}
//其它方法同,省略...
}
测试类:
public class DuckTest {
public static void main(String[] args) {
Duck duck=new MallardDuck();
duck.performFly();
duck.performQuack();
duck.setFlyBehavior(new FlyNoWay());
duck.performFly();
}
}