【HF设计模式】01-策略模式
声明:仅为个人学习总结,还请批判性查看,如有不同观点,欢迎交流。
摘要
《Head First设计模式》第1章笔记:结合示例应用和代码,介绍策略模式,包括遇到的问题、应用的 OO 原则、达到的效果。
目录
- 摘要
- 1 示例应用
- 2 遇到问题
- 2.1 对应新需求
- 2.2 出现 BUG
- 2.3 寻找办法
- 3 引入设计模式
- 3.1 OO 原则:分离变和不变
- 3.2 OO 原则:针对接口编程
- 3.3 OO 原则:优先使用组合
- 3.3.1 行为组合
- 3.3.2 动态设置行为
- 3.3.3 HAS-A 比 IS-A 好
- 3.4 策略模式
- 3.4.1 模式定义
- 3.4.2 拓展示例
- 4 示例代码
- 4.1 Java 示例
- 4.2 C++11 示例
- 5 设计工具箱
- 5.1 OO 基础
- 5.2 OO 原则
- 5.3 OO 模式
- 参考
面向对象设计模式(Object-Oriented Design Patterns)
Christopher Alexander 说过:“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”。
尽管 Alexander 所指的是城市和建筑模式,但他的思想也同样适用于面向对象设计模式,只是在面向对象的解决方案里,我们用对象和接口代替了墙壁和门窗。
两类模式的核心都在于提供了相关问题的解决方案。
一般而言,一个模式有四个基本要素:模式名(pattern name)、问题(problem)、解决方案(solution)、效果(consequences)。
1 示例应用
一款鸭塘模拟游戏 SimUDuck:游戏中会出现各种鸭子,它们一边戏水,一边嘎嘎叫。
系统中包含一个 Duck
超类,各种类型的鸭子继承这个超类。
- 几乎所有鸭子都嘎嘎叫和戏水,所以由超类
Duck
负责实现quack()
和swim()
;- 橡皮鸭的叫声比较特殊,所以覆盖
quack()
,将其实现为 Squeak(吱吱叫);
注:由于当前 mermaid 类图不支持 note,所以方法(method)的返回类型都被用于作为注释,如 Squeak。
- 橡皮鸭的叫声比较特殊,所以覆盖
- 因为不同鸭子有不同的样子,所以
Duck::display()
为抽象方法,由各子类负责其具体实现。
2 遇到问题
2.1 对应新需求
需求描述:让鸭子能够飞行。
实现方式:只要为超类 Duck
添加 fly()
方法,所有鸭子就都能够飞行。
2.2 出现 BUG
BUG 描述:不是所有 Duck
的子类都应该会飞,比如没有生命的橡皮鸭、诱饵鸭。
原因分析:在给超类添加新的行为时,也给某些子类添加了不适合的行为。
2.3 寻找办法
可以像 橡皮Duck::quack() { //吱吱叫 }
方法一样,
使用 橡皮Duck::fly() { //不做任何事 }
方法覆盖超类的 Duck::fly()
方法:
继续查找其它不会飞行的鸭子,比如既不会飞也不会叫的诱饵鸭,修改其定义:
但是,采用当前“继承”的方式:
- 每当增加新的
Duck
子类时,都要留意检查是否需要覆盖quack()
和fly()
方法; - 每当修改
Duck
类的行为时,也要向下追踪所有定义了该行为的子类,判断是否需要进行修改。
上述过程,不但麻烦,还可能会引入新的 BUG。
思考题
使用继承来提供 Duck 的行为,存在下列哪些缺点?(多选)【答案在第 20 行】
Which of the following are disadvantages of using inheritance to provide Duck behavior? (Choose all that apply.)
A. 代码在多个子类中重复。Code is duplicated across subclasses.
B. 运行时的行为难以改变。Runtime behavior changes are difficult.
C. 没法让鸭子跳舞。We can’t make ducks dance.
D. 很难知道所有鸭子的全部行为。It’s hard to gain knowledge of all duck behaviors.
E. 鸭子不能边飞边叫。Ducks can’t fly and quack at the same time.
F. 变更会不经意地影响其它鸭子。Changes can unintentionally affect other ducks.
答案:A B D F
如果有一种构造软件的方法,能够使我们在变更时,对现有代码的影响达到最小,那岂不是太美妙了?
3 引入设计模式
设计模式的背后有着各种设计原则,我们先从“OO 原则”开始。
3.1 OO 原则:分离变和不变
在软件开发中,唯一的不变是变化。
不管你的应用设计得有多好,随着时间的推移,应用必定扩展和变更,否则,它就将消亡。
No matter how well you design an application, over time an application must grow and change or it will die.
在鸭塘模拟游戏中,鸭子的行为会不断改变。
对于这种情况,我们可以:提取出“变化的行为”,并把它“封装”起来。
这样,在修改“封装后的行为”时,就不会影响其它代码。
设计原则:(本原则是几乎所有设计模式的基础)
识别应用中变化的方面,把它们和不变的方面分开。
Identify the aspects of your application that vary and separate them from what stays the same.
原则应用场景:
- 每次有新需求时,如果都要改变某方面代码,那么这方面代码就要提取出来,与其它不变的代码分离;
- 在设计应用时,如果可以预测未来发生变化的方面,那么也可以预先提取变化的方面,从而让代码具有弹性。
在 SimUDuck 游戏中,嘎嘎叫 quack()
和 飞行 fly()
是 Duck
类中变化的方面,所以需要将它们分离出来:
从 Duck
类(以及所有子类)中提取出 quack()
和 fly()
方法。
3.2 OO 原则:针对接口编程
接下来,需要对提取出的 quack()
和 fly()
行为进行封装,具体方法是:定义“行为接口”,由“行为类”实现“行为接口”。
设计原则:
针对接口编程,而不是针对实现编程。
Program to an interface, not an implementation.
“针对接口编程”的要点是:通过针对超类编程,来利用多态。这样,实际的运行时对象不会被绑定为声明时的静态类型。
更详细的描述是:
- 在设计时,通过继承关系,定义超类类型和子类类型;
- 在编码时,
- 变量所声明的类型是超类类型,通常是抽象类或接口,而分配给变量的“实际对象类型”可以是超类的任何具体实现(子类类型);
- 变量所执行的操作,依据于超类中操作的签名;
- 在运行时,变量实际所执行的操作,对应于变量所绑定的“实际对象类型”的实现。
- 定义飞行行为接口
FlyBehavior
,其子类实现系统中需要的所有具体的fly()
行为; - 定义嘎嘎叫行为接口
QuackBehavior
,其子类实现系统中需要的所有具体的quack()
行为。
通过分离和封装:
- 便于复用:其它类型的对象可以复用飞行行为和嘎嘎叫行为(如,猎人使用的鸭鸣器),因为这些行为不再隐藏在
Duck
类的内部了; - 易于扩展:我们可以增加新的行为(如,利用火箭动力飞行),而不用修改任何已有的行为类,或修改任何使用已有行为的
Duck
类。
3.3 OO 原则:优先使用组合
3.3.1 行为组合
在完成分离和封装之后,我们还需要将 Duck
和行为组合在一起。
- 添加两个实例变量(数据成员),即
QuackBehavior
和FlyBehavior
类型的quackBehavior
和flyBehavior
(针对接口编程);
在运行时,这些变量会绑定具体的行为对象,如Squeak
和FlyWithWings
类型对象。 - 添加
performQuack()
方法,通过quackBehavior.quack()
实现嘎嘎叫行为; - 添加
performFly()
方法,通过flyBehavior.fly()
实现飞行行为。
因为 Duck
的子类决定具体的行为,所以在子类的构造函数中为 quackBehavior
和 flyBehavior
变量赋值:
public 橡皮Duck() {
quackBehavior = new Squeak();
flyBehavior = new FlyNoWay();
}
3.3.2 动态设置行为
既然鸭子的行为取决于变量的赋值,那么我们也完全可以在运行的过程中,进行变量赋值,也就是在运行时动态地改变行为。
为 Duck
类添加 setter
方法,在需要改变鸭子行为的时候,我们就调用这些方法。
public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}
public void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}
3.3.3 HAS-A 比 IS-A 好
经过分离、封装、组合后,形成的完整类图如下:
Duck
及其子类的继承关系保持不变;Duck
将嘎嘎叫行为委托给QuackBehavior
相关类;Duck
将飞行行为委托给FlyBehavior
相关类。
采用“组合方式”的设计之后,原“继承方式”所 遇到的问题和存在的缺点 已经得到解决,并且获得了 复用和弹性 的好处。
设计原则:
优先使用组合而不是继承。
Favor composition over inheritance.
3.4 策略模式
刚刚我们使用策略模式重新设计了 SimUDuck 游戏。
更具体的说法是:我们使用策略模式实现了鸭子的飞行行为和嘎嘎叫行为。
3.4.1 模式定义
策略模式(Strategy Pattern)
定义一(个)系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化。
Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
在需要支持多种算法或行为时,策略模式是一个替代继承的方法:
- 通过继承可以支持多种算法或行为
- 直接生成一个
Context
类的子类,从而给它以不同的行为; - 但是,这会将行为硬行编制到
Context
中,将行为的实现与Context
的实现混合起来,从而使Context
难以理解、难以维护和难以扩展,而且还不能动态地改变行为。
- 直接生成一个
- 策略模式将算法或行为封装在独立的
Strategy
类中- 可以独立于
Context
改变行为,使它易于切换、易于理解、易于扩展。
- 可以独立于
延伸阅读:《设计模式:可复用面向对象软件的基础》 5.9 Strategy(策略)—— 对象行为型模式 [P234-241]
3.4.2 拓展示例
使用策略模式实现武器(Weapon
)行为:
- 每个角色(
Character
)调用setWeapon()
切换武器,在打斗fight()
的过程中,通过weapon.useWeapon()
发起攻击。 - 任何对象都可以实现
WeaponBehavior
接口,例如回形针、牙膏、突变的海鲈。
4 示例代码
4.1 Java 示例
QuackBehavior
接口及实现类定义:
// QuackBehavior.java
public interface QuackBehavior {
public void quack();
}
// Quack.java
public class Quack implements QuackBehavior {
public void quack() { System.out.println("Quack"); }
}
// Squeak.java
public class Squeak implements QuackBehavior {
public void quack() { System.out.println("Squeak"); }
}
// MuteQuack.java
public class MuteQuack implements QuackBehavior {
public void quack() { System.out.println("<< Silence >>"); }
}
FlyBehavior
接口及实现类定义:
// FlyBehavior.java
public interface FlyBehavior {
public void fly();
}
// FlyWithWings.java
public class FlyWithWings implements FlyBehavior {
public void fly() { System.out.println("I'm flying!!"); }
}
// FlyNoWay.java
public class FlyNoWay implements FlyBehavior {
public void fly() { System.out.println("I can't fly"); }
}
// FlyRocketPowered.java
public class FlyRocketPowered implements FlyBehavior {
public void fly() { System.out.println("I'm flying with a rocket"); }
}
Duck
及子类定义:
// Duck.java
public abstract class Duck {
QuackBehavior quackBehavior;
FlyBehavior flyBehavior;
public Duck() {}
abstract void display();
public void swim() { System.out.println("All ducks float, even decoys!"); }
public void performQuack() { quackBehavior.quack(); }
public void performFly() { flyBehavior.fly(); }
public void setQuackBehavior(QuackBehavior qb) { quackBehavior = qb; }
public void setFlyBehavior(FlyBehavior fb) { flyBehavior = fb; }
}
// MallardDuck.java 野生鸭
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"); }
}
// ModelDuck.java 模型鸭
public class ModelDuck extends Duck {
public ModelDuck(FlyBehavior fb, QuackBehavior qb) {
flyBehavior = fb;
quackBehavior = qb;
}
public void display() { System.out.println("I'm a model duck"); }
}
测试代码:
// MiniDuckSimulator.java
public class MiniDuckSimulator {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.display();
mallard.performQuack();
mallard.performFly();
Duck model = new ModelDuck(new FlyNoWay(), new MuteQuack());
model.display();
model.performQuack();
model.performFly();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
}
}
4.2 C++11 示例
QuackBehavior
接口及实现类定义:
struct QuackBehavior {
virtual ~QuackBehavior() = default;
virtual void quack() const = 0;
};
struct Quack : public QuackBehavior {
void quack() const override { std::cout << "Quack\n"; }
};
struct Squeak : public QuackBehavior {
void quack() const override { std::cout << "Squeak\n"; }
};
struct MuteQuack : public QuackBehavior {
void quack() const override { std::cout << "<< Silence >>\n"; }
};
FlyBehavior
接口及实现类定义:
struct FlyBehavior {
virtual ~FlyBehavior() = default;
virtual void fly() const = 0;
};
struct FlyWithWings : public FlyBehavior {
void fly() const override { std::cout << "I'm flying!!\n"; }
};
struct FlyNoWay : public FlyBehavior {
void fly() const override { std::cout << "I can't fly\n"; }
};
struct FlyRocketPowered : public FlyBehavior {
void fly() const override { std::cout << "I'm flying with a rocket\n"; }
};
Duck
及子类定义:
struct Duck {
virtual ~Duck() = default;
virtual void display() const = 0;
void swim() const { std::cout << "All ducks float, even decoys!\n"; }
void performQuack() const { quackBehavior->quack(); }
void performFly() const { flyBehavior->fly(); }
void setQuackBehavior(std::shared_ptr<QuackBehavior> qb) { quackBehavior = qb; }
void setFlyBehavior(std::shared_ptr<FlyBehavior> fb) { flyBehavior = fb; }
protected:
std::shared_ptr<QuackBehavior> quackBehavior;
std::shared_ptr<FlyBehavior> flyBehavior;
};
struct MallardDuck : public Duck { // 野生鸭
MallardDuck() {
quackBehavior = std::make_shared<Quack>();
flyBehavior = std::make_shared<FlyWithWings>();
}
void display() const override { std::cout << "I'm a real Mallard duck\n"; }
};
struct ModelDuck : public Duck { // 模型鸭
ModelDuck(std::shared_ptr<QuackBehavior> qb,
std::shared_ptr<FlyBehavior> fb) {
quackBehavior = qb;
flyBehavior = fb;
}
void display() const override { std::cout << "I'm a model duck\n"; }
};
测试代码:
#include <iostream>
#include <memory>
// 在这里添加 QuackBehavior、FlyBehavior、Duck 相关类
int main() {
std::shared_ptr<Duck> duck;
duck = std::make_shared<MallardDuck>();
duck->display();
duck->performQuack();
duck->performFly();
duck = std::make_shared<ModelDuck>(std::make_shared<MuteQuack>(),
std::make_shared<FlyNoWay>());
duck->display();
duck->performQuack();
duck->performFly();
duck->setFlyBehavior(std::make_shared<FlyRocketPowered>());
duck->performFly();
}
5 设计工具箱
5.1 OO 基础
- 抽象(Abstraction)
- 抽象是一个过程,从业务需求中识别关键实体(类)及其特征(类成员),舍弃无关信息。为上层应用设计做准备。
Abstraction is a process of identifying essential entities (classes) and their characteristics (class members) and leaving irrelevant information from the business requirement to prepare a higher-level application design. - 一种实现抽象的方法是:从识别业务需求或功能请求中的名词开始。名词可以是人、地点、事物或过程。这些名词可以成为实体。在确定所需的实体之后,就可以收集每个实体的相关特征。特征是与每个实体相关的信息 (数据) 和行为 (方法)。
Abstraction starts with identifying nouns in the business requirement or feature request. A noun is a person, place, thing, or process. These nouns can be your entities. After figuring out the required entities you can then collect the relevant characteristics of each entity. Characteristics are information (data) and behaviors (methods) relevant to each entity.
- 抽象是一个过程,从业务需求中识别关键实体(类)及其特征(类成员),舍弃无关信息。为上层应用设计做准备。
- 封装(Encapsulation)
- 封装的结果是将对象的表示和实现隐藏起来。在对象之外,看不到其内部表示,也不能直接对其进行访问。操作(operation)是访问和修改对象表示的唯一途径。
The result of hiding a representation and implementation in an object. The representation is not visible and cannot be accessed directly from outside the object. Operations are the only way to access and modify an object’s representation.
- 封装的结果是将对象的表示和实现隐藏起来。在对象之外,看不到其内部表示,也不能直接对其进行访问。操作(operation)是访问和修改对象表示的唯一途径。
- 继承(Inheritance)
- 两个实体之间的一种关系,其中一个实体是基于另一个实体而定义的。
类继承以一个或多个父类为基础定义一个新类,这个新类继承了其父类的接口和实现,被称为子类或派生类。父类又被称为超类(Java)或基类(C++)。
A relationship that defines one entity in terms of another.
Class inheritance defines a new class in terms of one or more parent classes. The new class inherits its interface and implementation from its parents. The new class is called a subclass or a derived class. The parent class is also known as the superclass (Java) and base class (C++).
- 两个实体之间的一种关系,其中一个实体是基于另一个实体而定义的。
- 多态(Polymorphism)
- 在运行时刻,接口匹配的对象能互相替换的能力。
The ability to substitute objects of matching interface for one another at run-time.
- 在运行时刻,接口匹配的对象能互相替换的能力。
术语补充:
- 对象(object)
- 一个封装了数据及数据操作过程的运行实体。过程通常称为方法或操作。
A run-time entity that packages both data and the procedures that operate on that data. The procedures are typically called methods or operations.
- 一个封装了数据及数据操作过程的运行实体。过程通常称为方法或操作。
- 类(class)
- 类定义对象的接口和实现。它规定对象的内部表示,定义对象可实施的操作。
A class defines an object’s interface and implementation. It specifies the object’s internal representation and defines the operations the object can perform.
- 类定义对象的接口和实现。它规定对象的内部表示,定义对象可实施的操作。
- 接口(interface)
- 一个对象所有操作的签名的集合。接口描述了一个对象可以响应的请求的集合。
The set of all signatures defined by an object’s operations. The interface describes the set of requests to which an object can respond. - 除了上面抽象的概念,在 UML 类图和 Java 语言中,接口还是一种具体的构造,定义了一组方法的签名,而不包含方法的实现,由实现类负责提供这些方法的具体实现。
The word interface is overloaded. There’s the concept of an interface, but there’s also the Java construct of an interface.
- 一个对象所有操作的签名的集合。接口描述了一个对象可以响应的请求的集合。
- 操作(operation)
- 对象的数据仅能由其自身的操作来存取。对象在收到客户的请求(或消息)后,执行相应的操作。C++ 中的成员函数。
An object’s data can be manipulated only by its operations. An object performs an operation when it receives a request (or message) from a client. In C++, operations are called member functions.
- 对象的数据仅能由其自身的操作来存取。对象在收到客户的请求(或消息)后,执行相应的操作。C++ 中的成员函数。
- 实例变量(instance variable)
- 定义“对象表示的一部分”的数据。C++ 中的数据成员。
A piece of data that defines part of an object’s representation. C++ uses the term data member.
- 定义“对象表示的一部分”的数据。C++ 中的数据成员。
延伸阅读:《设计模式:可复用面向对象软件的基础》 1.6 设计模式怎样解决设计问题 [P10-22]
5.2 OO 原则
- 封装变化。
Encapsulate what varies. - 针对接口编程,而不是针对实现编程。
Program to interfaces, not implementations. - 优先使用组合,而不是继承。
Favor composition over inheritance.
5.3 OO 模式
策略模式:
- 定义一个算法家族,把其中的算法分别封装起来,使得它们之间可以互相替换。
Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. - 让算法的变化独立于使用算法的客户。
Strategy lets the algorithm vary independently from clients that use it.
参考
- [美]弗里曼、罗布森著,UMLChina译.Head First设计模式.中国电力出版社.2022.2
- [美]伽玛等著,李英军等译.设计模式:可复用面向对象软件的基础.机械工业出版社.2019.3
- wickedlysmart: Head First设计模式 Java 源码
Hi, I’m the ENDing, nice to meet you here! Hope this article has been helpful.