本系列的所有代码参见 Head First Design Patterns—Github
以 SimUDuck app 这个案例开始
假设有为一家以制作鸭子池塘模拟游戏而闻名的公司,这家公司的得意之作是 SimUDuck。这个游戏可以模拟多种不同的鸭子的游泳姿态及其叫声。系统最初采用标准的面向对象设计原则,创建一个鸭子超类让所有子类继承。
现在,公司 CEO 决定增加一个特征:让鸭子飞起来。作为一个面向对象程序员,可能你只需要在超类中增加一个 fly() 方法就解决问题了。如果这么想,就太(too)年轻(young)太(too)天真(naive)了。真这么做的话,公司产品中的所有鸭子都会飞了,包括刚刚推出的橡皮鸭。换一种思路,在子类中重写 fly() 方法,可以解决橡皮鸭的问题。但是如果公司的产品中有木头鸭,它不会飞也不会叫。甚至可能还有很多例外情况,手动一个一个子类修改太痛苦了。
为了重复利用代码采用继承机制,在面对可维护性时完全是灾难。
如果使用接口呢?因为并不是所有子类都有飞行(flying)或鸣叫(quacking)的行为,所以继承机制并不好。如果创建两个接口 Flyable 和 Quackable 让子类分别实现,可以部分解决上面的问题,但是完全破坏了代码的重用性。
如何解决问题
对于上面这种继承会破坏可维护性、接口会破坏重用性的情况,有一条设计原则:
找出代码中会变化的部分,并把它们同不变的部分分开。
Identify the aspects of your application that vary and seperate them from what stays the same.
换个方式思考这一原则:把代码中将来会变化的部分封装起来,所以之后修改代码时就不会影响不变的部分。
尽管这一概念很简单,但是它是所有设计模式的基础。所有的设计模式都提供了一种方式使得变化的部分独立于不变的部分。
对于一开始的问题,我们可以创建两个 class 的集合(完全独立于 Duck 超类),一个模拟飞行,另一个模拟鸣叫。每个 class 的集合包含了所有行为的实现。例如,一个类实现了 quacking,另一个实现了 squeaking,并且另一个实现了 silence。
现在的问题是如何设计这两个实现 fly 和 quack 行为的 class 集合。这时抛出第二个设计原则:
对接口编程,而不是实现。
Program to an interface,not an implementation.
PS: In design patterns, the phrase implement an interface does NOT always mean write a class that implements a Java interface, by using the implements keyword in the class declaration. In the genral use of the phrase, a concrete class implementing a method from a supertype(which could be a class OR interface) is still consideraed to be implementing the interface of that supertype.
所以现在并不是 Duck 这个超类实现 flying 和 quacking 接口。
第二个原则中的 interface 并不是说真的要使用一个 Java interface。关键在于利用多态从而实现对超类编程(program to supertype),使得真正运行时的对象不被锁(lock)在代码中。
这里的 program to supertype 原文中指 the declared type of the variables should be a supertype, usually an abstract class or interface, so that the objects assigned to those variables can be of any concrete implementation of the supertype, which means the class declaring them doesn’t have to know about the actual object types!
举个例子,假设有个抽象类 Animal,有两个具体实现的类 Dog 和 Cat。
Programimg to an implementation:
Dog d = new Dog();
d.bark();Programming to an interface/supertype:
Animal animal = new Dog();
animal.makeSound();
测试代码
// Duck类及其子类
public abstract class Duck
{
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck() {}
public abstract void display();
public void performFly() { flyBehavior.fly();}
public void performQuack() { quackBehavior.quack();}
public void swim() {System.out.println(“All ducks float, even decoys!”);}
}
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”);
}
}
// FlyBehavior 接口及相应的类
public interface FlyBehavior
{
public void fly();
}
public class FlyWithWings implements FlyBehavior
{
public void fly()
{
System.out.println(“I’m flying!!”);
}
}
public class FlyNoWay implements FlyBehavior
{
public void fly()
{
System.out.println(“I can’t fly”);
}
}
// QuackBehavior 接口及相应的类
public interface QuackBehavior
{
public void quack();
}
public class Quack implements QuackBehavior
{
public void quack()
{
System.out.println(“Quack”);
}
}
public class MuteQuack implements QuackBehavior
{
public void quack()
{
System.out.println(“<< Silence >>”);
}
}
public class Squeak implements QuackBehavior
{
public void quack()
{
System.out.println(“Squeak”);
}
}
// Test class
public class MiniDuckSimulator
{
public static void main(String[] args)
{
Duck mallard = new MallardDuck();
mallard.performQuack();
mallard.performFly();
}
}
但是上面的代码并没有动态设定鸭子的行为,这并不满足前面提到的设计原则第二个准则:Program to interface。
让行为变成动态
为了让鸭子的行为变成动态设定的,需要添加一个 setter 方法。代码如下:
// 在 Duck 类中增加这两个方法
public void setFlyBehavior(FlyBehavior fb)
{
flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}
// 创建一个新的 Duck 类
public class ModelDuck extends Duck
{
public ModelDuck()
{
flyBehavior = new FlyNoWay();
quackBehavior = new Quack();
}
public void display()
{
System.out.println(“I’m a model duck”);
}
}
// 创建一个新的 FlyBehavior 类型
public class FlyRocketPowered implements FlyBehavior
{
public void fly()
{
System.out.println(“I’m flying with a rocket!”);
}
}
// 新的测试类
public class MiniDuckSimulator
{
public static void main(String[] args)
{
Duck mallard = new MallardDuck();
mallard.performQuack();
mallard.performFly();
Duck model = new ModelDuck();
model.performFly();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
}
}
Has-A 比 Is-A 要好
每只 duck 都有一个 FlyBehavior 和一个 QuackBehavior,由它们来分配具体的行为。当你把两个类像这样放在一起,你就使用了 composition。这也引出了第三个设计原则:
偏爱组合,而不是继承。
Favor composition over inheritance.
正如所见,使用组合会使得你创建的系统更加灵活。不但可以封装这些类的算法族,而且可以动态改变他们的行为。
谈谈设计模式
刚刚的例子已经应用了一个设计模式:策略模式(Strategy Pattern)。它的正是定义如下:
The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.