概念什么的都是正确的废话!
所以不说废话,直接上栗子:
简单的鸭子
需求:假设公司现在有个项目,要做的是一款鸭子游戏,游戏中会有各种鸭子,一边游泳戏水一边呱呱叫,而且外观不同。
通常我们的做法是直接先来个超类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()
方法后使得某些并不适合该行为的子类也具有了该行为!
大家从这里使用继承可以体会到:
当涉及维护时,为了复用而使用继承并不理想
聪明的你可能会耍下小聪明:在RubberDuck
的fly()
方法里可以什么也不做啊!
那我就要问你了:对于DecoyDuck
(诱饵鸭,不会叫也不会飞),quack()
和fly()
是不是都要override
后什么也不做? 对于后面可能要加入各种 不叫不飞、只叫不飞、只飞不叫 的鸭子,是不是都要这样麻烦地去改代码???
使用接口
从上面我们可以看到此处使用继承的缺点:
- 代码在多个子类中重复
- 运行时的行为不容易改变
- 很难知道鸭子的全部行为
- 改变会牵一发动全身,造成其他鸭子不想要的改变
既然使用继承不能让人满意,那就用接口吧!
将fly()
从Duck
中取出来,放进一个“Flyable接口”中,会飞的鸭子才实现此接口;同理可以设计“Quackable接口”,因为不是所有鸭子都会叫。
but,麻烦又来了:以前已经有很多只鸭子了,需要会飞的鸭子都必须实现Flyable
接口,修改代码变多,导致代码无法复用!
抛弃继承转而使用接口,发现从一个坑跳进另一个坑。。。
重新分析
我们知道,软件开发的不变真理就是改变:
不管当初软件设计得多好,一段时间后,总是需要成长与改变,不然软件就会“死亡。
把问题归零,可以发现:使用继承和接口,都留着一个坑——无论何时修改某个行为,都必须往下追踪并在每个定义了此行为的类中修改它,不小心就会造成新的错误!
这里引出第一个设计原则:
把可能会变化的部分取出来并“封装”起来,以便以后可以轻易地改动或扩充此部分,而不影响其他不需要变化的部分。
这个原则很简单,几乎是每个设计模式背后的精神所在,即“让系统中的某部分改变不会影响到其他部分”。
封装变化
在第一原则的指导下,分开变化和不会变化的部分:fly()
和quack()
会随着鸭子的不同而改变,将他们从 Duck
中分开,建立新的类来表示每个行为。
定义FlyBehavior
和QuackBehavior
接口,特定的具体行为编写在实现了这两个接口的子类中。 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
中加入两个实例变量——flyBehavior
和quackBehavior
,申明为接口类型而不是具体实现类型,让每个子类自己去动态地设置运行时的具体行为。
然后,将Duck
的fly()
和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)。和继承不一样,鸭子的行为不是继承来的,而是和适当的对象组合而来的。(比如:Duck
和FlyBebavior
组合)
这里引出第三个设计原则:
多用组合,少用继承
策略模式
我们可以将一组行为想成一簇算法,在Duck中,算法代表鸭子能做的事(飞法和叫法):这种关系是HAS-A(有一个),而不是IS-A(是一个)。
吃完栗子可以讲正确的废话了!
策略模式的定义:
策略模式定义了算法族,将它们分别封装起来,让它们之间可以互相替换,使算法的变化独立于使用算法的客户。
总结
- 坚持设计模式的三个原则,它们是我们做模式设计的根基。
- 软件开发后的维护时间比开发时间多很多,要致力于提高可维护性和可扩展性的复用程度。
- 使用组合使系统具有很大的弹性,不仅可以将算法封装成类,还可以在运行时动态地改变行为。