【Head First 模式设计】第1章 策略模式


Intro to Design Patterns
Welcome to Design Patterns


SimUDuck应用程序

一个模拟鸭子游戏:SimUDuck。游戏中会出现各种鸭子,一边游泳,一边呱呱叫。此系统设计了一个鸭子超类 (Superclass),并让各种鸭子继承此超类。

MallardDuck display() Duck quack() swim() display() RedheadDuck display() OtherDuck

每个鸭子的子类型负责实现自己的 display() 行为,在屏幕上显示其外观。MallardDuck 外观是绿头,RedheadDuck 外观是红头。

软件开发的一个不变真理:改变

问题:改变程序,让鸭子能飞。

如果在 Duck 类中添加成员 fly(),会导致所有子类都具备 fly(),连那些不该具备 fly() 的类都不能免除。
例如,橡皮鸭不会飞,叫声是吱吱叫。

RubberDuck quack() display() fly()

可以覆盖橡皮鸭中的 fly() 方法,变成什么事都不做。

但是规范会不断变化,比如增加诱饵鸭,它不会飞也不会叫。这样开发人员总是需要检查,且可能重写程序中每个新的 Duck 子类的 fly() 和 quack()。

利用继承来提供 Duck 行为,会导致以下缺点:

  • 代码在多个子类中重复
  • 运行时的行为不容易改变
  • 很难知道所有鸭子的全部行为
  • 改变会牵一发动全身,造成其他鸭子不想要的改变

换一种方式,使用接口:将 fly() 从 Duck 超类中取出,并使用 fly() 方法创建 Flyable() 接口。这样,只有应该会飞的鸭子才能实现该接口并具有 fly() 方法,同理,创建 Quackable 接口。

MallardDuck display() fly() quack() Duck swim() display() RedheadDuck display() fly() quack() RubberDuck display() quack() DecoyDuck display() Flyable fly() Quackable quack()

虽然接口可以解决部分问题——不会再有会飞的橡皮鸭,但是却造成代码无法复用。甚至,鸭子的飞行姿势可能会有多种变化,而且会出现叫声不同的情况。

设计原则一

现在已经知道,继承与接口并不能很好地解决问题。有一个设计原则,可以适用这种情况。

设计原则1:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的混在一起。

下面是这个原则的另一种思考方式:把会变化的部分取出并封装起来,以便以后可以轻易地改动或扩充此部分,而不影响不需要改变的其他部分。

结果如何?代码变化引起的不经意后果变少,系统变得更有灵活性。

分开变化和不会变化的部分

将 quack() 和 fly() 两个行为从 Duck 类中分开:把它们从 Duck 类中取出来,建立一组新类来代表每个行为。

设计原则二

如何设计一组实现 fly 和 quack 行为的类呢?

让鸭子的行为变得灵活。而且要将行为赋值给 Duck 实例。
例如,实例化一个新的 MallardDuck 实例,并使用特定类型的飞行行为对其初始化。这时,可以动态地改变鸭子的行为。即,应该在 Duck 类中包含设定行为的方法,以便在运行时改变 MallardDuck 的飞行行为。

设计原则2:针对接口编程,而不是针对实现编程。

利用接口表示每个行为,比如 FlyBehavior 和 QuackBehavior,一个行为的每个实现将实现其中一个接口。

这样,Duck 类不需要知道它们自己行为的实现细节。

“针对接口编程”真正的意思是“针对超类型编程”

“针对接口编程”的关键是利用多态,这可通过对超类型编程来实现,以使实际的运行时对象不会被锁定在代码中。

可以将“针对超类型编程”改写为“变量的声明类型应该是超类型,通常是抽象类或接口,以便赋值给这些变量的对象可以是超类型的任何具体实现,这意味着声明它们的类不必知道实际的对象类型!”

例如,假设有一个抽象类 Animal,它有两个具体的实现类 Dog 和 Cat。

Dog makeSound() //bark() bark() Animal makeSound() Cat makeSound() //meow() meow()

针对实现编程:

Dog *d = new Dog();
d->bark();

针对接口/超类型编程:

Animal *animal = new Dog();
animal->makeSound();

知道对象是 Dog,但可以动态地使用 Animal 进行调用。

更好的是,与其将子类型的实例化 (例如 new Dog()) 硬编码到代码中,不如在运行时赋值具体的实现对象

a = getAnimal();
a->makeSound();

不知道实际的 Animal 子类型,只关心它知道如何正确进行 makeSound() 的动作。

实现鸭子的行为

两个接口 FlyBehavior 和 QuackBehavior,以及它们对应的实现每个具体行为的类。

FlyWithWings fly() «interface» FlyBehavior fly() FlyNoWay fly() Quack quack() «interface» QuackBehavior quack() Squeak quack() MuteQuack quack()

使用这种设计,对象的其他类型可以复用 fly 和 quack 行为,因为这些行为不再隐藏在 Duck 类中。
而且,可以添加新的行为,而无需修改任何现有的行为类,也不会影响到任何使用 fly 行为的 Duck 类。

整合鸭子的行为

关键在于,Duck 现在将委托 (delegate) 其飞行和呱呱叫行为,而不是使用 Duck 类 (或子类) 中定义的 fly 和 quack 方法。

做法:

  1. 首先,在 Duck 类中添加两个变量 flyBehavior 和 quackBehavior,它们被声明为接口类型。每个鸭子对象都将多态设置这些变量,以在运行时引用其想要的特定行为类型 (如 FlyWithWings)。
    从 Duck 类 (和所有子类) 中删除 fly() 和 quack() 方法,因为这些行为已被移到了 FlyBehavior 和 QuackBehavior 类中。
    用两种类似的方法 performDly() 和 performQuack() 替换 Duck 类中的 fly() 和 quack()。
Duck #flyBehavior : FlyBehavior #quackBehavior : QuackBehavior +performQuack() +swim() +display() +performFly()
  1. 现在,实现 performQuack()。
    为了执行呱呱叫动作,Duck 只需允许 quackBehavior 引用的对象为它呱呱叫。
    在下面这段代码中,我们不关心对象的类型是什么,我们关心的是它知道如何 quack()!
class Duck {
protected:
	std::unique_ptr<QuackBehavior> quackBehavior;
	// more
public:
	void performQuack() {
		// Duck对象本身并不处理呱呱叫行为,而是将这个行为委托给quackBehavior引用的对象
		quackBehavior->quack();
	}
};
  1. 如何设定 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;
}

运行代码:
运行结果

动态设定行为

可以通过鸭子子类上的设定方法来设定鸭子的行为类型,而不只是通过在鸭子的构造函数中实例化鸭子的行为类型。

  1. 在 Duck 类中,添加两个方法。
    可以随时调用下面两个方法改变鸭子的行为。
	public:	void setFlyBehavior(FlyBehavior* fb) {
		flyBehavior = std::unique_ptr<FlyBehavior>(fb);
	}
	public: void setQuackBehavior(QuackBehavior* qb) {
		quackBehavior = std::unique_ptr<QuackBehavior>(qb);
	}
  1. 创建一个新的 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;
	}
};
  1. 创建一个新的 FlyBehavior 类型 FlyRocketPowered。
// 创建一个利用火箭动力飞行的行为
class FlyRocketPowered : public FlyBehavior {
public:
	void fly () const {
		std::cout << "I'm flying with a rocket" << std::endl;
	}
};
  1. 在 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 (实现)。

Duck #flyBehavior : FlyBehavior #quackBehavior : QuackBehavior +swim() +display() +performQuack() +performFly() +setFlyBehavior() +setQuackBehavior() MallardDuck display() RedheadDuck display() RubberDuck display() DecoyDuck display() FlyWithWings fly() «interface» FlyBehavior fly() FlyNoWay fly() Quack quack() «interface» QuackBehavior quack() Squeak quack() MuteQuack quack()

设计原则三

HAS-A 可能比 IS-A 更好

HAS-A 关系:每个鸭子有一个 FlyBehavior 和 QuackBehavior,可以将飞行和呱呱叫行为委托给它们。

将两个类像这样结合起来,就是组合 (composition)。鸭子获取行为不是通过继承行为,而是通过与适当的行为对象进行组合

这是一个很重要的技术,使用了下面的设计原则。

设计原则3:优先考虑组合,而非继承。

使用组合建立系统可以带来更大的灵活性。它不仅可以将算法族封装到它们自己的类的集合中,而且还可以在运行时改变行为,只要组合的对象实现了正确的行为接口即可。

策略模式

上面的应用程序使用了策略模式 (Strategy Pattern)。

策略模式定义了一族算法,将每个算法封装起来,并使它们之间可互相替换。策略模式使算法的变化可以独立于使用该算法的客户。

共享模式词汇的力量

设计模式为你提供了与其他开发人员共享的词汇表。掌握了这些词汇后,你可以更轻松地与其他开发人员进行交流。通过在模式级别进行思考,而不是在具体细节的对象级别进行思考,还可以提高你对架构的思考。

当你使用模式与他人沟通时,你做的不只是在与他人共享“行话”。

  • 共享的模式词汇是强有力的。当你使用模式与其他开发人员或团队交流时,你们交流的不只是模式名称,还是模式所代表的一整套质量、特性和约束。
    “使用策略模式来实现鸭子的各种行为。” 这告诉你,鸭子行为已封装到它自己的一组类中,可以被轻松地扩展和更改,甚至在需要时也可以在运行时进行更改。
  • 模式让你使用更少的话语表述更多的内容。当你在描述中使用模式时,其他开发人员会快速准确地了解你头脑中的设计。
  • 在模式级别进行交谈可以使你待“在设计中”的时间更长。讨论使用模式的软件系统使你可以将讨论保持在设计级别,而不必深入了解实现对象和类的具体细节。
  • 共享的词汇可以为你的开发团队提供强大的动力。精通设计模式的团队可以更快地行动,减少误解的空间。
  • 共享的词汇表鼓励更多的初级开发人员快速上手。初级开发人员仰赖经验丰富的开发人员。当高级开发人员使用设计模式时,初级开发人员也会开始学习它们。在你的组织中建立模式使用者的社区。

如何使用设计模式

我们都使用过现成的库和框架。我们使用它们,针对它们的API编写一些代码,将它们编译到我们的程序中。库和框架对开发模型大有帮助,我们可以选择组件并直接将其放入适当的位置。但是,它们不能帮助我们以更易于理解、更易于维护和更灵活的方式来构建应用程序。这就是设计模式的用武之地。

设计模式不会直接进入你的代码,而是会首先进入你的大脑。一旦你在大脑中装载了许多关于模式的运用知识,就可以开始将其应用于新设计,并在发现旧代码变得混乱没有灵活性时对其进行重新处理。

设计模式比库的等级更高。设计模式告诉我们如何构建类和对象来解决某个问题。

框架和库不是设计模式;它们提供特定的实现,可以应用到我们的代码中。有时,库和框架会在实现中使用模式设计。一旦理解了设计模式,你会更快地理解围绕着设计模式构建的API。

为什么要使用设计模式

知道抽象、继承和多态这些概念不会让你成为一个好的面向对象的设计者。设计大师考虑如何创建灵活的设计,它是可维护的,可以应对变化。

如果找不到设计模式怎么办?

构成模式的基础是一些面向对象的原理,当找不到适合的模式解决问题时,这些原理会为你提供帮助。

创建可维护的OO系统的秘诀之一就是思考它们将来如何变化,这些原则解决了这些问题。

设计工具箱内的工具

OO基础:抽象、封装、多态、继承

OO原则:

  • 将变化的部分封装起来
  • 多用组合,少用继承
  • 针对接口编程,不针对实现编程

OO模式:

  • 策略模式

要点:

  • 好的OO设计是可复用的、可扩充的、可维护的。
  • 模式是经过验证的面向对象的经验。
  • 模式不会为你提供代码,而是为你提供设计问题的通用解决方案。
  • 大多数模式和原则都应对软件变化问题。
  • 大多数模式都允许系统的某些部分的变化独立于所有其他部分。
  • 我们经常尝试取出系统中的变化部分,并将其封装。

【Head First 模式设计】目录

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值