c++设计模式详解
- 1. 设计模式分类
- 2. 好的面向对象设计
- 3. 设计原则
- 4. 创建型模式
- 5. 结构型模式
- 6. 行为型模式
- 7. 参考资料
- 7. 参考资料
1. 设计模式分类
设计模式一般有 23 种设计模式,可分为三类:
- 创建型模式(Creational Patterns)
- 结构型模式(Structural Patterns)
- 行为型模式(Behavioral Patterns)
2. 好的面向对象设计
2.1. 三大面向对象机制
- 封装:隐藏内部实现(创建型模式)
- 继承:复用现有代码(结构型模式)
- 多态:改写对象行为(行为型模式)
2.2. 重构技法
静态->动态
早绑定->晚绑定
继承->组合
编译时依赖->运行时依赖
紧耦合->松耦合
2.3. 软件设计需要解决的因素
产品需求的变化
开发技术的升级换代
技术团队的变更
市场环境的改变
…
设计模式应对的是稳定中的变化。
一般来说,抽象基类是稳定,具体实现是变化的。
3. 设计原则
设计原则是设计模式的核心。
- 单一职责原则(Single Responsibility Principle)
每个类应该实现单一的职责,否则就应该把类拆分。
解读:每个类只做一件事,可以降低代码的耦合性,也方便代码的维护。实现新需求应注意不随意修改旧代码,注意职责的划分。
- 开-闭原则(Open Closed Principle)
对外可以扩展,对内不能修改。
解读:用抽象构建框架,用实现扩展细节。实现新需求不改动原有类,而是应该实现抽象出来的接口(或具体类继承抽象类)。
- 里氏替换原则(Liskov Substitution Principle)
里氏替换原则主要是规范子类与基类(父类)继承的关系。
里氏替换原则是对“开-闭原则”的补充。实现“开闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范。
解读:要求衍生类可以替换基类,软件单位的功能不受到影响,此时基类才能真正被复用。子类中可以增加自己特有的方法,也可以实现父类的抽象方法,但是不能重写或重载父类的非抽象方法,否则该继承关系就不是一个正确的继承关系。因为父类代表了定义好的结构,与外界交互,子类不应该随便破坏它。
- 依赖倒置原则(Dependence Inversion Principle)
面向接口编程,依赖抽象而不依赖实现。
解读:写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。针对接口编程,而不是针对实现编程。尽量不要从具体的类派生,而是以继承抽象类或实现接口来实现。一般业务层处于上层,逻辑层和数据层归为底层。
- 接口隔离原则(Interface Segregation Principle)
每个接口不存在子类用不到却必须实现的方法。否则将接口拆分成多个隔离的接口。
解读:软件不应该依赖不需要实现的接口。尽量细化接口,接口中的方法应尽量少。但也不能太少,如果过少会造成接口数量过多,设计复杂化。
- 迪米特法则(最少知道原则)(Demeter Principle)
一个类对自己依赖的类知道的越少越好。无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
解读:一个类应该只和它的成员变量,方法的输入,返回参数中的类作交流,而不应该引入其他的类。
我们要求陌生的类不要作为局部变量出现在类中。最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。
4. 创建型模式
创建型模式用于构建对象,以便它们可以从实现系统中分离出来。主要有5种:
- 单例模式(Singleton Pattern)
- 工厂方法模式(Factory Method Pattern)
- 抽象工厂模式(Abstract Factory Pattern)
- 建造者模式(Builder Pattern)
- 原型模式(Prototype Pattern)
4.1. 单例模式
4.1.1. 简介
单例模式是指整个系统中一个类只有一个对象的实例。
单例模式虽然简单,但有一个经典问题:如何保证多线程安全?
- 构造要点
- 构造私有private
如果要保证一个类不能多次被实例化,那么肯定要阻止对象被多次new出来,所以需要把类的所有构造方法私有化。 - 以静态方法static返回实例
因为外界不能通过new来获得对象,所以我们要通过提供类的方法GetInstance()来让外界获取对象实例。 - 确保对象实例只有一个
只对类进行一次实例化,以后都直接获取第一次实例化的对象。
// 单例
class Singleton
{
public:
// 修改返回类型为指针类型
static Singleton* GetInstance()
{
static Singleton instance;
return &instance;
}
private:
Singleton() {}
};
-
解决问题
实例对象保证只有一个。 -
分类
单例模式一般分为懒汉模式,饿汉模式。
// 单例 - 饿汉式
Singleton *Singleton::m_pSingleton = new Singleton();
Singleton *Singleton::GetInstance()
{
return m_pSingleton;
}
饿汉模式指先把对象创建好,等我要用时直接来拿就行。
优点:最简单
缺点:容易造成资源上的浪费(如:我把对象创建好了,但不用)
// 单例 - 懒汉式
Singleton *Singleton::m_pSingleton = NULL;
Singleton *Singleton::GetInstance()
{
if (m_pSingleton == NULL)
m_pSingleton = new Singleton();
return m_pSingleton;
}
懒汉模式指先不创建类的对象实例,等需要时再创建。
优点:解决饿汉模式可能会造成资源浪费的问题
缺点:懒汉模式在并发情况下可能引起的问题,会出现创建多个对象的情况
因为可能出现外界多人同时访问SingleCase.getInstance()方法,这里可能会出现因为并发问题导致类被实例化多次,所以懒汉模式需要加上锁synchronized (Singleton.class) 来控制类只允许被实例化一次。
- UML
4.1.2. 使用效果?优缺点?
优点:
- 单例模式可以在系统设置全局的访问点, 优化和共享资源访问。
- 由于单例模式在内存中只有一个实例, 减少了内存开支,减少了系统的性能开销,避免对资源的多重占用。
缺点:
- 单例模式与单一职责原则有冲突。 一个类应该只实现一个逻辑, 而不关心它是否是单例的, 是不是要单例取决于环境, 单例模式把“要单例”和业务逻辑融合在一个类中。
- 单例模式一般没有接口, 扩展很困难。
4.1.3. 使用时注意事项!
单例模式在多线程中会出现问题
Singleton *Singleton::GetInstance()
{
if (m_pSingleton == NULL)
m_pSingleton = new Singleton();
return m_pSingleton;
}
如何保证多线程安全?
方法一:直接加锁
Singleton *Singleton::GetInstance()
{
Lock lock;
if (m_pSingleton == NULL)
m_pSingleton = new Singleton();
return m_pSingleton;
}
这种方法锁的代价过高,不管m_pSingleton是不是NULL,其他线程都会陷入等待锁的释放。
方法二:双检查锁,由于内存读写reorder不安全
构造对象的逻辑过程:开辟对象的内存空间->执行构造函数初始化的操作->返回指向对象的指针
reorder指令执行的过程:开辟对象的内存空间->返回指向对象的指针—在这一步时—>执行构造函数初始化的操作
当其他线程获取该对象时,可以获取对象的指针,但是没有执行构造函数初始化,就会在造成安全问题。
内存读写reorder不安全这个问题是由编译器优化造成的,一度很难解决。
c#,Java引入关键词volatile约束实例对象,让编译器不会reorder,按照顺序步骤进行。
Singleton *Singleton::GetInstance()
{
if (m_pSingleton == NULL){
Lock lock;
m_pSingleton = new Singleton();
}
return m_pSingleton;
}
方法三:双检查锁,c++11版本之后的跨平台实现(c++中vc2005之后添加了volatia,但只能在windows中使用)
std::atomic<Singleton*> Singleton::m_pSingleton; //约束为原子对象
std::mutex Singleton::m_mutex;
Singleton *Singleton::GetInstance()
{
Singleton* tmp= m_pSingleton.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
if (tmp == NULL){
std::lock_guarg<std::mutex> lock(m_mutex);
tmp = m_pSingleton.load(std::memory_order_relaxed);
if (tmp == NULL){
tmp = new Singleton();
std::atomic_thread_fence(std::memory_order_release);//释放内存fence
m_pSingleton.store(tmp,std::memory_order_relaxed);
}
}
return m_pSingleton;
}
4.1.4. 使用场景?业界知名例子
- 像日志管理、打印机、数据库连接池、应用配置。
- Singleton 模式经常和 Factory( AbstractFactory) 模式在一起使用, 因为系统中工厂对象一般来说只要一个。
- 在整个项目中需要一个共享访问点或共享数据, 例如一个Web页面上的计数器, 可以不用把每次刷新都记录到数据库中, 使用单例模式保持计数器的值, 并确保是线程安全的。
- 创建一个对象需要消耗的资源过多, 如要访问IO和数据库等资源等。
4.1.5. 难点
单例模式看着很简单,但实现上却很复杂,如何做到单例模式下多线程安全是个难点,多线程编程需要认真学习。
需要注意的一点是,上面讨论的线程安全指的是UML中的获取Instance()是线程安全的,假如多个线程都获取类A的对象,如果只是只读操作,完全OK,但是如果有线程要修改,有线程要读取,那么类A自身的函数需要自己加锁防护,不是说线程安全的单例也能保证修改和读取该对象自身的资源也是线程安全的。
4.2. 工厂方法模式
4.2.1. 简介
- 构造要点
- 工厂类创建对象产品具体类
- 使用面向对象的手法,将具体对象创建延迟到子类
-
解决问题
为一类对象创建构造接口 -
举例
现在有奔驰、宝马、奥迪工厂、分别生产奔驰、宝马、奥迪汽车 -
UML
4.2.2. 使用效果?优缺点?
优点:
- 工厂类扩展比较容易
- 具有封装性,不需要知道产品类是如何创建的,只要知道如何调用工厂类的接口去创建具体的产品即可
缺点:
- 产品类扩展会很麻烦,违背开闭原则
4.2.3. 使用场景?业界知名例子
工厂方法模式还可以与其他模式混合使用,如模板方法模式、 单例模式、原型模式等。
4.3. 抽象工厂模式
4.3.1. 简介
- 构造要点
- 抽象工厂与工厂模式类似,区别是应对多系列对象的构建
- “系列对象”指某一特定系列下的对象之间有相互依赖、作用的关系
- 不同系列的对象之间不能有相互依赖
-
解决问题
为多类(相关)对象创建构造接口 -
举例
现在有奔驰、宝马、奥迪工厂、分别生产奔驰、宝马、奥迪汽车和自行车 -
UML
4.3.2. 使用效果?优缺点?
优点:
- 应对“新系列”的需求变动(工厂),工厂类扩展比较容易
- 具有封装性,不需要知道产品类是如何创建的,只要知道如何调用工厂类的接口去创建具体的产品即可
缺点:
- 难以应对“新对象的需求变动”(产品),产品类扩展会很麻烦,违背开闭原则
4.3.3. 使用场景?业界知名例子
具体的工厂类可以使用单例模式。
使用场景较多,如:在软件产品开发过程中, 涉及不同操作系统的时候,可以考虑使用抽象工厂模式。
例如一个应用, 需要在三个不同平台(Windows、 Linux、
Android(Google发布的智能终端操作系统) ) 上运行,此时通过抽象工厂模式屏蔽掉操作系统对应用的影响。
三个不同操作系统上的软件功能、 应用逻辑、 UI都应该是非常类似的, 唯一不同的是调用不同的工厂方法, 由不同的产品类去处理与操作系统交互的信息。
4.4. 建造者模式
4.4.1. 简介
- 构造要点
- Director对象并不直接返回对象,而是通过一步步(BuildPartA, BuildPartB, BuildPartC)来进行对象的创建
- 分步骤构建对象,其中“分步骤”是稳定不变的,复杂对象的各个部分则经常变化
- 产品不需要设置抽象类,每个步骤中具体操作都不同生成的产品差异巨大,没必要设置公共父类
- 针对变化的各个部分进行封装
- 在c++中不能在构造器中调用虚函数,java、c#则可以
-
解决问题
分步骤构建对象,每个步骤里面的操作不同,就能构建不同的对象 -
举例
组装 ThinkPad、Yoga电脑,都需要组装CPU、主板、内存 、显卡。
同时CPU、主板、内存 、显卡可以选择不同的型号。 -
UML
4.4.2. 使用效果?优缺点?
优点:
- 封装性
客户端不必知道产品内部组成的细节, 如例子中我们就不需要关心每一个具体的模型内部是如何实现的, 产生的对象类型就是ThinkPadModel、YogaModel。 - 建造者独立, 容易扩展
ThinkPad、Yoga的建造者是相互独立的, 对系统的扩展非常有利。
缺点:
4.4.3. 使用场景?业界知名例子
建造者模式经常会与模板方法模式结合。
4.5. 原型模式
4.5.1. 简介
- 构造要点
- 通过克隆自己clone()创建对象,拷贝构造函数(深拷贝)
- 创建对象的过程中需要保留中间状态,使用原型模式
-
解决问题
“克隆”再创建
即一个对象的产生可以不由零起步, 直接从一
个已经具备一定雏形的对象克隆, 然后再修改为生产需要的对象 -
举例
孙悟空复制出来成千上万的孙悟空 -
UML
4.5.2. 使用效果?优缺点?
优点:
- 性能优良
在需要频繁产生相似对象时,可以使用原型模式。
缺点:
4.5.3. 使用场景?业界知名例子
组合模式和装饰器模式可以与原型模式结合使用。
通过new产生一个对象需要非常繁琐的数据准备或访问权限,或类初始化需要消化非常多的资源(数据、 硬件资源等),可以使用原型模式。
一个对象需要提供给其他对象访问, 而且各个调用者可能都需要修改其值时, 可以考虑使用原型模式拷贝多个对象供调用者使用。
切忌不能浅拷贝!由于浅拷贝还是指向原生对象的内部元素地址,其对象内部的数组、 引用对象等都不拷贝。
4.6. 创建型模式对比
-
抽象工厂模式 VS 工厂模式
AbstractFactory 模式是为创建一组( 有多类) 相关或依赖的对象提供创建接口
Factory 模式是为一类对象提供创建接口或延迟对象的创建到子类中实现
AbstractFactory 模式通常都是使用 Factory 模式实现 -
原型模式和建造者模式、抽象工厂模式
都是通过一个类(对象实例)来专门负责对象的创建工作(工厂对象)
Builder 模式重在复杂对象的一步步创建(并不直接返回对象)
AbstractFactory 模式重在产生多个相互依赖类的对象
Prototype 模式重在从自身复制自己创建新类 -
建造者模式 VS 抽象工厂模式
功能相似,因为都是用来创建大的复杂的对象Builder 模式强调的是一步步创建对象,并通过相同的创建过程可以获得不同的结果对象,一般来说 Builder 模式中对象不是直接返回的
AbstractFactory 模式中对象是直接返回的, AbstractFactory 模式强调的是为创建多个相互依赖的对象提供一个同一的接口
5. 结构型模式
结构型模式用于在许多不同的对象之间形成大型对象结构。主要有7种:
- 适配器模式(Adapter Pattern)
- 组合模式(Composite Pattern)
- 装饰者模式(Decorator Pattern)
- 桥接模式(Bridge Pattern)
- 外观模式(Facade Pattern)
- 享元模式(Flyweight Pattern)
- 代理模式(Proxy Pattern)
5.1. 适配器模式
5.1.1. 简介
- 构造要点
- 复用现存的类,接口与复用环境不一致时
- 应对新环境的迁移,制定成新的一套新的接口规范即适配器
-
解决问题
复用现有接口,解决接口不兼容的问题 -
举例
在俄罗斯提供的插座是使用双脚圆形充电,而自带的充电器是双脚扁型
那么可以在双脚扁型的充电器外面加一个电源适配器匹配俄罗斯的双脚圆形插座 -
分类
- 类适配器
多继承
通过 private、protected、public 继承 IAdaptee 获得实现继承的效果
通过 public 继承 ITarget 获得接口继承的效果
class Adpater : public ITarget,
protected IAdaptee{
}
- 对象适配器
对象组合
class Adpater : public ITarget{
protected:
IAdaptee* pAdaptee;
}
- UML
5.1.2. 使用效果?优缺点?
优点:
- 提高了类的复用度,灵活性非常好
缺点:
1.
5.1.3. 使用场景?业界知名例子
适配器模式最好在详细设计阶段不要考虑它, 它不是为了解决还处在开发阶段的问题。
这个模式使用的主要场景是扩展应用中, 系统扩展了但不符合原有设计的时候才考虑通过适配器模式减少代码修改带来的风险。
5.2. 组合模式
5.2.1. 简介
- 构造要点
- 将对象组合成树形结构以表示“部分-整体”的层次结构(普遍存在的对象容器),组合模式使用户对单个对象和组合对象具有一致性(稳定)
- 对于子节点(Leaf)的管理策略是STL中的vector,或数组、链表、Hash表等。
- 组合模式核心是将客户代码和复杂的对象容器结构解耦,客户代码与纯粹的抽象接口发生依赖,而非对象容器的内部实现
- 内部实现递归调用
-
解决问题
只要是树形结构,体现局部和整体的关系时,考虑使用组合模式 -
举例
江湖组织架构
江湖公司(任我行)创建分支日月神教(东方不败)、五岳剑派(左冷蝉),
还有叶子节点少林(方证大师)、武当(冲虚道长) -
UML
叶子节点继承Component抽象基类不应该有Add()、Remove()、GetChild()方法,所以通用类图还有另一种表示方式:
5.2.2. 使用效果?优缺点?
优点:
- 高层模块调用简单
一棵树形结构构中的所有节点都是Component, 局部和整体对调用者来说没有任何区别,也就是说, 高层模块不必关心自己处理的是单个对象还是整个组合结构, 简化了高层模块的代码。 - 节点自由扩展,增加树节点或叶子结点只要找到父节点即可,符合开闭原则。
缺点:
- 树节点和叶节点使用时定义了直接使用的实现类!这在面向接口编程上是与依赖倒置原则冲突。
5.2.3. 使用场景?业界知名例子
- 导航菜单一般都是树形的结构,文件夹管理等
- 常用的XML结构也是一个树形结构, 根节点、 元素节点、 值元素这些都与我们的组合模式相匹配
5.3. 装饰者模式
5.3.1. 简介
- 构造要点
- 主体类在多个方向上的扩展功能
- 装饰类继承主体类,也将主体具体类组合
- 运行时动态扩展(多态)
-
解决问题
动态地给对象添加一些额外的职责,就增减功能来说,装饰者比生成子类更灵活。 -
举例
饮料为主体类,现在有黑咖啡和深度烘培咖啡豆两种,但现在我想让这些饮料有一些味道,怎么做?
加调味品!奶油,摩卡,糖浆
黑咖啡 + 奶油
黑咖啡 + 摩卡 -
UML
5.3.2. 使用效果?优缺点?
优点:
- 扩展性非常好,装饰器模式比生成子类更灵活,可以动态扩展功能
- 装饰类和被装饰类可以独立发展, 而不会相互耦合。Decorator类是从外部来扩展Component类的功能, 而Decorator也不用知道具体的构件
缺点:
- 有比较多的小对象,难维护,难排错。
5.3.3. 使用场景?业界知名例子
继承是静态地给类增加功能, 而装饰模式则是动态地增加功能。所以装饰模式可以替代继承, 解决我们类膨胀的问题。
当需要动态地给一个对象增加功能,或动态地撤销功能,以及为一批的兄弟类进行改装或加装功能时,使用装饰模式。
5.4. 桥接模式
5.4.1. 简介
- 构造要点
- 使用对象组合解耦了抽象与实现的关系
- 子类要继承父类的关系,就必须先实现所有的方法,但是有很多方法子类不需要实现,此时可以将这些方法抽离出去,当子类在需要用时使用桥接模式去取这些方法
- 继承关系多且复杂,颗粒度要求较细
-
解决问题
将抽象部分和实现部分分离,使他们都可以独立地变化。桥接模式可以代替多继承方案。 -
举例
将拉链式开关和电灯关联起来,将两位开关和风扇关联起来,这里主体是开关。
开关是abstract,电器是implementor。可以看到的开关是抽象的,不用管里面具体怎么实现的。 -
UML
5.4.2. 使用效果?优缺点?
优点:
- 将抽象部分和实现部分分离,可以代替多继承方案。
- 易扩展,具有一定的封装性。实现细节对客户透明,它已经由抽象层通过聚合关系完成了封装。
缺点:
5.4.3. 使用场景?业界知名例子
桥梁模式的意图还是对变化的封装, 尽量把可能变化的因素封装到最细、 最小的逻辑单元中, 避免风险扩散。 因此在进行系统设计时, 发现类的继承有N层时, 可以考虑使用桥梁模式。
- 不希望或不适用使用继承的场景
例如继承层次过渡、 无法更细化设计颗粒等场景, 需要考虑使用桥梁模式。 - 接口或抽象类不稳定的场景
明知道接口不稳定还想通过实现或继承来实现业务需求, 那是得不偿失的。 - 重用性要求较高的场景设计的颗粒度越细, 则被重用的可能性就越大, 而采用继承则受父类的限制, 不可能出
现太细的颗粒度。
5.5. 外观模式
5.5.1. 简介
- 构造要点
- 为子系统中的接口提供一致的界面,外观模式提供一个高层接口是的子系统更加容易使用
- 外观模式更多的是架构设计模式,注重从架构模式去看整个系统,而不是单个类的层次(松耦合)
- 组件内部应为耦合关系比较大的一系列组件,而不是任意的功能组合(高内聚)
通常来说,只需要一个Facade对象,Facade对象一般为单例模式。
-
解决问题
使外界通过子系统的高层接口访问系统内部实现,很好地实现隔离与封装 -
举例
提供接口供其他人调用
网购时流程:收到订单->提交给订单团队->提交给供应商->提交给快递员
我们不需要知道其中的内部细节,如“收到订单->提交给订单团队”需要经历的步骤:“收到”, “确认付款”, “联系供应商”, “完成” -
UML
解耦系统,隔离客户程序和系统。
5.5.2. 使用效果?优缺点?
优点:
- 减少系统的相互依赖。所有的依赖都是对Facade对象的依赖, 与子系统无关。不管子系统内部如何变化, 只要不影响到门面对象,任你自由活动。
- 对子系统来说更安全。
缺点: - 不符合开闭原则。 对修改关闭, 对扩展开放。这个缺点很严重。
5.5.3. 使用场景?业界知名例子
- 为一个复杂的模块或子系统提供一个供外界访问的接口
- 子系统相对独立——外界对子系统的访问只要黑箱操作即可。
比如银行利息的计算问题, 没有深厚的业务知识和扎实的技术水平是不可能开发出该子系统的, 但是对于使用该系统的开发人员来说, 他需要做的就是输入金额以及存期, 其他的都不用关心, 返回的结果就是利息。 - 预防低水平人员带来的风险扩散
比如一个低水平的技术人员参与项目开发, 为降低个人代码质量对整体项目的影响风险, 一般的做法是“画地为牢”, 只能在指定的子系统中开发, 然后再提供Facade对象接口进行访问
操作。
5.6. 享元模式
5.6.1. 简介
- 构造要点
- 运用共享技术有效地支持大量细粒度的对象
- 有一个享元对象工厂去获取对象
- 对象由享元对象池管理,可以由map、list等实现
- 对象池中已经有创建的对象就直接获取,没有对象就创建对象并插入对象池
-
解决问题
解决面向对象的代价问题(性能问题)
面对需要大量使用的对象时,采用对象共享的方法来降低系统中对象的个数
降低细粒度对象给系统的压力 -
举例
一个字母“ a”在文档中出现了100000 次, 而实际上我们可以让这一万个字母“ a” 共享一个对象,
当然因为在不同的位置可能字母“ a” 有不同的显示效果(如字体和大小等设置不同)
此时可将对象的状态分为“外部状态”和“ 内部状态”
将可以被共享(不会变化)的状态作为内部状态存储在对象中
而外部状态(如上面提到的字体、 大小等) 可作为参数传递给对象 -
UML
5.6.2. 使用效果?优缺点?
优点:
- 大大减少应用程序创建的对象,减少程序内存,增强程序的性能。
缺点:
- 提高了系统复杂性,需要分离出外部状态和内部状态。而且外部状态具有固化特性,不应该随内部状态改变而改变, 否则导致系统的逻辑混乱。
- 当对象池中的享元对象数量比想成数量还要少时,极可能发生线程不安全的问题。
5.6.3. 使用场景?业界知名例子
- 系统中存在大量的相似对象。
- 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关。
- 需要缓冲池的场景。
5.7. 代理模式
5.7.1. 简介
- 构造要点
- 为其他对象提供一种代理来控制对这个对象的访问
- Proxy类知道需要代理的类的属性和方法
- 在架构层次提供代理类,提供一层间接层,避免直接使用对象带来的一些问题
-
解决问题
让抽象和实现彻底解耦 -
举例
电信运行商提供充值服务,同时也有很多代理点提供充值服务 -
UML
-
分类
- 创建开销大的对象时候,比如显示一幅大的图片,我们将这个创建的过程交给代理去完成,GoF称之为虚代理(Virtual Proxy)。
- 为网络上的对象创建一个局部的本地代理,比如要操作一个网络上的一个对象(网络性能不好的时候,问题尤其突出),我们将这个操纵的过程交给一个代理去完成,GoF称
之为远程代理(Remote Proxy)。 - 对对象进行控制访问的时候,比如在 Jive 论坛中不同权限的用户(如管理员、普通用户等)将获得不同层次的操作权限,我们将这个工作交给一个代理去完成,GoF称之为保护代理( Protection Proxy)。
- 智能指针( Smart Pointer),取代了简单的指针,他在访问对象时执行一些附加的操作。关于这个方面的内容, 建议参看 Andrew Koenig 的《C++沉思录》中的第5章。
5.7.2. 使用效果?优缺点?
优点:
- 职责清晰,编程简洁清晰。真实的角色就是实现实际的业务逻辑, 不用关心其他非本职的事务, 通过后期的代理完成一件事务。
- 高扩展性,虽然具体主题角色是随时都会发生变化的, 但只要它实现了接口, 代理类完全可以在不做任何修改的情况下使用。
- 智能化,代理可分类,还有:动态代理(Java中的面向横切面编程,即AOP[AspectOriented Programming],其核心就是采用了动态代理机制)。动态代理指在实现阶段不用关心代理谁, 而在运行阶段才指定代理哪一个对象。
缺点:
1.
5.7.3. 使用场景?业界知名例子
代理模式应用得非常广泛,大到一个系统框架、企业平台, 小到代码片段、事务处理,都可能会用到代理模式。
5.8. 结构型模式对比
-
适配器模式 VS 代理模式
适配器模式为他所是配的对象提供了一个不同的接口。
代理模式提供了与代理类实体相同的接口,由于保护代理的存在,其接口实际上可能只是实体接口的一个子集。 -
外观模式、代理模式、适配器模式、中介者模式都属于“接口隔离”原则的具体体现
外观模式主要是针对系统外和系统内的接口隔离,解耦系统间的单向对象关联关系
代理模式主要是针对两个对象,由于性能、安全、分布式的原因必须隔离,让A访问B的代理类
适配器模式主要是解决接口不兼容的问题,复用老接口形成新接口
中介者模式主要是解耦系统内各个对象之间的双向关联关系 -
组合模式通过 VS 装饰者模式
组合模式、装饰器模式UML类图相似,都基于递归组合来组织可变数目的对象。
Composite 模式旨在构造类,使多个相关的对象能够以统一的方式处理,而多重对象可以被当作一个对象来处理。Composite 模式重在(结构)表示。
Decorator 模式重在不生成子类即可给对象添加职责,Decorator 模式重在修饰。
6. 行为型模式
行为型模式用于管理对象之间的算法、关系和职责。主要有11种:
- 模板方法模式(Template Method Pattern)
- 策略模式(Strategy Pattern)
- 观察者模式(Observer Pattern)
- 命令模式(Command Pattern)
- 迭代器模式(Iterator Pattern)
- 中介者模式(Mediator Pattern)
- 备忘录模式(Memento Pattern)
- 解释器模式(Interpreter Pattern)
- 状态模式(State Pattern)
- 职责链模式(Chain of Responsibility Pattern)
- 访问者模式(Visitor Pattern)
6.1. 模板方法模式
6.1.1. 简介
- 构造要点
- 有一套固定的逻辑方法,写成抽象基类(虚函数可供扩展)
- 继承抽象基类,子类实现细节(多态)
-
解决问题
用于解决具有一定处理步骤的问题。架构师在获取需求后制定产品的大方向,制定一套逻辑框架,然后将任务分出去,提高工作效率。 -
举例
校园招聘的流程:宣讲会->接受简历->面试->发放offer
公司Company类为抽象基类,里面定义了校园招聘流程的每个流程方法
其中面试和发放offer为虚函数,可供扩展
这时有两个公司Alibaba和Tecent继承Company类实现上面两个虚函数,开启自己公司的校园招聘 -
UML
6.1.2. 使用效果?优缺点?
优点:
- 封装不变部分, 扩展可变部分。模板方法模式通过把不变的行为搬移到超类,可变部分的则可以通过继承来继续扩展。基本方法是由子类实现的,因此子类可以通过扩展的方式增加相应的功能,符合开闭原则。
- 提取公共部分代码, 便于维护。
缺点:
- 每个不同的实现都需要定义一个子类,这会导致类的个数的增加,设计更加抽象。按照我们的设计习惯, 抽象类负责声明最抽象、 最一般的事物属性和方法, 实现类完成具体的事物属性和方法。 但是模板方法模式却颠倒了, 抽象类定义了部分抽象方法, 由子类实现, 子类执行的结果影响了父类的结果, 也就是子类对父类产生了影响, 这在复杂的项目中, 会带来代码阅读的难度, 而且也会让新手产生不适感。
6.1.3. 使用场景?业界知名例子
- 多个子类有公有的方法, 并且逻辑基本相同时。
- 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现。
- 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数约束其行为。
初级程序员在写程序的时候经常会问高手“父类怎么调用子类的方法”。那么父类是否可以调用子类的方法呢?
能,但强烈地、极度地不建议这么做,那该怎么做呢?
- 把子类传递到父类的有参构造中,然后调用。
- 使用反射的方式调用,你使用了反射还有谁不能调用的!
- 父类调用子类的静态方法。
这三种都是父类直接调用子类的方法, 好用不? 好用! 解决问题了吗? 解决了! 项目中允许使用不? 不允许! 我就一直没有搞懂为什么要用父类调用子类的方法。 如果一定要调用子类, 那为什么要继承它呢? 搞不懂。 其实这个问题可以换个角度去理解, 父类建立框架,子类在重写了父类部分的方法后, 再调用从父类继承的方法, 产生不同的结果(而这正是模板方法模式) 。 这是不是也可以理解为父类调用了子类的方法呢? 你修改了子类, 影响了父类行为的结果, 曲线救国的方式实现了父类依赖子类的场景, 模板方法模式就是这种效果。模板方法在一些开源框架中应用非常多, 它提供了一个抽象类, 然后开源框架写了一堆子类。 如果你需要扩展功能, 可以继承这个抽象类, 然后覆写protected方法, 再然后就是调用一个类似execute方法, 就完成你的扩展开发, 非常容易扩展的一种模式。
6.2. 策略模式
6.2.1. 简介
- 构造要点
- 各个步骤组装成算法 ,形成 策略抽象基类
- 当不同的策略出现时可继承抽象基类,创建策略子类
- 将算法的逻辑封装到一个类
- 具体场景时选用不同的策略(组合对象,运行时可提供多态调用)
-
解决问题
如果出现“if-else”“switch-case”等条件判断的情况,并且每个情况(算法)不变时,可考虑策略模式。 -
举例
旅行时如何选择出行方式?自行车,小轿车,火车
策略抽象基类IStrategy有虚函数Travel()
BikeStrategy、CarStrategy、TrainStrategy继承抽象基类IStrategy,实现Travel()
Context类封装逻辑可选择出行方式,含策略抽象基类IStrategy,真正实例化时可实例化子类
这时你可以通过Context实例化对象,选择出行方式 -
UML
6.2.2. 使用效果?优缺点?
优点:
- 算法可以自由切换。这是策略模式本身定义的, 只要实现抽象策略, 它就成为策略家族的一个成员, 通过封装角色对其进行封装, 保证对外提供“可自由切换”的策略。
- 避免使用多重条件判断。多重条件语句不易维护, 而且出错的概率大大增强。 使用策略模式后, 可以由其他模块决定采用何种策略, 策略家族对外提供的访问接口就是封装类, 简化了操作, 同时避免了条件语句判断。
- 扩展性良好,易于修改和扩展那些被复用的实现。 在现有的系统中增加一个策略太容易, 只要实现接口就可以了, 其他都不用修改, 类似于一个可反复拆卸的插件, 这大大地符合了开闭原则。
缺点:
- 策略类数量增多。每一个策略都是一个类, 复用的可能性很小, 类数量增多,易引起类膨胀的问题。
- 所有的策略类都需要对外暴露上层模块必须知道有哪些策略, 然后才能决定使用哪一个策略, 这与迪米特法则是相违背的。如果系统中的一个策略家族的具体策略数量超过4个, 我们可以使用其他模式来修正这个缺陷,如工厂方法模式、 代理模式或享元模式, 解决策略类膨胀和对外暴露。
6.2.3. 使用场景?业界知名例子
- 多个类只有在算法或行为上稍有不同的场景。
- 算法需要自由切换的场景。
例如, 算法的选择是由使用者决定的, 或者算法始终在进化, 特别是一些站在技术前沿的行业, 连业务专家都无法给你保证这样的系统规则能够存在多长时间, 在这种情况下策略模式是你最好的助手。 - 需要屏蔽算法规则的场景。
策略模式是一个非常简单的模式。 它在项目中使用得非常多, 但它单独使用的地方就比较少了。
6.3. 观察者模式
6.3.1. 简介
- 构造要点
- 先构造观察者,有抽象基类,具体观察者类可以继承抽象基类
- 管理观察者的类Subject,主要包含三个方法:注册观察者,注销观察者,通知观察者
- 管理观察者的类Subject中用列表list<IObserver *>管理观察者
-
解决问题
应对“一”对“多”的变化,当“一”变化时“多”也能随着改变自动刷新。 -
举例
想要随时知道土豆的价格,需要派遣2位观察者Jack Ma、Pony随时观察土豆价格。
先注册观察者,观察者发现土豆价格变化,自动通知刷新土豆价格。 -
UML
6.3.2. 使用效果?优缺点?
优点:
- 观察者和被观察者Subject之间是抽象耦合,即抽象基类是稳定的,同时也是可扩展的。
- 建立一套触发机制,形成了一个触发链。观察者模式可以完美地实现这种链条形式。
缺点:
- 观察者模式需要考虑一下开发效率和运行效率问题,一个被观察者,多个观察者,开发和调试就会比较复杂。此处要考虑是否采用异步的方式。多级触发时的效率更是让人担忧。
6.3.3. 使用场景?业界知名例子
- 关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合”关系。
- 事件多级触发场景。
- 跨系统的消息交换场景,如消息队列的处理机制。
注意事项:
- 广播链的问题
如果你做过数据库的触发器,你就应该知道有一个触发器链的问题,比如表A上写了一个触发器,内容是一个字段更新后更新表B的一条数据,而表B上也有个触发器,要更新表C,表C也有触发器……完蛋了,这个数据库基本上就毁掉了!我们的观察者模式也是一样的问题, 一个观察者可以有双重身份, 既是观察者,也是被观察者,这没什么问题呀但是链一旦建立,这个逻辑就比较复杂,可维护性非常差,根据经验建议,在一个观察者模式中最多出现一个对象既是观察者也是被观察者, 也就是说消息最多转发一次(传递两次),这还是比较好控制的。
注意!!!它和职责链模式的最大区别就是观察者广播链在传播的过程中消息是随时更改的,它是由相邻的两个节点协商的消息结构;而职责链模式在消息传递过程中基本上保持消息不可变,如果要改变,也只是在原有的消息上进行修正。 - 异步处理问题
被观察者发生动作了,观察者要做出回应,如果观察者比较多,而且处理时间比较长怎么办?那就用异步,异步处理就要考虑线程安全和队列的问题,这个大家有时间看看Message Queue,就会有更深的了解。
具体例子:
- MVC框架
MVC框架中Model类担任目标角色,而View是观察者的基类。中介者模式中的同事类Colleage可以用观察者模式与中介者Mediator通讯。 - 文件系统
比如,在一个目录下新建立一个文件,这个动作会同时通知目录管理器增加该目录,并通知磁盘管理器减少1KB的空间,也就说“文件”是一个被观察者,“目录管理器”和“磁盘管理器”则是观察者。 - 猫鼠游戏
夜里猫叫一声,家里的老鼠撒腿就跑,同时也吵醒了熟睡的主人,这个场景中,“猫”就是被观察者,老鼠和人则是观察者。 - ATM取钱
比如你到ATM机器上取钱,多次输错密码,卡就会被ATM吞掉,吞卡动作发生的时候,会触发哪些事件呢?第一,摄像头连续快拍,第二,通知监控系统,吞卡发生;第三,初始化ATM机屏幕,返回最初状态。一般前两个动作都是通过观察者模式来完成的,后一个动作是异常来完成。 - 广播收音机
电台在广播,你可以打开一个收音机,或者两个收音机来收听,电台就是被观察者,收音机就是观察者。
6.4. 中介者模式
6.4.1. 简介
- 构造要点
- 数据绑定,通讯协议
- 将多个对象之间集中管理,变"多个对象互相关联"为"一个中介者和多个对象关联“
- 解耦系统内各个对象之间的双向关联关系
-
解决问题
解耦系统内各个对象之间的双向关联关系 -
举例
房东通过中介者发送消息,中介者将消息发送给租客
租客返回消息给中介者,中介者返回消息给房东
-
UML
6.4.2. 使用效果?优缺点?
优点:
- 减少类间的依赖,把原有的一对多的依赖变成了一对一的依赖,同事类只依赖中介者,减少了依赖,当然同时也降低了类间的耦合。
缺点:
- 中介者会膨胀得很大,而且逻辑复杂,原本N个对象直接的相互依赖关系转换为中介者和同事类的依赖关系,同事类越多,中介者的逻辑就越复杂。
6.4.3. 使用场景?业界知名例子
中介者模式适用于多个对象之间紧密耦合的情况,紧密耦合的标准是:在类图中出现了蜘蛛网状结构。在
这种情况下一定要考虑使用中介者模式,这有利于把蜘蛛网梳理为星型结构,使原本复杂混乱的关系变得清晰简单。
中介者模式很少用到接口或者抽象类,这与依赖倒置原则是冲突的,这是什么原因呢?
首先,既然是同事类而不是兄弟类(有相同的血缘),那就说明这些类之间是协作关系,完成不同的任务,处理不同的业务,所以不能在抽象类或接口中严格定义同事类必须具有的方法。
大家可以在如下的情况下尝试使用中介者模式:
- N个对象之间产生了相互的依赖关系(N>2)。
- 多个对象有依赖关系,但是依赖的行为尚不确定或者有发生改变的可能,在这种情况下一般建议采用中介者模式,降低变更引起的风险扩散。
- 产品开发。 一个明显的例子就是MVC框架,中介者模式主要是应用于C(Controller),把中介者模式应用到产品中,可以提升产品的性能和扩展性,但是对于项目开发就未必,因为项目是以交付投产为目标,而产品则是以稳定、高效、扩展为宗旨。
- 机场调度中心
大家在每个机场都会看到有一个“××机场调度中心”,它就是具体的中介者,用来调度每一架要降落和起飞的飞机。比如,某架飞机(同事类)飞到机场上空了,就询问调度中心(中介者)“我是否可以降落”以及“降落到哪个跑道”,调度中心(中介者)查看其他飞机(同事类)情况,然后通知飞机降落。如果没有机场调度中心,飞机飞到机场了,飞行员要先看看有没有飞机和自己一起降落的,有没有空跑道,停机位是否具备等情况,这种局面是难以想象的! - MVC框架
大家都应该使用过Struts,MVC框架。其中的C(Controller)就是一个中介者,叫做前端控制器(Front Controller),它的作用就是把M(Model,业务逻辑)和V(View,视图)隔离开,协调M和V协同工作,把M运行的结果和V代表的视图融合成一个前端可以展示的页面,减少M和V的依赖关系。MVC框架已经成为一个非常流行、成熟的开发框架。 - 媒体网关
媒体网关也是一个典型的中介者模式,比如使用MSN时,张三发消息给李四,其过程应该是这样的:张三发送消息,MSN服务器(中介者)接收到消息,查找李四,把消息发送到李四,同时通知张三消息已经发送。在这里,MSN服务器就是一个中转站,负责协调两个客户端的信息交流,与此相反的就是IPMsg(也叫飞鸽),它没有使用中介者,而直接使用了UDP广播的方式,每个客户端既是客户端也是服务器端。 - 中介服务
现在中介服务非常多,比如租房中介、出国中介。
6.5. 状态模式
6.5.1. 简介
- 构造要点
- 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。
- 行为随着状态改变,在行为受状态约束的情况下可以使用状态模式, 但使用时对象的状态最好不
要超过5个 - 可以用状态模式实现的,一般也可以用策略模式实现。
-
解决问题
行为随状态改变而改变的场景,条件、分支判断语句的替代者 -
举例
电梯的动作:开门、关门、运行、停止
可以通过设置电梯的状态,随之改变电梯的行为 -
UML
6.5.2. 使用效果?优缺点?
优点:
- 结构清晰,避免了过多的switch…case或者if…else语句的使用, 避免了程序的复杂性,提高系统的可维护性。
- 很好地体现了开闭原则和单一职责原则,每个状态都是一个子类,你要增加状态就要增加子类,你要修改状态,你只修改一个子类就可以了。
- 封装性非常好。这也是状态模式的基本要求,状态变换放置到类的内部来实现,外部的调用不用知道类内部如何实现状态和行为的变换。
缺点:
- 子类会太多,易引起类膨胀问题。如果一个事物有很多个状态也不稀奇,如果完全使用状态模式就会有太多的子类,不好管理,这个需要在项目中自己衡量。其实有很多方式可以解决这个状态问题,如在数据库中建立一个状态表,然后根据状态执行相应的操作,这个也不复杂。
6.5.3. 使用场景?业界知名例子
状态间的自由切换,那会有很多种呀!比如上面那个电梯的例子,我要一个正常的电梯运行逻辑,规则是开门->关门->运行->停止;还要一个紧急状态(如火灾)下的运行逻辑,关门->停止,紧急状态时,电梯当然不能用了;再要一个维修状态下的运行逻辑。需要我们把已经有的几种状态按照一定的顺序再重新组装一下,使用建造者模式!建造者模式+状态模式会起到非常好的封装作用。
工作流开发, 应该有个状态机管理,如一个Activity(节点)有初始化状态(Initialized State)、挂起状态(Suspended State)、完成状态(Completed State)等,流程实例也有这么多状态, 那这些状态怎么管理呢?通过状态机(State Machine)来管理。
使用场景:
- 行为随状态改变而改变的场景
这也是状态模式的根本出发点,例如权限设计,人员的状态不同即使执行相同的行为结果也会不同,在这种情况下需要考虑使用状态模式。 - 条件、分支判断语句的替代者
在程序中大量使用switch语句或者if判断语句会导致程序结构不清晰,逻辑混乱,使用状态模式可以很好地避免这一问题, 它通过扩展子类实现了条件的判断处理。
6.6. 备忘录模式
6.6.1. 简介
- 构造要点
- 在不破坏封装性的前提下,铺捉一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到该对象原先保存的状态
- Memento备忘录对象在需要的时候恢复Originator发起者对象的状态
- Memento核心是信息隐藏,Originator发起者对象需要向外界隐藏信息,保持其封装性,又需要将状态保持到外界
-
举例
“月光宝盒”, 可以让我们回到需要的年代,返回上一个状态,即“撤销”的操作 -
UML
6.6.2. 使用效果?优缺点?
优点:
- 封装性较高
缺点:
- 使用备忘录代价较高,频繁地创建备忘录和恢复Originator状态,可能会导致非常大的开销。备忘录有可能有多个状态,管理器管理变复杂,也会需要大的存储开销。
6.6.3. 使用场景?业界知名例子
使用过程需要注意:
- 备忘录的生命期
备忘录创建出来就要在“最近”的代码中使用,要主动管理它的生命周期,建立就要使用,不使用就要立刻删除其引用,等待垃圾回收器对它的回收处理。 - 备忘录的性能
不要在频繁建立备份的场景中使用备忘录模式(比如一个for循环中),原因有二:一是控制不了备忘录建立的对象数量;二是大对象的建立是要消耗资源的,系统的性能需要考虑。
- 需要保存和恢复数据的相关状态场景。
- 提供一个可回滚(rollback)的操作;比如Word中的CTRL+Z组合键,IE浏览器中的后退按钮,文件管理器上的backspace键等。
- 需要监控的副本场景中。例如要监控一个对象的属性,但是监控又不应该作为系统的主业务来调用,它只是边缘应用,即使出现监控不准、错误报警也影响不大,因此一般的做法是备份一个主线程中的对象,然后由分析程序来分析。
- 数据库连接的事务管理就是用的备忘录模式,JDBC驱动实现事务过程中使用备忘录模式。
6.7. 迭代器模式
6.7.1. 简介
- 构造要点
- 提供一种方法顺序访问问一个聚合对象中的各个元素,而又不暴露该对象的内部结构表示(稳定)
- 其他语言还是使用面向对象的方法实现迭代器(运行时多态),c++使用泛型编程模板方法实现迭代器(编译时多态效率更高),而不是运行时多态(c++抛弃)
- 游标Cursor
-
举例
一般库和框架都实现了迭代器模式,可以与组合模式使用 -
UML
-
分类
根据STL中的分类,iterator包括:
输入迭代器(Input Iterator):通过对输入迭代器解除引用,它将引用对象,而对象可能位于集合中。最严格的输入迭代只能以只读方式访问对象。例如:istream。
输出迭代器(Output Iterator):该类迭代器和Input Iterator极其相似,也只能单步向前迭代元素,不同的是该类迭代器对元素只有写的权力。例如:ostream, inserter。
以上两种基本迭代器可进一步分为三类:
++、 --&&++、-n&+n
前向迭代器(Forward Iterator):该类迭代器可以在一个正确的区间中进行读写操作,它拥有Input Iterator的所有特性,和Output Iterator的部分特性,以及单步向前迭代元素的能力。
双向迭代器(Bidirectional Iterator):该类迭代器是在Forward Iterator的基础上提供了单步向后迭代元素的能力。例如:list, set, multiset, map, multimap。
随机迭代器(Random Access Iterator):该类迭代器能完成上面所有迭代器的工作,它自己独有的特性就是可以像指针那样进行算术计算,而不是仅仅只有单步向前或向后迭代。例如:vector, deque, string, array。
6.7.2. 使用效果?优缺点?
优点:
- 在同一个聚合上支持以不同方式遍历,也可有多个遍历。
- 迭代器简化了聚合的接口。
缺点:
1.
6.7.3. 使用场景?业界知名例子
一般库和框架都实现了迭代器模式,可以与组合模式使用
迭代器模式已经很少使用
6.8. 职责链模式
6.8.1. 简介
- 构造要点
- 使多个对象都有机会处理请求, 从而避免了请求的发送者和接受者之间的耦合关系。 将这些对象连成一条链, 并沿着这条链传递该请求, 直到有对象处理它为止。
- 如果请求传递到职责链的末尾仍得不到处理,应该有一个合理的缺省机制
- 一个请求可能有多个接受者,但最后真正的接受者只有一个
-
解决问题
解决提交帮助请求的对象(如按钮)并不明确知道谁是最终提供帮助的对象。职责链将提交帮助请求的对象和提供帮助的对象解耦。 -
举例
职责链:经理->总监->ceo
员工请假需请求上级批准
Manager 可以批准 1 天假
Director 可以批准 2 天假
CEO 可以批准 7 天假 -
UML
6.8.2. 使用效果?优缺点?
优点:
- 将请求和处理分开。 请求者可以不用知道是谁处理的,处理者可以不用知道请求者的全貌。两者解耦,提高系统的灵活性。
缺点:
- 性能问题,每个请求都是从链头遍历到链尾,特别
是在链比较长的时候。 - 调试不很方便,特别是链比较长,环节比较多的时候,由于采用了类似递归的方式,调试的时候逻辑可能比较复杂。
6.8.3. 使用场景?业界知名例子
链中节点数量需要控制, 避免出现超长链的情况,一般的做法是在Handler中设置一个最大节点数量,然后判断是否已经是超过其阈值,超过则不允许该链建立,避免无意识地破坏系统性能。
许多类库使用职责链处理用户事件:当用户点击鼠标或按键盘时,一个事件产生并按链传播。
6.9. 命令模式
6.9.1. 简介
- 构造要点
- 将一个请求封装成一个对象, 从而让你使用不同的请求把客户端参数化, 对请求排队或者记录请求日志, 可以提供命令的撤销和恢复功能
- 有时必须向对象提交请求,但并不知道关于被请求的操作和请求的接受者的任何信息
- 举例
用户界面工具箱包括按钮和菜单对象,他们执行请求响应用户输入。但工具箱不能显示地在按钮或菜单中实现该请求,因为只有使用工具箱的应用知道该由哪个对象做哪个操作,而工具箱的设计者无法知道请求的接受者或执行的操作。
当有多个命令时,可以用组合模式封装成一个复合命令,如宏命令。
- UML
6.9.2. 使用效果?优缺点?
优点:
- 类间解耦
调用者角色与接收者角色之间没有任何依赖关系, 调用者实现功能时只需调用Command抽象类的execute方法就可以, 不需要了解到底是哪个接收者执行。 - 可扩展性
Command的子类可以非常容易地扩展, 而调用者Invoker和高层次的模块Client不产生严重的代码耦合。 - 命令模式结合其他模式会更优秀
命令模式可以结合责任链模式, 实现命令族解析任务; 结合模板方法模式, 则可以减少Command子类的膨胀问题。
缺点:
- 看Command的子类:如果有N个命令,这个类膨胀得非常大,需在项目中慎重考虑使用。
6.9.3. 使用场景?业界知名例子
只要你认为是命令的地方就可以采用命令模式, 例如, 在GUI开发中, 一个按钮的点击是一个命令, 可以采用命令模式; 模拟DOS命令的时候, 当然也要采用命令模式; 触发-反馈机制的处理等。
6.10. 访问器模式
6.10.1. 简介
- 构造要点
- 不改变类层次结构,在运行时透明地为类层次结构上的各个类动态添加新的操作
- 前提条件要求elemment抽象基类和其子类都是稳定的,这样visitor才可以访问每一个具体子类,动态添加新的操作
- elemment类层次结构必须稳定,其中的操作可以面临频繁变动
-
举例
西安有景点:钟楼、兵马俑
到景点的人:游客、清洁工
object_structure.h中的City对象提供访问方法绑定景点 -
UML
6.10.2. 使用效果?优缺点?
优点:
- 符合单一职责原则。具体元素角色也就是Element抽象类的两个子类负责数据的加载,而Visitor类则负责报表的展现,两个不同的职责非常明确地分离开来,各自演绎变化。
- 优秀的扩展性,由于职责分开, 继续增加对数据的操作(访问者)是非常快捷的。
缺点:
- Visitor稳定的前提要求elemment抽象基类和其子类都是稳定的。具体元素对访问者公布细节,访问者要访问一个类就必然要求这个类公布一些方法和数据, 也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的。
- 具体元素变更比较困难,具体元素角色的增加、 删除、 修改都是比较困难的。
- 违背了依赖倒置转原则,访问者依赖的是具体元素,而不是抽象元素,这破坏了依赖倒置原则, 特别是在面向对象的编程中,抛弃了对接口的依赖,而直接依赖实现类,扩展比较难。
6.10.3. 使用场景?业界知名例子
- 一个对象结构包含很多类对象, 它们有不同的接口, 而你想对这些对象实施一些依赖
于其具体类的操作, 也就说是用迭代器模式已经不能胜任的情景。 - 需要对一个对象结构中的对象进行很多不同并且不相关的操作, 而你想避免让这些操
作“污染”这些对象的类。
总结一下,在这种地方你一定要考虑使用访问者模式:业务规则要求遍历多个不同的对象。这本身也是访问者模式出发点,迭代器模式只能访问同类或同接口的数据(当然了,如果你使用instanceof,那么能访问所有的数据,这没有争论),而访问者模式是对迭代器模式的扩充,可以遍历不同的对象,然后执行不同的操作,也就是针对访问的对象不同,执行不同的操作。访问者模式还有一个用途,就是充当拦截器(Interceptor)角色。
访问者模式是一种集中规整模式,特别适用于大规模重构的项目,在这一个阶段需求已经非常清晰,原系统的功能点也已经明确,通过访问者模式可以很容易把一些功能进行梳理,达到最终目的——功能集中化,如一个统一的报表运算、 UI展现等,我们还可以与其他模式混编建立一套自己的过滤器或者拦截器。
6.11. 解析器模式
6.11.1. 简介
- 构造要点
- 给定一个语言,定义它的文法的一种表示,并定义一种解释器,这个解释器使用该表示来解释语言中的句子。
- 解析器模式中使用类来表示文法规则,因此可以使用面向对象的方法实现文法的扩展。另外对于终结符我们可以使用享元模式来实现终结符的共享。
- 但只适用于比较简单的文法表示,复杂的还需要求助语法分析生成器等工具。
-
举例
解析器下的四则运算
构建语法规则:变量表达式,符号表达式,加法运算,减法运算 -
UML
6.11.2. 使用效果?优缺点?
优点:
- 解释器是一个简单语法分析工具,它最显著的优点就是扩展性,修改语法规则只要修改相应的非终结符表达式就可以,若扩展语法,则只要增加非终结符类就可以。
缺点:
- 解释器模式会引起类膨胀
每个语法都要产生一个非终结符表达式,语法规则比较复杂时,就可能产生大量的类文件,为维护带来了非常多的麻烦。 - 解释器模式采用递归调用方法
每个非终结符表达式只关心与自己有关的表达式,每个表达式需要知道最终的结果,必须一层一层地剥茧,无论是面向过程的语言还是面向对象的语言,递归都是在必要条件下使用的,它导致调试非常复杂。想想看,如果要排查一个语法错误,我们是不是要一个断点一个断点地调试下去,直到最小的语法单元。 - 效率问题
解释器模式由于使用了大量的循环和递归,效率是一个不容忽视的问题,特别是一用于解析复杂、冗长的语法时,效率是难以忍受的。
6.11.3. 使用场景?业界知名例子
解释器模式在实际的系统开发中使用得非常少,因为它会引起效率、 性能以及维护等问题,一般在大中型的框架型项目能够找到它的身影,如一些数据分析工具、 报表设计工具、科学计算工具等。
若你确实遇到“一种特定类型的问题发生的频率足够高”的情况, 准备使用解释器模式时, 可以考虑一下Expression4J、 MESP(Math Expression String Parser) 、 Jep等开源的解析工具包(这三个开源产品都可以通过百度、 Google搜索到),功能都异常强大, 而且非常容易使用, 效率也还不错, 实现大多数的数学运算完全没有问题, 自己没有必要从头开始编写解释器。
尽量不要在重要的模块中使用解释器模式, 否则维护会是一个很大的问题。 在项目中可以使用shell、 JRuby、 Groovy等脚本语言来代替解释器模式, 弥补编译型语言的不足。
另外,在学习编译器原理课程时,将语言从汇编语言->可执行文件时也会用到解析器模式,使用场景还有:
- 重复发生的问题可以使用解释器模式
例如,多个应用服务器,每天产生大量的日志,需要对日志文件进行分析处理,由于各个服务器的日志格式不同,但是数据要素是相同的,按照解释器的说法就是终结符表达式都是相同的,但是非终结符表达式就需要制定了。在这种情况下,可以通过程序来一劳永逸地解决该问题。 - 一个简单语法需要解释的场景
为什么是简单?看看非终结表达式,文法规则越多,复杂度越高,而且类间还要进行递归调用。排查问题越难。因此,解释器模式一般用来解析比较标准的字符集,例如SQL语法分析,不过该部分逐渐被专用工具所取代。
6.12. 行为型模式对比
-
模板方法模式、策略模式、观察者模式相似
-
状态模式 VS 策略模式
两者最大的差别就是 State 模式中派生类持有指向 Context 对象的依赖,并通过这个依赖调用、 Context 中的方法,但在 Strategy 模式中就没有这种情况。因此可以说一个 State 实例同样是 Strategy 模式的一个实例,反之却不成立。
7. 参考资料
- CSDN博客 c++设计模式 https://blog.csdn.net/weixin_35602748/article/details/78667543
- GitHub源码地址 Waleon/DesignPatterns https://github.com/Waleon/DesignPatterns
- 《设计模式之禅》 秦小波著 ISBN: 9787111437871
- c++设计模式 视频教程 李建忠讲师 https://www.bilibili.com/video/av52251106
- CSDN博客 设计模式思维导图 https://blog.csdn.net/weixin_35602748/article/details/78667543
- 《GoF23种设计模式模式解析附C++实现源码》
起效率、 性能以及维护等问题,一般在大中型的框架型项目能够找到它的身影,如一些数据分析工具、 报表设计工具、科学计算工具等。
若你确实遇到“一种特定类型的问题发生的频率足够高”的情况, 准备使用解释器模式时, 可以考虑一下Expression4J、 MESP(Math Expression String Parser) 、 Jep等开源的解析工具包(这三个开源产品都可以通过百度、 Google搜索到),功能都异常强大, 而且非常容易使用, 效率也还不错, 实现大多数的数学运算完全没有问题, 自己没有必要从头开始编写解释器。
尽量不要在重要的模块中使用解释器模式, 否则维护会是一个很大的问题。 在项目中可以使用shell、 JRuby、 Groovy等脚本语言来代替解释器模式, 弥补编译型语言的不足。
另外,在学习编译器原理课程时,将语言从汇编语言->可执行文件时也会用到解析器模式,使用场景还有:
- 重复发生的问题可以使用解释器模式
例如,多个应用服务器,每天产生大量的日志,需要对日志文件进行分析处理,由于各个服务器的日志格式不同,但是数据要素是相同的,按照解释器的说法就是终结符表达式都是相同的,但是非终结符表达式就需要制定了。在这种情况下,可以通过程序来一劳永逸地解决该问题。 - 一个简单语法需要解释的场景
为什么是简单?看看非终结表达式,文法规则越多,复杂度越高,而且类间还要进行递归调用。排查问题越难。因此,解释器模式一般用来解析比较标准的字符集,例如SQL语法分析,不过该部分逐渐被专用工具所取代。
6.12. 行为型模式对比
-
模板方法模式、策略模式、观察者模式相似
-
状态模式 VS 策略模式
两者最大的差别就是 State 模式中派生类持有指向 Context 对象的依赖,并通过这个依赖调用、 Context 中的方法,但在 Strategy 模式中就没有这种情况。因此可以说一个 State 实例同样是 Strategy 模式的一个实例,反之却不成立。
7. 参考资料
- CSDN博客 c++设计模式 https://blog.csdn.net/weixin_35602748/article/details/78667543
- GitHub源码地址 Waleon/DesignPatterns https://github.com/Waleon/DesignPatterns
- 《设计模式之禅》 秦小波著 ISBN: 9787111437871
- c++设计模式 视频教程 李建忠讲师 https://www.bilibili.com/video/av52251106
- CSDN博客 设计模式思维导图 https://blog.csdn.net/weixin_35602748/article/details/78667543
- 《GoF23种设计模式模式解析附C++实现源码》
- 《设计模式 可复用面向对象软件的基础》