Last Updated on 2022年9月29日
For C/C++ user
很多设计模式相关的资料都是用
Java
来描述的,有必要简单补充一下Java
和C++
在OOP
技术层面上的区别
- Java不支持任何形式的运算符重载
- Java明确区分接口和类,类只能从一个类派生,但是一个类可以实现多个接口
- 在Java中,所有方法默认是虚(virtual)的
- 对CPP而言,
Java
风格的接口可以视为一个只有pure virtual
成员函数的基类,称为Interface
- implement,is-a(inherit),has-a
- implement: 特指继承
Interface
,并通过override
实现其中函数的行为. - is-a: 特指继承普通类后,普通类和基类的关系.
- has-a: 特指持有某个对象.
- implement: 特指继承
杂谈
- 设计模式可以说就是各种各样的"OOP最佳实践方法". 一个设计模式对应了一种组织代码的风格,对应了一种解决问题的策略.
- 设计模式可以说是程序员的"行话",使用"行话"有利于交流,但是并不一定有利于问题的解决,简单优雅的解决问题总是最重要的,不要本末倒置,避免过度设计.无论何时,总应该使用简单的工具,而不是"感觉更好"的工具,只在真实需要的时候才使用更复杂的设计.
- 设计模式不是简单的"编程规范",而是由{问题, 场景, 编程规范}复合而成的. 一个设计模式必然有其要解决的问题,只给出编程方法而不考虑问题及场景的, 肯定不是一个好的设计.
- 一个好OO程序员应该把其他程序员都当成傻瓜,就能写出好的OO代码
- 内聚:内聚性是指代码服务功能的相关程度,代码支持的功能越贴近于同一主题,内聚程度越高.
- 耦合:不同实体之间共享的数据/信息/代码越多,则耦合程度越高.
- 一个详细的设计说明/文档应当包含:
- 名称
- 意图: 解决什么问题,如何解决
- 应用范围: 给出典型场景,包括不适用的典型场景
- 结构图与类图
- 参与者: 描述各个类的角色
- 结果: 该模式的优点和缺点
- 实现: 语言层面需要注意的地方.
- 已知应用: 介绍实例
- 相关模式: 说明这个设计和已有其他模式的关系,相似之处,不同之处,改进之处等.
原则
- 将相同的部分抽象出来,将可能需要变化的部分独立出去.
- 除了具体类的实现代码,其余代码都应该仅出现基类指针/接口,也就是说,针对抽象编程,而不是针对某个具体类进行编程.(依赖倒置)
- 从实现上说,如果你需要依据类型信息进行分支选择,就是在针对具体类型进行编程,有必要时,就应该抽象出一个基类,消除这种分支选择.
- 多用组合,少用继承.
- 例如,持有一个多态接口,而不是继承多态接口一般会更好,就是说,
class Foo
持有一个InterfaceX *
比class Foo:public InterfaceX
更好.
- 例如,持有一个多态接口,而不是继承多态接口一般会更好,就是说,
- 当需要扩展已有对象的功能时,不要侵入已有的代码.(这一般要求已有的代码要保留好足够的开放性)
大纲
- 工厂: 创建对象的工具
- 策略: 使用组合替代继承,通过可调用成员实现多态.
- 状态: 状态机的OOP实现.
- 观察者: 基于通知实现自动化响应.
- 装饰器,外观,代理,适配器:包装对象,改变原来的行为.
- 模板方法: 提供框架式的代码
- 组合: 用对象树组织对象,使集合和个体有相同的接口.
- 命令: 将请求/操作封装为对象传递.
工厂模式
工厂模式与继承/多态基本无关,只是应用在OO体系中的一种重要编程方法,更不是一种"设计模式",只是习惯上被称为"工厂模式"
- 工厂模式是一种把对象创建权统一的工具,它的主要目标是: 将创建对象的权利保留在类开发者手中,而非类用户手中,保证对象创建都有统一的入口. 使用工厂模式时,一般要把类的构造函数设为
private
,强迫用户使用工厂. - 工厂模式的核心就是一个
T * Create(arg)
函数(或T * FactoryMethod(arg)
),依应用场景不同,可以分为简单工厂
,抽象工厂
,工厂方法
简单工厂和抽象工厂:
- 简单工厂是指不使用继承/多态特性的工厂, 一般可以直接实现为一个静态的
T * Create()
或者class Creator{T * Create()}
- 可以参考<
>书的 1.11对象池
, 来实现线程安全的的简单工厂,其中的第一个1.11.0的 version3 版本就足够好了
- 可以参考<
- 抽象工厂是指需要创建
creator
实例才能使用的工厂,通过class Creator{virtual T * Create()}
实现的工具.- 有时候,多个简单工厂需要按固定的组合使用,此时就可以使用抽象工厂把这些工厂函数封装在一个类内.
- 使用抽象工厂后
Creator * creator
具备更好的可扩展性,用户可以替换掉默认的creator
,类开发者也能提供多个不同类型的具体creator
供用户选用.
工厂方法:
- 在开发框架时,有的类也会额外提供创建对象的成员函数,这些成员函数一般被称为工厂方法.
- 通常而言,工厂方法是
virtual
的,可以供用户扩展.
class Process{
virtual BaseType * CreateObj();
void Exec(){
auto p_obj=CreateObj(); // 此时,调用的一般是派生类的`CreateObj()`,`p_obj`一般也是BaseType的派生类.
DoThings(p_obj);
}
}
单例模式
- 单例是一种特殊的工厂,它用于产生一个全局唯一的对象.
- 在C++11之前,高性能且线程安全的单例模式是比较难实现的,要么需要严格控制函数调用的时机,要么需要使用全局变量.
- 在C++ 11后,有两个常见的策略实现单例:
- 一般情况下可以直接使用
static
- Double-Checked Locking is Fixed In C++11
- 一般情况下可以直接使用
T & GetInstance(){
static T obj
return obj;
}
// 或者
struct TCreator(){
TCreator(){
p_obj = new T();
}
T * p_obj;
};
T* GetInstance()
{
static TCreator tc;
return tc.p_obj;
}
策略模式
- 理想的
virtual
成员函数应当是纯虚的,每个派生类都有完全不同的实现.不是这种理想状况时,就应该优先考虑策略模式. - 策略模式使用可调用的
handler
成员替代virtual
函数,主要优势:- 代码可复用, 我们不用重复的为每个派生类编写功能相同的代码,直接为
InterfaceX *
赋值即可 - 代码可扩展, 直接继承
InterfaceX
就能实现新的行为,不影响已有代码,且可以直接应用于新代码 - 可以动态修改,
InterfaceX *
的多态是运行期的,可以动态的替换.
- 代码可复用, 我们不用重复的为每个派生类编写功能相同的代码,直接为
- NOTE:
- 在C这样的语言中,可以通过函数指针实现弱的策略模式.
状态模式
- 状态机的主要特点是:
- 输入是无法预测的,必须对所有可能的输入产生响应.(响应可以是什么也不做,或者报警)
- 响应输入后,状态可能会产生改变.
- 状态模式的典型实现:
- 有一个
StateMachine
类,它负责持有全局的信息,接受所有外界的输入,只有这个类是暴露给用户的,用户不需要感知到"状态". - 有一个抽象基类
IState
和若干个XXXState
实现具体状态. IState
也需要能够支持响应任意输入- 具体类一般需要读取
StateMachine
内存储的全局信息,所以IState
一般会持有一个StateMachine
实例的引用. XXXState
彼此之间完全独立,负责执行具体的响应
- 有一个
- 在传统的状态机实现中,每一个
action_x
函数内都是一个巨大的switch-case
,根据当前的状态选择要执行的操作,经过状态模式解耦之后,各个状态只需要负责好自己的响应即可.添加新的具体State
也变得容易. - 状态模式可以近似看做是策略模式的一种变体.
StateMachine
其实就是在动态的切换自己持有的`stratety.
// 每一个action都可能触发状态变化,如果状态不变,那么就返回`this`,否则,返回一个new出来的新状态.
// 这种动态方案的代码结构更加简洁,也更易于扩展,相对的,会带来一些额外的new/delete开销.
class StateMachine;
class IState
{
public:
IState(StateMachine * machine):the_machine(machine){}
virtual IState * do_action_i();
virtual IState * do_action_j();
// 省略很多... do_action
private:
StateMachine * the_machine;
};
class StateMachine
{
public:
StateMachine();
// 仅需要把操作转发给current_state即可.
void action_i()
{
auto new_state = current_state->do_action_i();
ChangeStateTo(new_state);
}
void action_j()
{
auto new_state = current_state->do_action_j();
ChangeStateTo(new_state);
}
// ... 省略很多 acion_x()
void ChangeStateTo(IState * new_state){
if(new_state != current_state){
delete current_state;
current_state=new_state;
}
}
private:
IState* current_state = nullptr;
};
class DefaultState: public IState{
DefaultState(StateMachine * machine):IState(machine){}
IState * do_action_i() override
{
printf("Not supported");
return this;// state not change
}
IState * do_action_j() override
{
printf("action j has done");
return new ExampleState(the_machine);
}
// .... 省略很多 do_action_x()
}
StateMachine::StateMachine(){
current_state = DefaultState(this);
}
class ExampleState : public IState
{
// ... 省略
};
观察者模式:
- 典型场景:
Subject
负责主动刷新状态,再根据更新后的数据来通知Observer
- 这里的通知通常就是
subject
直接调用observer.update()
- 调用
obverser.update()
时,可以附带数据,用于传递数据,这种风格称为push
- 这里的通知通常就是
- 在现代的实现中,很多时候不需要
Observer
类,而是直接把可调用对象注册到subject
上,subject
直接调用即可.
装饰器,适配器,代理,外观
- 这几个模式都用于在运行期动态改变对象的行为.
命令模式
- 简而言之,命令模式就是封装过的可调用对象,这些可调用对象主要提供
exec()
,undo()
这样的接口.- 使用命令模式时,我们只需要知道它会干某件事,但是我们并不关心它干的是什么事.
- 命令模式一般会把函数和它需要的数据都封装起来.
模板方法模式:
- 由开发者A提供整体的算法流程,由开发者B实现部分可能不同的细节.
- 例如:基类提供框架,派生类提供部分API的细节.
- 例如:sort中可以由用户提供一个compare函数.
组合模式
- 为
class T
添加一个vector<T *> child_list
成员,使之可以按树状结构组织.- 例如,只需要
class Menu
一个类就可以同时代表"菜单"以及"菜单"中的item.
- 例如,只需要
child_list.size()
可以用于区分叶节点和中间节点,从而能让我们做一些区分的操作.- 例如:
class Menu
中,如果child_list
为空,则它是一个MenuItem
,反之,它是一个子菜单,我们据此就可以执行不同的操作,这是一种常用的设计.
- 例如:
MVC模式
- MVC模式广泛运用于带有GUI的系统中,有些GUI组件天生就适合用MVC来组织,例如媒体播放器.
- 想要理解MVC模式,就必须想象出三个独立的开发者,因为这个模式对于独立开发者而言意义比较模糊.
Model
开发者负责提供基础的API
,这些API
的具体何时调用需要由Controll
开发者控制;View
开发者负责提供更新界面的API,这些API的具体何时调用由Control
控制;View
也能收到用户触发的UI事件,但是View
并不负责进行响应,View
只把这个事件转发出去,具体要做什么由Controll
决定.View
向Controll
这个转发一般是异步地向controller
抛出一个事件,例如controller.send(some_event)
(也可以用观察者模式同步调用,Controll
是observer,主动 registerView
内可能发出的事件即可.)
- 从实现上看,典型的MVC使用了多个模式:
View
和Controller
都是Model
的观察者,在没有用户操作时(系统自动运作时),View
和Controller
可以自动的随Model
的更新而更新.- 例如,
Model
播放音乐时,View
的进度条就需要自动更新,当Model
播放到一半发现文件损坏时,Controll
就需要停止播放,并更新GUI
- 例如,
View
自身常常是按"组合模式"设计的,即整体是一个Tree
,当用户或者Controller
触发Event
时,Event
可以在树中的对象流动.- 在某些实现中,
Controller
会作提供工厂方法来创建Model
和View
对象
- QT/MFC这样的GUI框架,整体的设计逻辑是
Model/View
,把Control
和View
合并在一起,以QMyWidget
举例.Model
完全由用户给出,框架不涉及Model
相关的内容.QMyWidget
是用户实现的某个界面元素,它将同时负责View
和Control
的部分.QMyWidget
会在内部持有一个model
引用,并主动观察它,从而能自动响应model
的变化.View
在接收到UI事件(或model变化)时,直接会在OnXXXEvent
内操作model
,并更新自身.- 注: 在需要时,我们仍然可以手动的按
MVC
的风格开发,GUI框架对此没有任何限制. QT内也提供了一些按MVC
风格组织的类,例如文件树
和ListView/TreeView
类型擦除
在运行期将一系列不同类型的实例均转换为某个 ErasedType 类型, 从而统一类型, 使得这些不同类型的实例能存储在相同的容器中.
为了使用类型擦除,需要
- 定义一个统一基类作为ErasedType
- 定义一个辅助函数,用于将不同类型转换为ErasedType
例如,下面的例子中就可以把不同的动物Erase成Animal
class Animal {
public:
virtual const char *see() const = 0;
virtual const char *say() const = 0;
};
template <typename ConcreteType>
class AnimalEraser : public Animal {
ConcreteType *m_animal;
public:
AnimalModel(ConcreteType *animal) : m_animal(animal) {}
const char *see() const { return m_animal->see(); }
const char *say() const { return m_animal->say(); }
};
template <typename ConcreteType>
std::shared_ptr<Animal> EraseToAnimal(ConcreteType *animal) {
return std::make_shared<AnimalEraser>(animal);
}
class Dog {
public:
const char *see();
const char *say();
};
class Dog {
public:
const char *see();
const char *say();
};
void main(){
auto p_c = new Cat();
auto p_d = new Dog();
std::vector<std::shared_ptr<Animal>> = {EraseToAnimal(p_c),EraseToAnimal(p_d)}
}