软件设计模式 - 行为模式
软件设计模式 - 行为模式
行为模式负责对象之间的高效沟通和职责委派。
行为模式包括:责任链,命令,迭代器,中介者,备忘录,观察者,状态,策略,模版方法,访问者。
责任链模式(Chain of Responsibility/CoR)
责任链模式允许你将请求沿着处理者链进行传递。每个节点上的处理者收到请求后,可对请求进行处理,或者将请求继续传递给链上的下个处理者。
例如:
这里只是两级责任链。
示例代码:
class Handler {
public:
void handle() = 0;
};
class BasicHandler : public Handler {
public:
void handle() {
// TODO: 进行第一级必要处理
if (okay && next != nullptr) {
next(); // 转上级责任链节点处理
}
}
void setNext(Handler& hdr) {
next = hdr
}
private:
Handler next;
};
class SecondHandler : public Handler {
public:
void handle() {
// TODO: 进行必要处理
}
// 最后一级不必设置next
// void setNext(Handler& hdr) {
// next = hdr
// }
private:
// Handler next;
};
命令模式(Command)
命令模式将请求转换成包含与请求相关的所有信息的独立对象,并且对象存放在队列中,队列元素可以进行撤销操作。
这里以编辑器软件的设计为例,如下:
示例代码:
class Command {
public:
void execute() = 0;
};
// 打开文件
class OpenCommand : public Command {
public:
void execute() {
// 进行打开文件操作
}
};
// 保存文件
class SaveCommand : public Command {
public:
void execute() {
// 进行保存文件操作
}
};
// 打印文件
class PrintCommand : public Command {
public:
void execute() {
// 进行文件打印的操作
}
};
此外还需要一个命令队列机制:
// 命令队列
class CommandQueue {
public:
void push(Command&) {
// 命令入列
}
Command pop() {
// 命令出列
}
};
命令队列用于存储命令,并且队列的进出使用一定的调度算法:
CommandQueue cq(10);
// 可以根据具体的调度方式,这里使用定时器来调度
while (true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
auto cmd = cq.pop();
cmd.execute();
}
如果要进行撤销操作,只需推出队列元素不用执行即可:
cq.pop();
如果创建菜单打开操作,只需要如下:
auto openCmd = new OpenCommand();
auto menuOpen = new Menu("Open");
menuOpen.action = [openCmd](){
cq.push(openCmd); // 发送打开命令进入队列
};
迭代器模式(Iterator)
迭代器模式的目的是为了不暴露集合的底层的前提下,让外部仍然可以遍历集合中的所有元素。
结构如下:
首先定义一个集合:
// 迭代器
class Iterator {
public:
Iterator next() = 0;
};
class Item : public Iterator {
public:
Item next() {
// 下一个元素
}
};
// 迭代器集合
class IteratorCollection {
public:
Iterator create(Iterator) = 0;
Iterator begin() = 0;
Iterator end() = 0;
};
class Collection : public IteratorCollection {
public:
Iterator create(Iterator) {
// 建立一个迭代器
}
Iterator begin() {
// 返回第一个迭代器
}
Iterator end() {
// 返回最后一个迭代器
}
};
迭代器集合构造可以如下:
Collection collection;
Item empty;
first = collection.create(empty);
second = collection.create(first);
third = collection.create(second);
对于迭代器集合的构造,方式多样,可以如上面通过函数逐个构造,也可以通过构造函数进行构造。
Collection collection([1,2,3,4,5,6,7,8]);
使用迭代器集合来访问迭代器元素:
for (auto it = collection.begin(); it != collection.end(); it = it.next()) {
// 访问迭代器元素it
}
中介者模式(Mediator)
中介者模式是为了减少对象之间混乱无序的依赖关系,该模式会限制对象之间的直接交互,而是使用一个中介对象作为交互者。
假设在一个表单UI交互设计的场景下,用户可以填写姓名,出生日期,性别等信息,每一栏信息的更新也会触发其他UI栏目的元素变化,例如:填写完出生日期会触发表单中年龄更新,如果所有的UI控件元素之间直接交互来更新,那么这将导致最终的混乱。如果使用中介者就可以避免这种混乱的发生。
如上图,更改出生日期后,更新年龄,使用中介者就避免了直接调用导致的混乱。
class Mediator {
public:
void notify(int type, std::string param) = 0;
bool registerObject() = 0;
};
class UserInformation {
public:
void notify(int type, std::string param) {
// 根据type不同,通知不同的对象进行UI更新
}
bool registerObject() {
// 注册UI对象
}
private:
std::vector<UIObject> objects;
};
使用时,内存环境维持一个唯一的用户信息,这样定义:
UserInformation info;
info.registerObject(userNameLabel);
info.registerObject(birthLabel);
info.registerObject(ageLabel);
// ...
用户触发了UI控件的值修改:
info.notify(123, "1975-02-12"); // 假设123为更新出生日期的事件,参数是更新后的日期
备忘录模式(Memento)
备忘录模式允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态.
在编辑器软件中一般都支持操作的撤销与恢复,实现如下:
如图中,文档打开时由一个初始原发器构成,下一步的快照基于该原发器,以此类推,然后,快照由一个快照列表构成来存储管理,代码如下:
class Memento {
public:
void restore() = 0;
};
class Originator {
public:
Memento* save() = 0;
};
class Snapshot : public Memento {
public:
Snapshot(Memento* m, int s) {
originator = m;
state = s;
// 存储操作状态
}
void restore() {
// 恢复操作状态
}
};
class Initial : public Originator {
public:
void setState(int state) {
this->state = state;
}
Memento* save() {
return Snapshot(this, state);
}
private:
int state;
};
定义管理器,用于管理快照,创建和恢复快照:
std::vector<Snapshot*> snapshots;
// 初始原发器
Initial init;
init.setState(0);
// 保存快照
Snapshot* s1 = new Snapshot(init, 1);
snapshots.push_back(s1);
// 恢复快照
auto s = snapshots.pop_back();
s->restore();
观察者模式(Observer)
观察者模式是一种发布订阅机制,允许当对象状态发生改变时,通知多个其他的“观察”对象。
这里以代码编辑器为例,代码编辑器不仅是文本编辑器,同时支持基于代码文本的语法加亮,自动缩进,括号自动匹配等功能。
代码实现如下:
class EventListener {
public:
void update() = 0;
};
class Highlight : public EventListener {
public:
void update() {
// 更新语法高亮
}
};
class AutoIndent : public EventListener {
public:
void update() {
// 更新自动缩进
}
};
class AutoBracket : public EventListener {
public:
void update() {
// 更新括号匹配
}
};
class EventManager {
public:
void subscribe(int type, EventListener* listener) {
auto it = listeners[type];
if (it == nullptr) {
it = initListenerList(); // 初始化监听者列表
listeners.insert(std::make_pair(type, it));
}
it->push_back(listener); // 添加监听者
}
void unsubscribe(int type, EventListener* listener) {
auto it = listeners[type];
if (it == nullptr) return;
if (find_if(it, listener)) { // 查找监听者
it = remove_if(it, listener); // 删除
listeners[type] = it;
}
}
void notify(int type, DataFrame* data) {
// 遍历所有监听该事件的监听者并发送通知
auto it = listeners[type];
if (it == nullptr) return;
for (l := it->begin(); l != it->end(); l = l->next()) {
l->update(); // 通知事件
}
}
private:
std::map<int, ListenerList*> listeners;
};
class Editor {
public:
Editor() {}
private:
EventManager eventMgr;
};
状态模式(State)
状态模型是指一个对象的状态发生变化后,它的行为也会发生改变。其主要思想是有限状态机的原理。
空调恒温系统是一个最简单的状态机,如下图:
对于状态模型的类定义如下:
代码如下:
class AirConditionerState {
public:
AirConditionerState() {
// 初始化状态转换表
transits.insert(compound(STATE_START, SYM_RDY), STATE_ON);
transits.insert(compound(STATE_ON, SYM_BELOW), STATE_WAIT);
// ...
}
void transit(int cur) { // 转换函数
auto sym = cur > 26 ? SYM_ABOVE : SYM_BELOW;
auto key = compound(state, sym);
auto val = transits.find(key);
state = val;
}
private:
int state;
std::map<int, int> transits;
inline int compound(int state, int symbol) {
return state | (symbol << 16);
}
};
每次调用时,只需将当前的温度作为transit()
函数的参数传入,函数会根据状态和符号在状态表中切换状态。
状态机原理
状态机原理是由符号表,状态表,转移函数,以及开始和结束,简单可以用一句话以概括:状态机通过符号表的符号在转移函数上切换状态表中的不同状态。状态机分为有穷状态机和无穷状态机,无穷状态机的状态表大小不同于有穷状态机,无穷状态机可存在无限个状态。有穷状态机在编译技术的词法分析和构造形式化语法分析都会用到。
策略模式(Strategy)
策略模式能够实现一种算法自定义的功能。例如,如果现在有一款软件支持机器学习,为了使用它,我们需要进行模型选择并加以训练,以用于后续的实际预测。这里的模型即可以抽象化为策略,对于不同的模型选择,即为对不同的策略选择。
代码实现:
class Strategy {
public:
void train() = 0;
void predict() = 0;
};
class LinearRegression : public Strategy {
public:
void train() {
// 线性回归训练
}
void predict() {
// 线性回归预测
}
};
class DecisionTree : public Strategy {
public:
void train() {
// 决策树训练
}
void predict() {
// 决策树预测
}
};
class Forest : public Strategy {
public:
void train() {
// 森林训练
}
void predict() {
// 森林预测
}
};
class NeuralNetwork : public Strategy {
public:
void train() {
// 神经网络训练
}
void predict() {
// 神经网络预测
}
};
class AIDecision {
public:
void setStrategy(Strategy* s) {
strategy = s;
scheduleTrain();
}
void decisionPredict() {
strategy->predict();
}
private:
Strategy* strategy;
};
调用者使用,如下:
AIDecision ai;
// 使用神经网络模型的策略进行预测
auto stra = new NeuralNetwork();
ai.setStrategy(stra);
ai.decisionPredict();
模版方法模式(Template Method)
模版方法是在超类中定义一套算法框架,继承该超类的子类可以在不修改结构的情况下重写算法的特定步骤。
例如,编辑器软件中,可以将基本操作方法设计为超类,但是对于不同类型的文件处理方式,子类可以重写方法实现特定文件的操作。
代码如下:
class BasicEditor {
public:
void open() {
// 打开文件
}
void load() {
// 加载文件数据
}
void close() {
// 关闭文件
}
void save() {
// 保存文件
}
void find() {
// 查找文件
}
void undo() {
// 恢复
}
void redo() {
// 重做
}
void goto_() {
// 光标偏移
}
void select() {
// 文本选择
}
};
class RtfEditor : public BasicEditor {
public:
void load() {
// 富文本数据加载
}
};
class DocEditor : public BasicEditor {
public:
void load() {
// Doc文档加载
}
};
class PdfEditor : public BasicEditor {
public:
void load() {
// Pdf文档数据加载
}
void save() {
// 不支持保存,Pdf只支持查看,不能更改
}
void undo() {
// 不支持保存,Pdf只支持查看,不能更改
}
void redo() {
// 不支持保存,Pdf只支持查看,不能更改
}
};
访问者模式(Visitor)
访问者模式是把算法和访问对象隔离开来的方式。
这种方式下,访问对象不需要任何更新,同时可以达到对象属性访问能力。
上图中,实现的是通过访问者模式从应用程序的菜单和工具栏获取图标图片的示例。
class CtrlItem {
public:
Image* image() = 0;
};
class ToolbarItem : public CtrlItem {
public:
Image* image() {
// 返回图标图片
}
};
class MenuItem : public CtrlItem {
public:
Image* image() {
// 返回图标图片
}
};
class Visitor {
public:
Image* visit(ToolbarItem*) = 0;
Image* visit(MenuItem*) = 0;
};
class ImageVisitor : public Visitor {
public:
Image* visit(ToolbarItem* item) {
return item->image();
}
Image* visit(MenuItem* item) {
return item->image();
}
};
使用访问者模式如下:
ToolbarItem* item1 = new ToolbarItem("a.png");
ToolbarItem* item2 = new ToolbarItem("b.png");
MenuItem* item3 = new MenuItem("c.png");
ImageVisitor visitor;
auto image1 = visitor.visit(item1);
auto image2 = visitor.visit(item2);
auto image3 = visitor.visit(item3);