假如我们现在有一个鸭子,鸭子会呱呱叫,也会游泳,但是每个鸭子的外观不相同(有白颜色的,有绿色的),那么你会怎么设计这个鸭子呢?
我们第一肯定是想到设计一个鸭子超类,这个超类包括swim()和quack()两个方法,还有一个抽象的dispaly()方法。
public abstract class Duck {
public void swim(){
System.out.println("I can swimming");
}
public void quack(){
System.out.println("quack quack");
}
public abstract void display();
}
白色的鸭子继承Duck类,并实现display()方法
public class WhiteDuck extends Duck {
public void display() {
System.out.println("I am a white duck!");
}
}
绿色鸭子也继承Duck类,实现自己的display()方法
public class GreenDuck extends Duck {
public void display() {
System.out.println("I am a green duck!");
}
}
如果这个时候我们加了一个需求,要求要鸭子会飞,那么你又会怎么设计呢?你是不是首先想到在Duck类上加上一个fly()方法,像下面这样
public abstract class Duck {
public void swim(){
System.out.println("I am swimming");
}
public void quack(){
System.out.println("quack quack");
}
public abstract void display();
public void fly(){
System.out.println("I can fly");
}
}
但是如果并不是所有的鸭子都会飞,比如橡皮鸭子不会飞。这时候你会想是不是想可以在橡皮鸭中覆盖掉fly()方法,让fly()方法啥也不做,像下面这样
public class RubberDuck extends Duck{
public void display() {
System.out.println("I am a rubber duck");
}
@Override
public void fly() {
}
}
虽然上面这个方法可以暂时解决这个问题,但是如果这个时候加入了一个木头鸭子,它既不会呱呱叫,也不会飞,那这个时候你是不是就会想到在木头鸭子里覆盖掉fly()方法和quack()方法。但这样带来的问题就是如果有成千上百个鸭子,每次都要检查quack()方法和fly()方法,这简直是无穷无尽的噩梦。
那如果把fly()方法和quack()方法抽出来呢,放到一个Flyable接口和一个Quackable接口当中。让会飞的鸭子实现Flyable接口,会呱呱叫的鸭子实现Quackable()接口。
Flyable接口
public interface Flyable {
void fly();
}
WhiteDuck类
public class WhiteDuck extends Duck implements Flyable{
public void display() {
System.out.println("I am a white duck");
}
public void fly() {
System.out.println("I am fly with wing");
}
}
假如现在有个火箭鸭,它能以火箭的动力飞行,我们可以这样
public class RocketDuck extends Duck implements Flyable{
public void fly() {
System.out.println("I can fly with rocket");
}
public void display() {
System.out.println("I am a rocket duck");
}
}
这样看好像没什么问题,每个种类的鸭子都可以选择性的实现自己想实现的接口。但是你忽视了一个非常大的问题,假如鸭子一共就有三种飞行方法(用翅膀飞、不会飞、以火箭动力去飞),这个时候如果你有几十种鸭子的话,就会造成大量的代码冗余,如果有的鸭子要修改一下飞行行为,就要对这些鸭子的fly()方法逐一的修改。
那么我们再来看看这种情况策略模式怎么去做的呢?
策略模式会将代码中变化的部分抽取出来封装(比如fly,quack),以便以后可以轻易的改动或扩充此部分,而不影响不需要变化的其他部分。其实这种思想是每个设计模式背后的精神所在。所有的设计模式都提供了一套方法让“系统中的某部分改变不会影响其他部分”。
我们利用接口代表每个行为,比方说,FlyBehavior与QuackBehavior,而行为的每个实现都将实现其中的一个接口。所以这次鸭子类不会负责实现Flyable与Quackable接口,反而是由我们制造的一组其他类专门负责实现FlyBehavior与QuackBehavior,这种就称为“行为类”。所以实际的行为实现不会绑死在鸭子的子类当中。
FlyBehavior接口
public interface FlyBehavior {
void fly();
}
FlyWithWing类,用翅膀飞
public class FlyWithWing implements FlyBehavior {
public void fly() {
System.out.println("I can fly with wing");
}
}
FlyWithRocket类,以火箭的动力飞
public class FlyWithRocket implements FlyBehavior {
public void fly() {
System.out.println("I can fly with rocket");
}
}
FlyNoWay类,不会飞
public class FlyNoWay implements FlyBehavior {
public void fly() {
System.out.println("I can not fly");
}
}
QuackBehavior接口同理,这里就不贴代码了。
我们看到这样的设计可以使相同的行为代码能被复用,即使我们再新增一些行为也不会影响既有的行为类,也不会影响到使用飞行行为的鸭子类。
那么我们怎样将行为类和和鸭子类进行整合呢?
首先,我们再Duck类中加入两个变量,分别为"flyBehavior"与"quackBehavior",申明为接口类型而不是具体的实现,每个鸭子都会动态的设置这些变量以在运行时引用正确的行为类型。我们再以两个相似的方法performFly()和performQuack()取代Duck类中的fly()方法和quack()方法。
改过之后的Duck类
public abstract class Duck {
FlyBehavior flyBehavior;
public void performFly(){
flyBehavior.fly();
}
public abstract void display();
}
我们看到要在Duck类中,我们要实现飞行的行为,只需要flyBehavior去飞就行了,在Duck类中,我们不在乎flyBehavior接口的对象到底是什么,我们只要关心该对象如何进行飞行就行。
接下来我们通过RocketDuck看一下我们怎样去设置flyBehavior变量
public class RocketDuck extends Duck {
public RocketDuck() {
flyBehavior = new FlyWithRocket();
}
public void display() {
System.out.println("I am a rocket duck");
}
}
我们看到,RocketDuck类中的默认构造方法会设置flyBehavior变量为一个FlyWithRocket对象,当调用performFly()方法时就会去调用FlyWithRocket对象的fly()方法。我们写一个Main方法去执行看一下:
public class Main {
public static void main(String[] args) {
Duck duck = new RocketDuck();
duck.performFly();
}
}
输出:
I can fly with rocket
我们还可以在Duck类中通过setter方法类设定鸭子的行为类,以达到随时改变鸭子行为的效果。
public abstract class Duck {
FlyBehavior flyBehavior;
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void performFly(){
flyBehavior.fly();
}
public abstract void display();
}
假设现在有一个模型鸭ModelDuck,我们就可以这样
public class ModelDuck extends Duck {
public ModelDuck() {
flyBehavior = new FlyWithWing();
}
public void display() {
System.out.println("I am a model duck");
}
}
再运行看一下:
public class Main {
public static void main(String[] args) {
Duck duck = new ModelDuck();
duck.performFly();
duck.setFlyBehavior(new FlyNoWay());
duck.performFly();
}
}
输出:
I can fly with wing
I can not fly
我们能看到通过setter方法可以动态的区改变鸭子的行为。
那么假设现在我们的鸭子会讲话,但是每种鸭子会讲的语言不一样,有的会讲英文,有的会讲中文,还有的会讲汉语,这个时候我们应该怎么做呢?
首先我们可以写一个SpeakBehavior接口,它有一个speak()方法。
public interface SpeakBehavior {
void speak();
}
原后会讲汉语的鸭子和会讲英语的鸭子分别实现SpeakBehavior接口
public class SpeakChinese implements SpeakBehavior {
public void speak() {
System.out.println("I can speak chinese");
}
}
public class SpeakEnglish implements SpeakBehavior {
public void speak() {
System.out.println("I can speak english");
}
}
原后在Duck类中加入speakBehavior变量
public abstract class Duck {
FlyBehavior flyBehavior;
SpeakBehavior speakBehavior;
public void setSpeakBehavior(SpeakBehavior speakBehavior) {
this.speakBehavior = speakBehavior;
}
public void performBehavior(){
speakBehavior.speak();
}
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void performFly(){
flyBehavior.fly();
}
public abstract void display();
}
在RocketDuck类中添加说话的行为
public class RocketDuck extends Duck {
public RocketDuck() {
flyBehavior = new FlyWithRocket();
speakBehavior = new SpeakChinese();
}
public void display() {
System.out.println("I am a rocket duck");
}
}
我们运行看一下
public class Main {
public static void main(String[] args) {
Duck duck = new RocketDuck();
duck.performFly();
duck.performSpeak();
duck.setFlyBehavior(new FlyNoWay());
duck.performFly();
duck.setSpeakBehavior(new SpeakEnglish());
duck.performSpeak();
}
}
输出:
I can fly with rocket
I can speak chinese
I can not fly
I can speak english
我们看到新加的行为不会影响到老的行为的运行。
总结
每一个鸭子都有一个FlyBehavior和一个SpeakBehavior,好将飞行和讲话委托给他们处理。当你将两个类结合起来使用,如同本例一般,这就是组合(composition)。这种做法和“继承”不同的地方在于,鸭子的行为不是继承来的,而是和适当的行为对象“组合”来的。这是一个很重要的技巧。其实是使用了我们的第三个设计原则:
多用组合,少用继承