Intro to Design Patterns
Welcome to Design Patterns
SimUDuck应用程序
一个模拟鸭子游戏:SimUDuck。游戏中会出现各种鸭子,一边游泳,一边呱呱叫。此系统设计了一个鸭子超类 (Superclass),并让各种鸭子继承此超类。
每个鸭子的子类型负责实现自己的 display() 行为,在屏幕上显示其外观。MallardDuck 外观是绿头,RedheadDuck 外观是红头。
软件开发的一个不变真理:改变
问题:改变程序,让鸭子能飞。
如果在 Duck 类中添加成员 fly(),会导致所有子类都具备 fly(),连那些不该具备 fly() 的类都不能免除。
例如,橡皮鸭不会飞,叫声是吱吱叫。
可以覆盖橡皮鸭中的 fly() 方法,变成什么事都不做。
但是规范会不断变化,比如增加诱饵鸭,它不会飞也不会叫。这样开发人员总是需要检查,且可能重写程序中每个新的 Duck 子类的 fly() 和 quack()。
利用继承来提供 Duck 行为,会导致以下缺点:
- 代码在多个子类中重复
- 运行时的行为不容易改变
- 很难知道所有鸭子的全部行为
- 改变会牵一发动全身,造成其他鸭子不想要的改变
换一种方式,使用接口:将 fly() 从 Duck 超类中取出,并使用 fly() 方法创建 Flyable() 接口。这样,只有应该会飞的鸭子才能实现该接口并具有 fly() 方法,同理,创建 Quackable 接口。
虽然接口可以解决部分问题——不会再有会飞的橡皮鸭,但是却造成代码无法复用。甚至,鸭子的飞行姿势可能会有多种变化,而且会出现叫声不同的情况。
设计原则一
现在已经知道,继承与接口并不能很好地解决问题。有一个设计原则,可以适用这种情况。
设计原则1:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的混在一起。
下面是这个原则的另一种思考方式:把会变化的部分取出并封装起来,以便以后可以轻易地改动或扩充此部分,而不影响不需要改变的其他部分。
结果如何?代码变化引起的不经意后果变少,系统变得更有灵活性。
分开变化和不会变化的部分
将 quack() 和 fly() 两个行为从 Duck 类中分开:把它们从 Duck 类中取出来,建立一组新类来代表每个行为。
设计原则二
如何设计一组实现 fly 和 quack 行为的类呢?
让鸭子的行为变得灵活。而且要将行为赋值给 Duck 实例。
例如,实例化一个新的 MallardDuck 实例,并使用特定类型的飞行行为对其初始化。这时,可以动态地改变鸭子的行为。即,应该在 Duck 类中包含设定行为的方法,以便在运行时改变 MallardDuck 的飞行行为。
设计原则2:针对接口编程,而不是针对实现编程。
利用接口表示每个行为,比如 FlyBehavior 和 QuackBehavior,一个行为的每个实现将实现其中一个接口。
这样,Duck 类不需要知道它们自己行为的实现细节。
“针对接口编程”真正的意思是“针对超类型编程”
“针对接口编程”的关键是利用多态,这可通过对超类型编程来实现,以使实际的运行时对象不会被锁定在代码中。
可以将“针对超类型编程”改写为“变量的声明类型应该是超类型,通常是抽象类或接口,以便赋值给这些变量的对象可以是超类型的任何具体实现,这意味着声明它们的类不必知道实际的对象类型!”
例如,假设有一个抽象类 Animal,它有两个具体的实现类 Dog 和 Cat。
针对实现编程:
Dog *d = new Dog();
d->bark();
针对接口/超类型编程:
Animal *animal = new Dog();
animal->makeSound();
知道对象是 Dog,但可以动态地使用 Animal 进行调用。
更好的是,与其将子类型的实例化 (例如 new Dog()
) 硬编码到代码中,不如在运行时赋值具体的实现对象:
a = getAnimal();
a->makeSound();
不知道实际的 Animal 子类型,只关心它知道如何正确进行 makeSound() 的动作。
实现鸭子的行为
两个接口 FlyBehavior 和 QuackBehavior,以及它们对应的实现每个具体行为的类。
使用这种设计,对象的其他类型可以复用 fly 和 quack 行为,因为这些行为不再隐藏在 Duck 类中。
而且,可以添加新的行为,而无需修改任何现有的行为类,也不会影响到任何使用 fly 行为的 Duck 类。
整合鸭子的行为
关键在于,Duck 现在将委托 (delegate) 其飞行和呱呱叫行为,而不是使用 Duck 类 (或子类) 中定义的 fly 和 quack 方法。
做法:
- 首先,在 Duck 类中添加两个变量 flyBehavior 和 quackBehavior,它们被声明为接口类型。每个鸭子对象都将多态设置这些变量,以在运行时引用其想要的特定行为类型 (如 FlyWithWings)。
从 Duck 类 (和所有子类) 中删除 fly() 和 quack() 方法,因为这些行为已被移到了 FlyBehavior 和 QuackBehavior 类中。
用两种类似的方法 performDly() 和 performQuack() 替换 Duck 类中的 fly() 和 quack()。
- 现在,实现 performQuack()。
为了执行呱呱叫动作,Duck 只需允许 quackBehavior 引用的对象为它呱呱叫。
在下面这段代码中,我们不关心对象的类型是什么,我们关心的是它知道如何 quack()!
class Duck {
protected:
std::unique_ptr<QuackBehavior> quackBehavior;
// more
public:
void performQuack() {
// Duck对象本身并不处理呱呱叫行为,而是将这个行为委托给quackBehavior引用的对象
quackBehavior->quack();
}
};
- 如何设定 flyBehavior 和 quackBehavior 实例变量?以 MallardDuck 类为例:
class MallardDuck : public Duck {
public:
MallardDuck() {
quackBehavior = std::unique_ptr<QuackBehavior>(new Quack());
flyBehavior = std::unique_ptr<FlyBehavior>(new FlyWithWings());
}
void display() const {
std::cout << "I'm a real Mallard duck" << std::endl;
}
};
当 MallardDuck 被实例化时,其构造函数将 MallardDuck 的继承的 quackBehavior 实例变量初始化为 Quack 类型 (QuackBehavior 的具体实现类) 的新实例。
测试 Duck 的代码
// Duck.h
#include <iostream>
#include <memory>
#include "FlyBehavior"
#include "QuackBehavior"
class Duck {
protected:
std::unique_ptr<FlyBehavior> flyBehavior;
std::unique_ptr<QuackBehavior> quackBehavior;
public:
Duck() { }
virtual void display() const = 0;
void performFly() const {
flyBehavior->fly();
}
void performQuack() const {
quackBehavior->quack();
}
void swim() const {
std::cout << "All ducks float, even decoys!" << std::endl;
}
};
class MallardDuck : public Duck {
public:
MallardDuck() {
quackBehavior = std::unique_ptr<QuackBehavior>(new Quack());
flyBehavior = std::unique_ptr<FlyBehavior>(new FlyWithWings());
}
void display() const {
std::cout << "I'm a real Mallard duck" << std::endl;
}
};
****************************************************************************
// FlyBehavior.h
#include <iostream>
#include <memory>
class FlyBehavior {
public:
virtual void fly () const = 0;
};
class FlyWithWings : public FlyBehavior {
public:
void fly () const {
std::cout << "I'm flying!!" << std::endl;
}
};
class FlyNoWay : public FlyBehavior {
public:
void fly () const {
std::cout << "I can't fly" << std::endl;
}
};
****************************************************************************
// QuackBehavior.h
#include <iostream>
#include <memory>
class QuackBehavior {
public:
virtual void quack() const = 0;
};
class Quack : public QuackBehavior {
public:
void quack () const {
std::cout << "Quack" << std::endl;
}
};
class MuteQuack : public QuackBehavior {
public:
void quack () const {
std::cout << "<< Silence >>" << std::endl;
}
};
class Squeak : public QuackBehavior {
public:
void quack () const {
std::cout << "Squeak" << std::endl;
}
};
****************************************************************************
// main.cpp
#include <iostream>
#include <memory>
#include "Duck.h"
#include "FlyBehavior.h"
#include "QuackBehavior.h"
int main() {
std::unique_ptr<MallardDuck> mallard(new MallardDuck());
mallard->performQuack();
mallard->performFly();
system("pause");
return 0;
}
运行代码:
动态设定行为
可以通过鸭子子类上的设定方法来设定鸭子的行为类型,而不只是通过在鸭子的构造函数中实例化鸭子的行为类型。
- 在 Duck 类中,添加两个方法。
可以随时调用下面两个方法改变鸭子的行为。
public: void setFlyBehavior(FlyBehavior* fb) {
flyBehavior = std::unique_ptr<FlyBehavior>(fb);
}
public: void setQuackBehavior(QuackBehavior* qb) {
quackBehavior = std::unique_ptr<QuackBehavior>(qb);
}
- 创建一个新的 Duck 类型 ModelDuck。
class ModelDuck : public Duck {
public:
ModelDuck() {
quackBehavior = std::unique_ptr<QuackBehavior>(new Quack());
flyBehavior = std::unique_ptr<FlyBehavior>(new FlyNoWay()); // 一开始,模型鸭是不会飞的
}
void display() const {
std::cout << "I'm a model duck" << std::endl;
}
};
- 创建一个新的 FlyBehavior 类型 FlyRocketPowered。
// 创建一个利用火箭动力飞行的行为
class FlyRocketPowered : public FlyBehavior {
public:
void fly () const {
std::cout << "I'm flying with a rocket" << std::endl;
}
};
- 在 main 函数中增加 ModelDuck,并使模型鸭具有火箭动力。
std::unique_ptr<ModelDuck> model(new ModelDuck());
model->performFly();
model->setFlyBehavior(new FlyRocketPowered());
model->performFly();
运行程序:
封装行为的大局观
现在看看整体的格局。下面是整个重新设计的类结构。
注意,现在开始对事物的描述有所不同。不再将鸭子行为视为一组行为 (set of behaviors),而是将它们视为一族算法 (family of behaviors)。想想看:在 SimUDuck 设计中,算法代表鸭子会做的事情 (呱呱叫或飞行的不同方式),这样的技术可以很容易地用于一组类,用来实现计算不同州的销售税金。
注意类之间的关系 (relationships)。
在类图中的每个箭头上标上适当的关系:IS-A (是一个)、HAS-A (有一个) 和 IMPLEMENTS (实现)。
设计原则三
HAS-A 可能比 IS-A 更好
HAS-A 关系:每个鸭子有一个 FlyBehavior 和 QuackBehavior,可以将飞行和呱呱叫行为委托给它们。
将两个类像这样结合起来,就是组合 (composition)。鸭子获取行为不是通过继承行为,而是通过与适当的行为对象进行组合。
这是一个很重要的技术,使用了下面的设计原则。
设计原则3:优先考虑组合,而非继承。
使用组合建立系统可以带来更大的灵活性。它不仅可以将算法族封装到它们自己的类的集合中,而且还可以在运行时改变行为,只要组合的对象实现了正确的行为接口即可。
策略模式
上面的应用程序使用了策略模式 (Strategy Pattern)。
策略模式定义了一族算法,将每个算法封装起来,并使它们之间可互相替换。策略模式使算法的变化可以独立于使用该算法的客户。
共享模式词汇的力量
设计模式为你提供了与其他开发人员共享的词汇表。掌握了这些词汇后,你可以更轻松地与其他开发人员进行交流。通过在模式级别进行思考,而不是在具体细节的对象级别进行思考,还可以提高你对架构的思考。
当你使用模式与他人沟通时,你做的不只是在与他人共享“行话”。
- 共享的模式词汇是强有力的。当你使用模式与其他开发人员或团队交流时,你们交流的不只是模式名称,还是模式所代表的一整套质量、特性和约束。
“使用策略模式来实现鸭子的各种行为。” 这告诉你,鸭子行为已封装到它自己的一组类中,可以被轻松地扩展和更改,甚至在需要时也可以在运行时进行更改。 - 模式让你使用更少的话语表述更多的内容。当你在描述中使用模式时,其他开发人员会快速准确地了解你头脑中的设计。
- 在模式级别进行交谈可以使你待“在设计中”的时间更长。讨论使用模式的软件系统使你可以将讨论保持在设计级别,而不必深入了解实现对象和类的具体细节。
- 共享的词汇可以为你的开发团队提供强大的动力。精通设计模式的团队可以更快地行动,减少误解的空间。
- 共享的词汇表鼓励更多的初级开发人员快速上手。初级开发人员仰赖经验丰富的开发人员。当高级开发人员使用设计模式时,初级开发人员也会开始学习它们。在你的组织中建立模式使用者的社区。
如何使用设计模式
我们都使用过现成的库和框架。我们使用它们,针对它们的API编写一些代码,将它们编译到我们的程序中。库和框架对开发模型大有帮助,我们可以选择组件并直接将其放入适当的位置。但是,它们不能帮助我们以更易于理解、更易于维护和更灵活的方式来构建应用程序。这就是设计模式的用武之地。
设计模式不会直接进入你的代码,而是会首先进入你的大脑。一旦你在大脑中装载了许多关于模式的运用知识,就可以开始将其应用于新设计,并在发现旧代码变得混乱没有灵活性时对其进行重新处理。
设计模式比库的等级更高。设计模式告诉我们如何构建类和对象来解决某个问题。
框架和库不是设计模式;它们提供特定的实现,可以应用到我们的代码中。有时,库和框架会在实现中使用模式设计。一旦理解了设计模式,你会更快地理解围绕着设计模式构建的API。
为什么要使用设计模式
知道抽象、继承和多态这些概念不会让你成为一个好的面向对象的设计者。设计大师考虑如何创建灵活的设计,它是可维护的,可以应对变化。
如果找不到设计模式怎么办?
构成模式的基础是一些面向对象的原理,当找不到适合的模式解决问题时,这些原理会为你提供帮助。
创建可维护的OO系统的秘诀之一就是思考它们将来如何变化,这些原则解决了这些问题。
设计工具箱内的工具
OO基础:抽象、封装、多态、继承
OO原则:
- 将变化的部分封装起来
- 多用组合,少用继承
- 针对接口编程,不针对实现编程
OO模式:
- 策略模式
要点:
- 好的OO设计是可复用的、可扩充的、可维护的。
- 模式是经过验证的面向对象的经验。
- 模式不会为你提供代码,而是为你提供设计问题的通用解决方案。
- 大多数模式和原则都应对软件变化问题。
- 大多数模式都允许系统的某些部分的变化独立于所有其他部分。
- 我们经常尝试取出系统中的变化部分,并将其封装。