设计模式介绍
设计模式最关键的作用是为了可复用,减少开发工作量。
模式就是针对现实世界重复出现的问题,给出核心解决方案,忽略掉一些不重要的细节。
分解和抽象是2种解决现实问题的通用方法。
底层思维是向下的,多数是理解计算机的,抽象是向上思维,多数是理解现实世界的。
在现实工程开发中,要寻找需求频繁变化点,应用对应的设计模式,从而提高代码复用性,降低开发成本,测试成本。
设计模式的应用不宜先入为主,最好是Refactoring to Patterns 也就是针对现实的痛点(变化点),重构到设计模式
重构的关键技巧
- 静态—-动态
- 早绑定—-晚绑定
- 继承—–组合
- 编译时依赖—-运行时依赖
- 紧耦合—-松耦合
面向对象设计原则
重新认识面向对象
-
理解隔离变化
宏观层面来看 面向对象的构建方式能够适应软件的变化,能将变化带来的影响减为最小
-
各司其职
微观层面来说,面向对象更强调独立的各个类
需求变化带来的新增类 不应该改变原来类的功能,也就是每个类各司其职
-
对象到底是什么
从语言的角度来说,对象封装了代码和数据
从规格层面来说,对象是一些列可以使用的公共接口
从概念层面来说,对象是拥有某种责任的抽象
1 依赖倒置原则(DIP)
-
高层模块(往往是稳定的)不应该依赖底层模块(往往是不停的变化的),二者都应该依赖于抽象。
-
抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)
2 开放封闭原则(OCP)
- 对于扩展是开放的,对于修改是封闭的
- 类模块应该是是可以扩展的,但是不能修改。
在设计之初应该考虑其扩展性,不应该因为新增一个功能,反而要更改原先的功能,应该让新增功能为扩展,不应该修改原先的代码。
3 单一职责原则(SRP)
- 一个类应该仅有一个引起其变化的原因
- 其变化的原因 往往就是其该承担的责任。
4 Liskov替换原则(LSP)
- 子类必须能够替换它的父类(IS-A)
- 继承表达类型抽象
5 接口隔离原则(ISP)
- 不应该强迫客户程序依赖他们不使用的方法
- 接口应该小而完备
6 优先使用对象组合 而不是类继承
- 类继承通常是白箱复用 对象组合通常为黑箱复用
- 继承在某种程度上破坏了封装性,子类父类耦合度较高
- 对象组合只要求被组合的对象有良好的接口定义,耦合度低
7 封装变化点
-
使用封装来创建对象之间的分界层,让设计者可以在分界层一侧修改,不影响另外一侧,实现分层间的松耦合。
-
封装其实是封装变化点。
8 针对接口编程,不要针对实现编程
- 不将变量声明为特定的类,而是声明为接口。
- 客户程序不需要知道对象具体类型,只需要知道其开放的接口
- 减少各部分依赖关系,实现高内聚 松耦合
- 产业强盛 最好是接口相当标准化。
模版方法Template method—组件协作
需求背景:
需要实现多个app,都需要5个操作步骤(5个步骤操作顺序也都是固定的),有3个已经有框架或者library库实现了,实现这个app
传统结构化思想实现
#include<iostream>
using namespace std;
class Library{//这里往往是库函数或者别人的实现 实现了3个方法
public:
bool Step1(){
//...
cout<<"Library::Step1()"<<endl;
return true;
}
void Step3(){
cout<<"Library::Step3()"<<endl;
//...
}
void Step5(){
cout<<"Library::Step5()"<<endl;
//...
}
};
class App{//应用程序 实现Step2 Step4方法 这2个方法经常变化 不同的应用不一样的实现
public:
void Step2(){
cout<<"App::Step2()"<<endl;
//...
}
void Step4(){
cout<<"App::Step4()"<<endl;
}
};
int main(){//如果要实现另外一个程序 则需要重新再写一个main 重新写一个App类 实现Step2和Step4方法
App app;
Library lib;
//以下为程序实现逻辑 也就是算法框架结构
if(lib.Step1()){
app.Step2();
}
lib.Step3();
for (int i=1;i<4;++i) {
app.Step4();
}
lib.Step5();
}
//如果34--41的算法步骤整体是稳定的 只有Step2和Step4是经常变化的 可以考虑用TempalteMethod设计模式来实现
动机
- 某项任务 常常有稳定的整体操作结构,但是各个子步骤需要经常变化,或者由于固有的原因(框架和应用的关系),无法和任务的整体结构同时实现
- 这就要思考如何确定稳定的操作结构的前提下,来灵活应对各个子步骤的变化或者晚期实现需求
模式定义
- 定义一个操作中的算法骨架(往往是稳定的),将一些步骤(往往是变化了,前期确定不下来)延迟到子类中实现,Template Method使得子类可以不改变(复用)一个算法的结构即可重定义(Override重写)该算法的某些特定子步骤。
要点总结
- tempalte method是一种非常基础性的设计模式 在面向对象系统中有大量的应用,机制非常简洁(虚函数的多态),为很多应用程序的框架提供了灵活的扩展点(继承+虚函数多态度),是代码复用的基本实现结构
- 不要调用我,让我来调用你
- 具体实现方面,被template method调用的虚方法可以有实现也可以没有实现,一般将被调用的方法设置为protectrd。因为这些被template method调用的虚方法离开了主流程往往没有意义,也不应该设置为public暴漏给外部。
几张图解
模版方法代码实现
#include<iostream>
using namespace std;
class Library{
public:
void run(){//template method
//这里的run方法也就是所说的相对稳定的整体处理结构 Step2和Step4会根据不同的应用实现为不同的逻辑,所以声明为纯虚函数
if(Step1()){
Step2();
}
Step3();
for (int i=1;i<4;++i) {
Step4();
}
Step5();
}
virtual ~Library(){}
protected:
bool Step1(){
//...
cout<<"Library::Step1()"<<endl;
return true;
}
void Step3(){
cout<<"Library::Step3()"<<endl;
//...
}
void Step5(){
cout<<"Library::Step5()"<<endl;
//...
}
virtual void Step2()=0;//这里声明为纯虚函数 让子类去实现
virtual void Step4()=0;
};
class App :public Library{//继承框架类 实现容易变化的2个函数
protected:
void Step2(){
cout<<"App::Step2()"<<endl;
//...
}
void Step4(){
cout<<"App::Step4()"<<endl;
}
};
int main(){
Library* pLib=new App();
pLib->run();
delete pLib;
return 0;
}
策略模式 strategy—组件协作
需求背景
需要实现一个类,计算各个国家的税,每个国家的计税算法不一样,后期可能修改或者增加一些国家的计税算法
传统实现
//传统解决方案 traditional solution
enum TaxBase {
CN_Tax,
US_Tax,
DE_Tax,
FR_Tax //如果要增加支持法国税法就要修改
};
class SalesOrder{
TaxBase tax;
public:
double CalculateTax(){
//传统解决方案 直接ifelse 分而治之 需要经常修改这个函数 很可能出现引入新的需求 导致原先的实现方案引入bug
if (tax == CN_Tax){
//CN***********
}
else if (tax == US_Tax){
//US***********
}
else if (tax == DE_Tax){
//DE***********
}
else if (tax == FR_Tax){
//如果要增加支持法国税法就要修改 这个地方违背了开闭原则(用扩展的方式支持修改 而不是直接修改)
//经验告诉我们,直接修改的方式 也比较容易引入bug
}
}
};
动机
- 如果现实软件开发过程中,某一个对象使用了多种算法,而且这些算法可能经常改变,或者经常需要新增类似算法(比如计算不同国家的税),这个时候如果将这些算法全部编码到对象中,这个对象就会变的非常复杂,容易出错,策略模式可以解决问题
- 更直观点 如果使用了大量if else或者switch case,而且不稳定,经常变化,就可以考虑用strategy模式改造
- 要考虑使用多态机制在运行时动态的使用相应的算法解决问题
模式的定义
- 定义一系列算法(动机章节提到的经常变化的算法),把它们一个个封装起来(不同的子类),并且使它们可互相替换(变化)–(使用多态运行时决定使用哪种算法)。该模式使得算法可独立于使用它的客户程序(稳定)而变化(扩展,子类化)。——《设计模式》GoF
要点总结
-
Strategy及其子类为组件提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法之间进行切换(多态)。
-
Strategy模式提供了用条件判断语句以外的另一种选择,消除条件判断语句,就是在解耦合。含有许多条件判断语句的代码通常都需 要Strategy模式。
-
如果Strategy对象没有实例变量,那么各个上下文可以共享同一个 Strategy对象,从而节省对象开销
图解
策略模式代码实现
//策略模式
class Context{};
class TaxStrategy{//策略类基类
public:
virtual double Calculate(const Context& context)=0;//纯虚函数 不同的算法抽象 运行时调用相应的算法
virtual ~TaxStrategy(){}
};
class StrategyFactory{//工厂类 根据现实情况生成不同的子类,解决想要的问题
public:
TaxStrategy* NewStrategy(){ return nullptr;}
};
class CNTax : public TaxStrategy{//子类 继承策略基类 实现虚方法 用于多态
public:
virtual double Calculate(const Context& context){
//***********
}
};
class USTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
class DETax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//***********
}
};
//这里如果新增一个法国的,就是扩展的方式支持
class FRTax : public TaxStrategy{
public:
virtual double Calculate(const Context& context){
//.........
}
};
class SalesOrder{
private:
TaxStrategy* strategy;
public:
SalesOrder(StrategyFactory* strategyFactory){
this->strategy = strategyFactory->NewStrategy();
}
~SalesOrder(){
delete this->strategy;
}
double CalculateTax(){
//...
Context context;
double val =
strategy->Calculate(context); //多态调用
//...
}
};
观察者模式Observer—组件协作
需求背景
需要实现一个文件分割业务,同时需要显示进度条,也就是文件分割时候达到一定状态需要通知某个类,后续很有可能还有通知多个类
传统实现
//traditional solution传统解决方案
//分割大文件,显示或者打印处理进度
#include<string>
#include<iostream>
#include "common.h"
using namespace std;
class FileSplitter//该类是被依赖的对象,状态发生变化需要通知ProgressBar
{
string m_filePath;
int m_fileNumber;
ProgressBar* m_progressBar;//该类负责收到通知更新进度条
//如果要新增其它观察者,这里需要新增具体的实现类指针 ProgressBar1* 比较麻烦,这个业务类也要频繁改变
public:
FileSplitter(const string& filePath, int fileNumber, ProgressBar* progressBar) :
m_filePath(filePath),
m_fileNumber(fileNumber),
m_progressBar(progressBar){
}
void split(){
//1.读取大文件
//2.分批次向小文件中写入
for (int i = 0; i < m_fileNumber; i++){
//...
float progressValue = m_fileNumber;
progressValue = (i + 1) / progressValue;
m_progressBar->setValue(progressValue);
//从编译层面如果ProgressBar发生变化 也就是观察者发生变化,FileSplitter类也需要变化 重新编译
//从业务发展来看,如果需要新增观察者也是比较麻烦的
//这个违背了依赖倒置原则,高层模块不应该依赖此层模块变化,FileSplitter不应该依赖ProgressBar
//个人觉得也违背了开闭原则,要用扩展的方式支持新增,不应该直接修改
}
}
};
//traditional solution传统解决方案
//分割大文件,显示或者打印处理进度
#include "common.h"
class MainForm : public Form
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar;
public:
void Button1_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
FileSplitter splitter(filePath, number, progressBar);
//从业务发展来看,如果需要新增观察者也是比较麻烦的 需要修改FileSplitter 构造方法,传入新增的观察者类指针
//也需要修改FileSplitter通知逻辑 增加对新增观察者的处理
splitter.split();
}
};
动机
-
我们要为某些对象之间建立"通知依赖关系",也就是一个对象状态发生变更,需要自动通知其它依赖与它的对象。如果这种依赖关系过于紧密,将使软件不能很好的抵御变化。
-
可以使用面向对象技术,将这种依赖关系弱化,形成一种较为稳定的依赖关系,从而实现松耦合。
-
大致实现,被依赖的对象需要包含一个集合(链表或者数组都可以),集合保存的是抽象接口基类(观察者的基类指针),当自己状态变化的时候遍历集合,调用抽象接口基类的通知函数(多态实现运行时调用多个观察者的相应处理函数)。
模式的定义
- 定义对象见的一对多(变化也就是观察者)的依赖关系,以便当被依赖对象(Subject)的状态发生变化时,所有依赖与它的对象(观察者)都能得到通知并且自动更新
结构图解
要点总结
- 使用Observer观察者模式可以独立的改变目标和观察者,实现松耦合
- 目标发送通知的时候,无需指定具体的观察者,通知(也可以写态通知信息作为参数)会自动传播
- 观察者自己决定是否需要订阅通知,如果需要请调用Subject的添加观察者API,目标对象对此可以是透明的
- 当听到状态变更需要通知多个对象的场景就往往可以考虑Observer观察者模式了
- Observer模式是基于事件的UI框架中非常常用的模式,也是MVC模式的一个重要组成部分
观察者模式代码实现
#include<string>
#include<iostream>
#include <list>
using namespace std;
class IProgress{//观察者抽象基类
public:
virtual void DoProgress(float value)=0;//观察者需要实现的函数(收到通知后被调用)
virtual ~IProgress(){}
};
class FileSplitter
{
string m_filePath;
int m_fileNumber;
list<IProgress*> m_iprogressList; // 抽象通知机制,支持多个观察者
//这种机制就不再依赖具体的观察者 保证FileSplitter类处理逻辑相对稳定
public:
FileSplitter(const string& filePath, int fileNumber) :
m_filePath(filePath),
m_fileNumber(fileNumber){
}
void split(){
//1.读取大文件
//2.分批次向小文件中写入
for (int i = 0; i < m_fileNumber; i++){
//...
float progressValue = m_fileNumber;
progressValue = (i + 1) / progressValue;
onProgress(progressValue);//发送通知
}
}
void addIProgress(IProgress* iprogress){//增加观察者API
m_iprogressList.push_back(iprogress);
}
void removeIProgress(IProgress* iprogress){//移除观察者API
m_iprogressList.remove(iprogress);
}
protected:
virtual void onProgress(float value){//通知函数
list<IProgress*>::iterator itor=m_iprogressList.begin();
while (itor != m_iprogressList.end() ){
(*itor)->DoProgress(value); //更新进度条 虚函数多态调用多个观察者的处理函数
itor++;
}
}
};
#include "common.h"
#include<string>
using namespace std;
class MainForm : public Form, public IProgress
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar;
public:
void Button1_Click(){
string filePath = txtFilePath->getText();
int number = atoi(txtFileNumber->getText().c_str());
ConsoleNotifier cn;
FileSplitter splitter(filePath, number);
splitter.addIProgress(this); //订阅通知
splitter.addIProgress(&cn); //订阅通知
splitter.split();
splitter.removeIProgress(this);
}
virtual void DoProgress(float value){
progressBar->setValue(value);
}
};
class ConsoleNotifier : public IProgress {
public:
virtual void DoProgress(float value){
cout << ".";
}
};
装饰模式Decorator—单一职责
需求背景
需要实现文件流操作简易系统,支持的操作大概就多些 read seek write,存在普通文件流,网络流,内存流等等
随着系统的发展,需要增加各种流的加密功能
随着系统的发展,需要增加各种流的缓存功能
随着系统的发展,需要增加各种流的加密同时缓存功能
传统实现
//业务操作
class Stream{//流操作基类,定义相关接口虚函数
public:
virtual char Read(int number)=0;
virtual void Seek(int position)=0;
virtual void Write(char data)=0;
virtual ~Stream(){}
};
//文件流操作主体类 应该继承Stream基类
class FileStream: public Stream{
public:
virtual char Read(int number){
//读文件流
}
virtual void Seek(int position){
//定位文件流
}
virtual void Write(char data){
//写文件流
}
};
//网络流操作类 应该继承Stream基类
class NetworkStream :public Stream{
public:
virtual char Read(int number){
//读网络流
}
virtual void Seek(int position){
//定位网络流
}
virtual void Write(char data){
//写网络流
}
};
//内存流操作类 应该继承Stream基类
class MemoryStream :public Stream{
public:
virtual char Read(int number){
//读内存流
}
virtual void Seek(int position){
//定位内存流
}
virtual void Write(char data){
//写内存流
}
};
//扩展加密的功能 传统方案新增多个类 分别继承文件流、网络流、内存流操作类 大量子类生成
class CryptoFileStream :public FileStream{
public:
virtual char Read(int number){
//额外的加密操作... 跟网络流、内存流代码存在大量重复
FileStream::Read(number);//读文件流
}
virtual void Seek(int position){
//额外的加密操作... 跟网络流、内存流代码存在大量重复
FileStream::Seek(position);//定位文件流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
FileStream::Write(data);//写文件流
//额外的加密操作...
}
};
//扩展加密的功能 传统方案新增多个类 分别继承文件流、网络流、内存流操作类
class CryptoNetworkStream :public NetworkStream{
public:
virtual char Read(int number){
//额外的加密操作...
NetworkStream::Read(number);//读网络流
}
virtual void Seek(int position){
//额外的加密操作...
NetworkStream::Seek(position);//定位网络流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
NetworkStream::Write(data);//写网络流
//额外的加密操作...
}
};
//扩展加密的功能 传统方案新增多个类 分别继承文件流、网络流、内存流操作类
class CryptoMemoryStream : public MemoryStream{
public:
virtual char Read(int number){
//额外的加密操作...
MemoryStream::Read(number);//读内存流
}
virtual void Seek(int position){
//额外的加密操作...
MemoryStream::Seek(position);//定位内存流
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
MemoryStream::Write(data);//写内存流
//额外的加密操作...
}
};
//扩展缓存的功能 传统方案新增多个类 分别继承文件流、网络流、内存流操作类
class BufferedFileStream : public FileStream{
//额外的缓存操作... 跟网络流、内存流代码存在大量重复
};
class BufferedNetworkStream : public NetworkStream{
//...
};
class BufferedMemoryStream : public MemoryStream{
//...
}
//扩展加密+缓存组合的的功能 传统方案新增多个类 分别继承文件流、网络流、内存流操作类
class CryptoBufferedFileStream :public FileStream{
public:
virtual char Read(int number){
//代码大量重复 生成大量子类 扩展一个功能非常不灵活,增加维护成本
FileStream::Read(number);//读文件流
}
virtual void Seek(int position){
//额外的加密操作...
//额外的缓冲操作...
FileStream::Seek(position);//定位文件流
//额外的加密操作...
//额外的缓冲操作...
}
virtual void Write(byte data){
//额外的加密操作...
//额外的缓冲操作...
FileStream::Write(data);//写文件流
//额外的加密操作...
//额外的缓冲操作...
}
};
void Process(){
//编译时装配 也就是在编译的时候已经决定了要使用哪种扩展功能对应的子类
CryptoFileStream *fs1 = new CryptoFileStream();
BufferedFileStream *fs2 = new BufferedFileStream();
CryptoBufferedFileStream *fs3 =new CryptoBufferedFileStream();
}
动机
- 滥用继承来扩展对象的功能,由于继承为类型引入的静态特质,导致这种扩展方式变得非常不灵活,而且如果扩展的功能很多或者需要组合,将会产生大量的子类,对项目的维护成本大幅度增加。
模式的定义
- 不使用继承,改用组合的方式动态的给对象增加一些扩展功能。对于新增扩展功能而言,Decorator装饰模式比滥用继承的方式更加灵活(因为可以消除大量的重复代码同时减少子类的数量,减少维护的成本)
结构图解
要点总结
- 采用组合而不是继承的方式,Decorator装饰模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能,避免了大量的重复代码,同时大量的子类产生的问题
- 装饰类在接口上变现为is-a关系,也就是继承业务操作组件基类,但在实现上又是has-a组合的关系,成员拥有业务操作组件基类指针,在运行时动态的传入不同类别的业务操作子类,来实现相应操作,同时扩展功能
- Decorator装饰模式的目的并不是解决“多子类衍生的多继承问题”,重要是在解决“主体类在多个方向上的扩展问题”
简易代码
//业务操作
class Stream{//流操作基类,定义相关接口虚函数
public:
virtual char Read(int number)=0;
virtual void Seek(int position)=0;
virtual void Write(char data)=0;
virtual ~Stream(){}
};
//文件流操作主体类 应该继承Stream基类
class FileStream: public Stream{
public:
virtual char Read(int number){
//读文件流
}
virtual void Seek(int position){
//定位文件流
}
virtual void Write(char data){
//写文件流
}
};
//网络流操作类 应该继承Stream基类
class NetworkStream :public Stream{
public:
virtual char Read(int number){
//读网络流
}
virtual void Seek(int position){
//定位网络流
}
virtual void Write(char data){
//写网络流
}
};
//内存流操作类 应该继承Stream基类
class MemoryStream :public Stream{
public:
virtual char Read(int number){
//读内存流
}
virtual void Seek(int position){
//定位内存流
}
virtual void Write(char data){
//写内存流
}
};
//扩展功能装饰基类
DecoratorStream: public Stream{//继承业务组件操作基类 主要是用于实现相关接口
protected:
Stream* stream;//保存业务组件操作基类指针,运行时动态的传入相应的业务操作主体子类实现相关操作并扩展功能
DecoratorStream(Stream * stm):stream(stm){
}
};
//加密扩展功能
class CryptoStream: public DecoratorStream {//继承装饰基类
public:
CryptoStream(Stream* stm):DecoratorStream(stm){
}
virtual char Read(int number){
//额外的加密操作...
stream->Read(number);//这里会多态的调用不同的流操作(文件、网络、内存) 没有重复代码
}
virtual void Seek(int position){
//额外的加密操作...
stream::Seek(position);//这里会多态的调用不同的流操作(文件、网络、内存)没有重复代码
//额外的加密操作...
}
virtual void Write(byte data){
//额外的加密操作...
stream::Write(data);//这里会多态的调用不同的流操作(文件、网络、内存)没有重复代码
//额外的加密操作...
}
};
//不同流操作增加缓存功能 跟增加加密功能一样 只生成了一个类 同时在运行时多态的调用不同的流操作(文件、网络、内存)没有重复代码
class BufferedStream : public DecoratorStream{
Stream* stream;//...
public:
BufferedStream(Stream* stm):DecoratorStream(stm){
}
//...
};
void Process(){
//运行时装配
FileStream* s1=new FileStream();
//这里s1可以动态的传入文件流、网络流、内存流,来实现不同类别流操作的加密扩展
CryptoStream* s2=new CryptoStream(s1);
//这里s1可以动态的传入文件流、网络流、内存流,来实现不同类别流操作的缓存扩展
BufferedStream* s3=new BufferedStream(s1);
//这里s2可以动态的传入文件流、网络流、内存流,来实现不同类别流操作的加密缓存扩展
BufferedStream* s4=new BufferedStream(s2);
}
桥模式Bridge—单一职责
需求背景
需要实现多平台(pc、mobile、pad,tv)等等,这个是一个纬度,同时也要发布一个经典轻量版本和完美复杂版本(比如发文字消息的时候也播放声音),这又是一个纬度的变化 下面看看经典的实现
传统实现
class Messager{
public:
//下面是三个通用接口方法
virtual void Login(string username, string password)=0;
virtual void SendMessage(string message)=0;
virtual void SendPicture(Image image)=0;
//下面是涉及不同的平台需要实现不同的方法 PlaySound方法只有完美版用到
virtual void PlaySound()=0;
virtual void DrawShape()=0;
virtual void WriteText()=0;
virtual void Connect()=0;
virtual ~Messager(){}
};
//PC平台实现
class PCMessagerBase : public Messager{
public:
virtual void PlaySound(){
//**********
}
virtual void DrawShape(){
//**********
}
virtual void WriteText(){
//**********
}
virtual void Connect(){
//**********
}
};
//手机平台实现
class MobileMessagerBase : public Messager{
public:
virtual void PlaySound(){
//==========
}
virtual void DrawShape(){
//==========
}
virtual void WriteText(){
//==========
}
virtual void Connect(){
//==========
}
};
//业务抽象 经典轻量发布版本 本身是业务抽象纬度,但因为另外一个纬度也要变化(平台纬度) 所以经典发布版本也要有多个平台类的实现
class PCMessagerLite : public PCMessagerBase {
public:
virtual void Login(string username, string password){
PCMessagerBase::Connect();
//........
}
virtual void SendMessage(string message){
PCMessagerBase::WriteText();
//........
}
virtual void SendPicture(Image image){
PCMessagerBase::DrawShape();
//........
}
};
//业务抽象 完美复杂版本 本身是业务抽象纬度,但因为另外一个纬度也要变化(平台纬度) 所以完美复杂版本也要有多个平台类的实现
class PCMessagerPerfect : public PCMessagerBase {
public:
virtual void Login(string username, string password){
PCMessagerBase::PlaySound();
//********
PCMessagerBase::Connect();
//........
}
virtual void SendMessage(string message){
PCMessagerBase::PlaySound();
//********
PCMessagerBase::WriteText();
//........
}
virtual void SendPicture(Image image){
PCMessagerBase::PlaySound();
//********
PCMessagerBase::DrawShape();
//........
}
};
//
class MobileMessagerLite : public MobileMessagerBase {
public:
virtual void Login(string username, string password){
MobileMessagerBase::Connect();
//........
}
virtual void SendMessage(string message){
MobileMessagerBase::WriteText();
//........
}
virtual void SendPicture(Image image){
MobileMessagerBase::DrawShape();
//........
}
};
//业务抽象 经典轻量发布版本 本身是业务抽象纬度,但因为另外一个纬度也要变化(平台纬度) 所以经典发布版本也要有多个平台类的实现
//这里是手机平台实现 跟pc平台存在大量类似重复代码
class MobileMessagerPerfect : public MobileMessagerBase {
public:
virtual void Login(string username, string password){
MobileMessagerBase::PlaySound();
//********
MobileMessagerBase::Connect();
//........
}
virtual void SendMessage(string message){
MobileMessagerBase::PlaySound();
//********
MobileMessagerBase::WriteText();
//........
}
virtual void SendPicture(Image image){
MobileMessagerBase::PlaySound();
//********
MobileMessagerBase::DrawShape();
//........
}
};
//实际调用
void Process(){
//编译时装配 需要指定不同平台的不用业务抽象 比如手机平台经典版本 手机平台完美版本等等
//所以这里如有一个纬度变化--业务抽象有M个(经典版本、完美版本...),另外一个纬度变化平台实现有N个(pc、mobile、pad、tv...)
//那么类至少需要1(messager)+N(平台实现基类)+M*N(最终用于用于发布的类 比如下面的手机平台完美版本类)
Messager *m =new MobileMessagerPerfect();
}
动机
某些类型的固有实现逻辑,使得它们有2个纬度的变化,或者多个纬度的变化
-
所以要利用面向对象技术使得类型可以轻松的沿着2个或者多个方向变化
-
假设需要实现一个发消息的业务,大致有三个方法login登录,sendMessage发文字消息 sendPicture发图片
模式定义
- 将抽象部分(业务功能)与实现部分(平台实现)分离,使它们 都可以独立地变化
结构图解
要点总结
-
Bridge模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓 抽象和实现沿着各自纬度的变化,即“子类化”它们。
-
Bridge模式有时候类似于多继承方案,但是多继承方案往往违背 单一职责原则(即一个类只有一个变化的原因),复用性比较差。 Bridge模式是比多继承方案更好的解决方法。
-
Bridge模式的应用一般在“两个非常强的变化维度”,有时一个 类也有多于两个的变化维度,这时可以使用Bridge的扩展模式
代码实现
//业务抽象基类 定义相关接口方法
class Messager{
protected:
MessagerImp* messagerImp;//用于平台实现基类指针 在平台切换方面可以运行时灵活变化
public:
virtual void Login(string username, string password)=0;
virtual void SendMessage(string message)=0;
virtual void SendPicture(Image image)=0;
virtual ~Messager(){}
};
//平台实现基类 定义相关接口方法
class MessagerImp{
public:
virtual void PlaySound()=0;
virtual void DrawShape()=0;
virtual void WriteText()=0;
virtual void Connect()=0;
virtual MessagerImp(){}
};
//不同平台实现子类 这里是pc
class PCMessagerImp : public MessagerImp{
public:
virtual void PlaySound(){
//**********
}
virtual void DrawShape(){
//**********
}
virtual void WriteText(){
//**********
}
virtual void Connect(){
//**********
}
};
//不同平台实现子类 这里是mobile
class MobileMessagerImp : public MessagerImp{
public:
virtual void PlaySound(){
//==========
}
virtual void DrawShape(){
//==========
}
virtual void WriteText(){
//==========
}
virtual void Connect(){
//==========
}
};
//业务抽象子类实现 这里是经典版本
class MessagerLite :public Messager {
public:
virtual void Login(string username, string password){
messagerImp->Connect();//通过基类拥有的平台实现基类指针 在不同平台间灵活切换
//........
}
virtual void SendMessage(string message){
messagerImp->WriteText();
//........
}
virtual void SendPicture(Image image){
messagerImp->DrawShape();
//........
}
};
//业务抽象子类实现 这里是完美版本
class MessagerPerfect :public Messager {
public:
virtual void Login(string username, string password){
messagerImp->PlaySound();//通过基类拥有的平台实现基类指针 在不同平台间灵活切换
//********
messagerImp->Connect();
//........
}
virtual void SendMessage(string message){
messagerImp->PlaySound();
//********
messagerImp->WriteText();
//........
}
virtual void SendPicture(Image image){
messagerImp->PlaySound();
//********
messagerImp->DrawShape();
//........
}
};
void Process(){
//运行时装配 类的数量大幅减少 但是实现并没有打折扣,这里在运行是初始化想要的平台实现子类就可以动态灵活变化
MessagerImp* mImp=new PCMessagerImp();
Messager *m =new MessagerPerfect(mImp);//传入pc平台子类 实现pc完美版本
}
工厂模式factory—对象创建
需求背景
我们需要实现一个文件分割器,支持分割多种类型文件(文本文件、普通二进制文件、图片、视频等等),所以我们就要根据需求创建对象
传统实现
//文件分割基类 文件分割实现
class ISplitter{
public:
virtual void split()=0;
virtual ~ISplitter(){}
};
//文件分割子类 二进制文件、文本文件、图片、视频
class BinarySplitter : public ISplitter{
};
class TxtSplitter: public ISplitter{
};
class PictureSplitter: public ISplitter{
};
class VideoSplitter: public ISplitter{
};
// ---------------
//高层调用模块
class MainForm : public Form
{
public:
void Button1_Click(){
ISplitter * splitter=new BinarySplitter();
//依赖具体类 违反了依赖倒置原则 高层稳定模块应该依赖抽象而不应该依赖具体的实现类
//这里的依赖为编译时依赖 具体的实现类可能反复变化就会导致高层模块也需要跟着编译甚至修改
splitter->split();
}
};
动机
- 在软件系统中,经常面临着创建对象的工作;由于需求的变化, 需要创建的对象的具体类型经常变化
- 因为直接new会依赖具体的实现类 所以要想办法绕过new 运行时动态的new出相应的类
- 一般是接口抽象之后的第一步工作
模式定义
- 定义一个用于创建对象的接口,让子类决定实例化哪一个类。
- Factory Method使得一个类的实例化延迟(目的:解耦, 手段:虚函数)到子类
工厂模式代码实现
//业务抽象基类 与工厂抽象基类可以放在一起
class ISplitter{
public:
virtual void split()=0;
virtual ~ISplitter(){}
};
//工厂抽象基类
class SplitterFactory{
public:
virtual ISplitter* CreateSplitter()=0;
virtual ~SplitterFactory(){}
};
//------------------------------
//高层调用模块
class MainForm : public Form
{
SplitterFactory* factory;//工厂抽象基类
public:
MainForm(SplitterFactory* factory){//通过构造方法 让更高层实际使用者 传入具体的工厂子类
this->factory=factory;
}
void Button1_Click(){
ISplitter * splitter=
factory->CreateSplitter();
//多态new 通过具体的工厂子类实例化具体的业务子类 这里不再依赖具体的业务子类 依赖的是工厂基类 具体的工厂子类变化或者业务处理子类变化对这个类没有编译时的影响,相对稳定,变化少一般问题也少,有变化就带来了测试工作量
splitter->split();
}
};
//-------------------------------------
//具体实现类和工厂子类放到同一个文件
class BinarySplitter : public ISplitter{
};
class TxtSplitter: public ISplitter{
};
class PictureSplitter: public ISplitter{
};
class VideoSplitter: public ISplitter{
};
//具体工厂子类 负责创建具体的业务处理子类
class BinarySplitterFactory: public SplitterFactory{
public:
virtual ISplitter* CreateSplitter(){
return new BinarySplitter();
}
};
class TxtSplitterFactory: public SplitterFactory{
public:
virtual ISplitter* CreateSplitter(){
return new TxtSplitter();
}
};
class PictureSplitterFactory: public SplitterFactory{
public:
virtual ISplitter* CreateSplitter(){
return new PictureSplitter();
}
};
class VideoSplitterFactory: public SplitterFactory{
public:
virtual ISplitter* CreateSplitter(){
return new VideoSplitter();
}
};
结构图解
要点总结
-
Factory Method模式用于隔离类对象的使用者和具体类型之间的 耦合关系。面对一个经常变化的具体类型,紧耦合关系(new)会导 致软件的脆弱。
-
Factory Method模式通过面向对象的手法,将所要创建的具体对 象工作延迟到子类,从而实现一种扩展(而非更改)的策略,较好 地解决了这种紧耦合关系,只需要增加相应的业务子类和对应的工厂子类 使用者MainFrom无需更改
-
Factory Method模式解决“单个对象”的需求变化。缺点在于要 求创建方法/参数相同。
抽象工厂AbstractFactory—对象创建
需求背景
需要写一个Dao数据访问层,刚开始只有oracle,后面要支持mysql,sqlserver db2等等,每个数据库系列都有相关的多个对象(connection、command、dataReader…),这些对象时相关的,有依赖的,不能传一个oracle的connection给mysql的command
传统解决方案
class EmployeeDAO{
public:
vector<EmployeeDO> GetEmployees(){
//刚开始只有一种数据库 后面可能要新增其它数据库 所以这里可能会写成if else来创建对象 给个入参来判断不同的类型
//这就是依赖了底层具体的对象 违反了依赖倒置原则DIP 应该依赖抽象
SqlConnection* connection =
new SqlConnection();
connection->ConnectionString = "...";
SqlCommand* command =
new SqlCommand();
command->CommandText="...";
command->SetConnection(connection);
SqlDataReader* reader = command->ExecuteReader();
while (reader->Read()){
}
}
};
用原始工厂方法模式改造
//数据库访问有关的基类
class IDBConnection{
};
class IDBConnectionFactory{
public:
virtual IDBConnection* CreateDBConnection()=0;
};
class IDBCommand{
};
class IDBCommandFactory{
public:
virtual IDBCommand* CreateDBCommand()=0;
};
class IDataReader{
};
class IDataReaderFactory{
public:
virtual IDataReader* CreateDataReader()=0;
};
//支持SQL Server
class SqlConnection: public IDBConnection{
};
class SqlConnectionFactory:public IDBConnectionFactory{
};
class SqlCommand: public IDBCommand{
};
class SqlCommandFactory:public IDBCommandFactory{
};
class SqlDataReader: public IDataReader{
};
class SqlDataReaderFactory:public IDataReaderFactory{
};
//支持Oracle
class OracleConnection: public IDBConnection{
};
class OracleCommand: public IDBCommand{
};
class OracleDataReader: public IDataReader{
};
//如果系列对象(有相关性的多个对象)有3个 使用者就拥有3个对应的工厂基类对象指针
class EmployeeDAO{
IDBConnectionFactory* dbConnectionFactory;
IDBCommandFactory* dbCommandFactory;
IDataReaderFactory* dataReaderFactory;
public:
vector<EmployeeDO> GetEmployees(){
IDBConnection* connection =
dbConnectionFactory->CreateDBConnection();
connection->ConnectionString("...");
IDBCommand* command =
dbCommandFactory->CreateDBCommand();
command->CommandText("...");
command->SetConnection(connection); //关联性
//这里没法保证传给oracle的command的connection是mysql类型的工厂初始化的 这个时候就会出现错误
IDBDataReader* reader = command->ExecuteReader(); //关联性
while (reader->Read()){
}
}
};
因为系列对象彼此间有相关性,会产生依赖,所以直接多个工厂基类的方法,无法建立其相关性,拿这里的例子来说,法保证传给oracle的command的connection是mysql类型的工厂初始化的 这个时候就会出现错误。
动机
- 在软件系统中,经常面临着“一系列相互依赖的对象”的创建工 作;同时,由于需求的变化,往往存在更多系列对象的创建工作
模式定义
- 供一个接口,让该接口负责创建一系列“相关或者相互依 赖的对象”,无需指定它们具体的类
结构图解
要点总结
-
如果没有应对“多系列对象构建”的需求变化,则没有必要使用 Abstract Factory模式,这时候使用简单的工厂完全可以。
-
“系列对象”指的是在某一特定系列下的对象之间有相互依赖、 或作用的关系。不同系列的对象之间不能相互依赖。
-
Abstract Factory模式主要在于应对“新系列”的需求变动。其缺 点在于难以应对“新对象”的需求变动
代码实现
//数据库访问有关的基类
class IDBConnection{
};
class IDBCommand{
};
class IDataReader{
};
//支持SQL Server
class SqlConnection: public IDBConnection{
};
class SqlCommand: public IDBCommand{
};
class SqlDataReader: public IDataReader{
};
class SqlDBFactory:public IDBFactory{
public:
virtual IDBConnection* CreateDBConnection()=0;
virtual IDBCommand* CreateDBCommand()=0;
virtual IDataReader* CreateDataReader()=0;
};
class IDBFactory{
public:
virtual IDBConnection* CreateDBConnection()=0;
virtual IDBCommand* CreateDBCommand()=0;
virtual IDataReader* CreateDataReader()=0;
};
//支持Oracle 类似操作 增加相关类即可
class OracleConnection: public IDBConnection{
};
class OracleCommand: public IDBCommand{
};
class OracleDataReader: public IDataReader{
};
class EmployeeDAO{
IDBFactory* dbFactory;
public:
vector<EmployeeDO> GetEmployees(){
IDBConnection* connection =
dbFactory->CreateDBConnection();
connection->ConnectionString("...");
IDBCommand* command =
dbFactory->CreateDBCommand();
command->CommandText("...");
command->SetConnection(connection); //关联性解决了多对象之间的依赖关系 因为是同一个工厂创建的
IDBDataReader* reader = command->ExecuteReader(); //关联性
while (reader->Read()){
}
}
};
原型模式 prototype—对象创建
需求背景及动机
如果需要初始化一个比较复杂的对象,这个对象刚开始初始化的状态不是你想要的状态,这个时候用原始工厂模式初始化就比较尴尬了,或者更直白的说就是需要一个对象运行一段时间后,达到了某种状态后,我才要这个对象去做一些处理,就可以用原型模式,用拷贝对象的方式创建一个对象。
如果创建对象只是非常简单原始的new即可,那原始工厂模式足够了,如果在最开始很难创建出来或者你希望对象运行到一定状态后要保留这个状态就可以考虑原型模式
模式定义
使用原型实例指定创建对象的种类,然后通过拷贝这些原型来创建新的对象,达到保存某种状态的目的。
是专门用来初始化相对很复杂的对象的时候用的,是工厂模式的变形。
现实项目用的不多
结构图解
要点总结
- 同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,同样要求这些“易变类”拥有“稳定的接口”
- 使用原型克隆的方法来实现创建一个包含有中间状态的对象
- Clone方法要是深拷贝 c++可以用拷贝构造函数实现
代码实现 对比工厂模式看
//原型基类包含克隆具体子类的虚函数 也包括其它一些业务函数
class ISplitter{
public:
virtual void split()=0;//实际业务虚函数
virtual ISplitter* clone()=0; //通过克隆自己来创建对象
virtual ~ISplitter(){}
};
//具体的业务子类 继承原型基类 要实现clone自己的虚函数
class BinarySplitter : public ISplitter{
public:
virtual ISplitter* clone(){
return new BinarySplitter(*this);
}
};
class TxtSplitter: public ISplitter{
public:
virtual ISplitter* clone(){
return new TxtSplitter(*this);
}
};
class PictureSplitter: public ISplitter{
public:
virtual ISplitter* clone(){
return new PictureSplitter(*this);
}
};
class VideoSplitter: public ISplitter{
public:
virtual ISplitter* clone(){
return new VideoSplitter(*this);
}
};
//使用者-----------------
class MainForm : public Form{
ISplitter* prototype;//原型基类指针
public:
MainForm(ISplitter* prototype){
this->prototype=prototype;//传入含有中间状态的子类对象(或者本身很难初始化完全)
}
void Button1_Click(){
ISplitter * splitter=
prototype->clone(); //克隆原型 深拷贝一个已经运行了一段时间的对象 保存其中间状态
splitter->split();
}
};
构建器模式Builder—对象创建
需求背景
//支持建不同房子 都需要几个共同的步骤,但是不同房子每个步骤实现不一样
//我们可以将这些稳定的步骤抽象出来,下面的init函数就是这样
传统实现
//支持建不同房子 都需要几个共同的步骤,但是不同房子每个步骤实现不一样
//我们可以将这些稳定的步骤抽象出来,下面的init函数就是这样
class House{
public:
void init(){
this->BuildPart1();
for (int i = 0; i < 4; i++){
this->BuildPart2();
}
bool flag=this->BuildPart3();
if(flag){
this->BuildPart4();
}
this->BuildPart5();
}
virtual ~House(){}
protected:
virtual void BuildPart1()=0;
virtual void BuildPart2()=0;
virtual bool BuildPart3()=0;
virtual void BuildPart4()=0;
virtual void BuildPart5()=0;
//....
};
// 建造石头房子 override5个建造房子的步骤
class StoneHouse: public House{
protected:
virtual void BuildPart1(){}
virtual void BuildPart2(){}
virtual bool BuildPart3(){}
virtual void BuildPart4(){}
virtual void BuildPart5(){}
};
int main(){
House *p=new StoneHouse();
p->init();
//p的其它操作
}
传统解决方案,将构建房子的方法都写到房子类本身中,如果构建房子的方法经常改变,那么我们就应该将构建部分拆分出去,否则修改原先的代码很有可能导致房子本身除了构建的其它逻辑出现bug。
有时候构建一个对象需要的传入大量的参数,然后又要执行很复杂的多个方法调用,而且还经常变化,这个时候应该改考虑将构建对象的工作单独拆分出去。
动机
软件系统中,有时候面临着“一个复杂对象”的创建工作,其 通常由各个部分的子对象用一定的算法构成;由于需求的变化,这 个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在 一起的算法却相对稳定
比如盖房子的大致流程固定,但是盖不同的房子每个子流程的实现各不相同,还可能经常变化。
模式定义
将一个复杂对象的构建与其表示相分离,使得同样的构建过 程(稳定)可以创建不同的表示(变化)
结构图解
代码实现
//产品基类 定义房子除了构建本身的其它接口
class House{
//....
};
//产品构建器基类 负责定义相关共有子接口 不实现
class HouseBuilder {
public:
House* GetResult(){
return pHouse;
}
virtual ~HouseBuilder(){}
protected:
House* pHouse;
virtual void BuildPart1()=0;
virtual void BuildPart2()=0;
virtual void BuildPart3()=0;
virtual void BuildPart4()=0;
virtual void BuildPart5()=0;
};
//石头房子 继承房子,实现除了构建工作外的的其它方法
class StoneHouse: public House{
};
//石头房子构建器 实现构建房子对象需要的几个共有的抽象接口,届时会组合这些接口生成石头房子
class StoneHouseBuilder: public HouseBuilder{
protected:
virtual void BuildPart1(){
//pHouse->Part1 = ...;
}
virtual void BuildPart2(){
}
virtual void BuildPart3(){
}
virtual void BuildPart4(){
}
virtual void BuildPart5(){
}
};
//监工 含有产品builder基类指针 传入不同产品的builder子类,就会构建出来不同的产品
//当然这里也要定义实现共用的构建算法(不同房子的建造流程是一直的 都要先打地基 然后....)
//如果项目发展后期需要建造一个更新奇的房子 不需要先打地基,而是先筑地梁 那就搞一个新的监工实现不同的构建算法
class HouseDirector{
public:
HouseBuilder* pHouseBuilder;//基类builder指针
HouseDirector(HouseBuilder* pHouseBuilder){
this->pHouseBuilder=pHouseBuilder;
}
//统一的构建方法
House* Construct(){
pHouseBuilder->BuildPart1();
for (int i = 0; i < 4; i++){
pHouseBuilder->BuildPart2();
}
bool flag=pHouseBuilder->BuildPart3();
if(flag){
pHouseBuilder->BuildPart4();
}
pHouseBuilder->BuildPart5();
return pHouseBuilder->GetResult();
}
};
要点总结
- builder 模式主要用于“分步骤构建一个复杂的对象”。在这其中 “分步骤”是一个稳定的算法,而复杂对象的各个部分则经常变化
- 变化点在哪里,封装哪里—— Builder模式主要在于应对“复杂对 象各个部分”的频繁需求变动。其缺点在于难以应对“分步骤构建 算法”的需求变动
- builder模式跟模版方法有点类似,但builder模式往往只是为了解决构建对象困难而产生的,而且builder模式有一个监工,模版方法的监工其实是父类。
单例模式singleton—性能问题
动机
实际开发项目中中就是需要一些这样的类,保证它们在系统中只有一个实例 ,才能保证逻辑正确性,或者大部分时候也是为了提高性能,因为频繁的产生多个实例,会带来性能下降,内存消耗等等
代码实现
class Singleton {
private:
Singleton() {cout << "Singleton construct\n";}
Singleton(const Singleton& s) = delete; // 禁用拷贝构造函数
Singleton& operator=(const Singleton& s) = delete; // 禁用拷贝赋值操作符
static Singleton m_singleton;
public:
static Singleton* getInstance(){
return &m_singleton;
}
};
//这种模式绝对是ok的,只是有些人会说有一定的内存浪费,因为即使后面不使用也会生成相应对象
//类的静态变量在类外部初始化 而且是存放在静态存储区 进入main函数之前就已经执行了
Singleton Singleton::m_singleton;
int main(){
Singleton *a=Singleton::getInstance();
Singleton *b=Singleton::getInstance();
Singleton *c=Singleton::getInstance();
Singleton *d=Singleton::getInstance();
}
class Singleton {
public:
static Singleton* getInstance(){
//如果线程1 执行到下面一行(还没有new出来) cpu让给线程2,线程2也到这行 最后2个线程都会new出来
if ( m_singleton==nullptr){ //线程不安全 可能会出现new出多个的情况
return new Singleton();
}
return m_singleton;
}
private:
Singleton() {cout << "Singleton construct\n";}
Singleton(const Singleton& s) = delete; // 禁用拷贝构造函数
Singleton& operator=(const Singleton& s) = delete; // 禁用拷贝赋值操作符
static Singleton *m_singleton;
};
//类的静态变量在类外部初始化 而且是存放在静态存储区 进入main函数之前就已经执行了
Singleton* Singleton::m_singleton=nullptr;
}
class Singleton {
public:
static Singleton* getInstance(){
m_lock.lock(); //还没有初始化的这个地方是要加锁的 但是如果已经new出来了 多个线程每次调用都要加锁就会影响性能
if ( m_singleton==nullptr){
m_singleton= new Singleton();
}
m_lock.unlock();
return m_singleton;
}
private:
Singleton() {cout << "Singleton construct\n";}
Singleton(const Singleton& s) = delete; // 禁用拷贝构造函数
Singleton& operator=(const Singleton& s) = delete; // 禁用拷贝赋值操作符
static Singleton *m_singleton;
static mutex m_lock;
};
//类的静态变量在类外部初始化 而且是存放在静态存储区 进入main函数之前就已经执行了
Singleton* Singleton::m_singleton=nullptr;
mutex Singleton::m_lock;
class Singleton {
public:
static Singleton* getInstance(){
if ( m_singleton==nullptr){//如果已经初始化 则直接返回即可
m_lock.lock(); //还没有初始化的这个地方是要加锁的
//再次检查是否new过了 下面判空逻辑不能去掉 去掉了还是会new出多个实例 因为如果2个线程同时进入上面的盼空后 就会出现
if ( m_singleton==nullptr){
m_singleton= new Singleton();
// 对象的new不是原子操作 1、分配内存,2 调用构造,3 赋值操作,到第3步的时候才是m_singleton非空
//1、分配内存,2 赋值操作 3 调用构造,到第2步的时候才是m_singleton非空
//如果顺序是先2在3 2做完后 另外一个线程就判断非空 直接使用但还没有调用构造函数这个时候就会让另外线程出错
}
m_lock.unlock();
}
return m_singleton;
}
private:
Singleton() {cout << "Singleton construct\n";}
Singleton(const Singleton& s) = delete; // 禁用拷贝构造函数
Singleton& operator=(const Singleton& s) = delete; // 禁用拷贝赋值操作符
static Singleton *m_singleton;
static mutex m_lock;
};
//类的静态变量在类外部初始化 而且是存放在静态存储区 进入main函数之前就已经执行了
Singleton* Singleton::m_singleton=nullptr;
mutex Singleton::m_lock;
//多线程安全版本 双检查为空并加锁 利用atomic库消除内存读写reorder问题 相对实现复杂 可以使用
class Singleton {
public:
static Singleton* getInstance(){
Singleton* tmp = m_singleton.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
if ( tmp==nullptr){//如果已经初始化 则直接返回即可
std::lock_guard<std::mutex> lock(m_lock);; //还没有初始化的这个地方是要加锁的
if ( tmp==nullptr){//再次检查是否new过了 这行不能去掉 去掉了还是会new出多个实例 因为如果2个线程同时进入67行代码 就会出现
tmp = new Singleton(); // 1、分配内存,2 调用构造,3 赋值操作 这里不会再出现内存读写reorder问题
std::atomic_thread_fence(std::memory_order_release);//释放内存fence
m_singleton.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
private:
Singleton() {cout << "Singleton construct\n";}
Singleton(const Singleton& s) = delete; // 禁用拷贝构造函数
Singleton& operator=(const Singleton& s) = delete; // 禁用拷贝赋值操作符
static std::atomic<Singleton*> m_singleton;
static mutex m_lock;
};
//类的静态变量在类外部初始化 而且是存放在静态存储区 进入main函数之前就已经执行了
std::atomic<Singleton*> Singleton::m_singleton;
mutex Singleton::m_lock;
-
通过返回局部静态变量方式实现 线程安全 写法简单 推荐使用
class Singleton { public: static Singleton* getInstance(){ static Singleton instance; //这种相对于使用atomic的写法更简洁 而且又是懒汉模式 使用的时候才生成 推荐使用 //局部静态变量初始化是会加锁的 也就是线程安全的,而且是c++和编译器保证的 推荐使用 return &instance; } private: Singleton() {cout << "Singleton construct\n";} Singleton(const Singleton& s) = delete; // 禁用拷贝构造函数 Singleton& operator=(const Singleton& s) = delete; // 禁用拷贝赋值操作符 static Singleton* m_singleton; }; //类的静态变量在类外部初始化 而且是存放在静态存储区 进入main函数之前就已经执行了 Singleton* Singleton::m_singleton=nullptr;
享元模式FlyWeight—性能问题
动机
如果一切皆对象,有些对象在系统中大量存在(万、十万、百万、千万等),这个时候就会带来大量的运行时开销,大部分情况下可能是内存浪费。比如设计围棋游戏,每一个黑旗、白棋都搞成对象,成千上万的人在玩,那就是十万甚至百万个对象,比如设计字体处理软件,每个有大量的字符,每个字符又有大量的字体,每个字符每个字体都生成1个对象,最终占用内存将是非常夸张的,而大部分时候是不需要,因为这些对象都是只读的,完全可以用池的概念来共享,如果已经创建过了,则直接使用 不再创建,否则就创建并且加到共享池中。
模式定义
通过共享技术有效支持大量细粒度的对象。
代码
class Font {
private:
//unique object key
string key;
//object state
//....
public:
Font(const string& key){
//...
}
};
//对象生成工厂 用map实现共享池
class FontFactory{
private:
map<string,Font* > fontPool;
public:
Font* GetFont(const string& key){
map<string,Font*>::iterator item=fontPool.find(key);
if(item!=footPool.end()){
return fontPool[key];
}
else{
Font* font = new Font(key);
fontPool[key]= font;
return font;
}
}
void clear(){
//...
}
};
要点总结
享元模式主要是解决大量对象带来的内存消耗问题,如果评估不可能发生,也没有必要使用。
门面模式 facade—接口隔离
需求背景与动机
项目中用到了多种数据库,比如mysql、oracle、DB2、Timesten等等,如果多个程序直接与这些数据库打交道,每个程序都会非常复杂,二期如果数据库升级,数据库接口变动,相关的程序都需要改动,这个时候就需要一个数据访问层(提供固定的数据访问接口),给外部提供统一的访问接口,将各个数据库的处理细节屏蔽,即使数据库升级改动,只修改接口实现,外部程序不用改动。
模式定义
为系统中的一组接口提供一个一致稳定的界面,facade模式定义一个高层接口,使得系统外部使用者更加易用,稳定,不用频繁变化。
要点总结
- facade模式没有固定的代码模式,更多的是一种架构思维
- facade模式内部必须是高相关的组件,不能什么都放
代理模式Proxy—接口隔离
需求背景和动机
由于一些实际的现实原因,不能直接使用某个对象(比如使用之前要做一些安全控制,或者说直接创建某个对象的开销太大,又或者根本无法直接创建[访问一些分布式机器上的资源]),所以直接访问会给使用者或者系统架构的设计代码麻烦,所以就需要一种代理,就像现实世界的代理一样,不用管一些细节,只要它们提供相应的服务即可(软件层面可能就是提供一些可用的接口)。
代理的操作往往跟直接操作对象一致,可以理解增加了一个间接层也就是代理,来实现控制这些对象。
模式定义
为其它对象提供一种代理以控制(隔离,使用接口)对这个对象的访问。
我的理解就是增加一个间接层 不让直接使用,这个有很多好处,跟门面模式一样,可以隔离很多变化。
结构图解
代码
class ISubject{
public:
virtual void process();
};
class RealSubject: public ISubject{
public:
virtual void process(){
//....
}
};
//客户端直接使用某个对象 如果现实中不能让直接使用 比如要增加安全控制
//合法的client才能使用就要考虑改造了 而且不能对每个client都改造 所以最后实现起可能比较复杂混乱
class ClientApp{
ISubject* subject;
public:
ClientApp(){
subject=new RealSubject();
}
void DoTask(){
//...
subject->process();
//....
}
};
class ISubject{
public:
virtual void process();
};
class RealSubject: public ISubject{
public:
virtual void process(){
//....
}
};
//Proxy的设计 继承ISubject实现 一致性的接口
class SubjectProxy: public ISubject{
public:
virtual void process(){
//对RealSubject的一种间接访问 比如调用前做安全控制 统计下调用次数,使用rpc访问别的机器等等
//....
}
};
//客户端使用代理跟直接使用RealSubject没有太大区别
class ClientApp{
ISubject* subject;
public:
ClientApp(){
subject=new SubjectProxy();
}
void DoTask(){
//...
subject->process();
//....
}
};
要点总结
- “增加一个中间层”是软件系统对许多复杂问题的一种常见解决方案,如果直接使用某个对象会带来很多麻烦,就尝试使用代理模式
- 具体proxy设计模式的实现方法,千差万别。
适配器模式Adapter—接口隔离
需求背景、动机
现实项目中由于应用环境的变化,常常需要将“一些现存的对象”放在新环境使用,但是新环境要求的接口又是一些老对象所不能直接满足的,这个时候就会想到适配器模式,简单来说就是定义一个适配器类继承新接口的基类,但是用对象组合的方式拥有老对象的基类或者子类,用老对象的某些方法实现新的接口。
现实生活中也有很多适配器,比如电源的适配器,HTMI转VGA等等,说白了就是把一个老的接口转成新的接口。
模式定义
将一个类的接口转换成客户希望的另外一个新接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
结构图解
要点总结
- Adapter模式主要应用于“希望复用一些现存的类”,但接口又与复用环境要求的不一致,在遗留代码复用,类库迁移等方面非常有用。
- GoF23定义的适配器有2种,对象适配器(上图的这种)和类适配器(直接多继承老对象),类适配器不推荐使用,因为使用了多继承,一般都是被摒弃的。对象组合的方式更符合松耦合精神
- 其实实际项目中完全也不用拘泥于书中定义的结构,即使把老对象作为新对象新接口的参数也是一样的。
代码
Adapter适配器模式 c++
//目标接口(新接口)
class ITarget{
public:
virtual void process()=0;
};
//遗留接口(老接口)
class IAdaptee{
public:
virtual void foo(int data)=0;
virtual int bar()=0;
};
//遗留类型
class OldClass: public IAdaptee{
//....
};
//对象适配器
class Adapter: public ITarget{ //继承新接口基类
protected:
IAdaptee* pAdaptee;//组合方式拥有老对象 这样就可以通过加工老对象的方法实现新接口
public:
Adapter(IAdaptee* pAdaptee){
this->pAdaptee=pAdaptee;
}
virtual void process(){
int data=pAdaptee->bar();
pAdaptee->foo(data);
}
};
//类适配器
class Adapter: public ITarget,
protected OldClass{ //多继承 不推荐使用
};
int main(){
IAdaptee* pAdaptee=new OldClass();
ITarget* pTarget=new Adapter(pAdaptee);
pTarget->process();
}
//stl的栈和队列 大致也是通过对象组合的方式实现的 也可以理解为Adapter模式
class stack{
deqeue container;
};
class queue{
deqeue container;
};
中介者Mediator—接口隔离
需求背景和动机
-
如果现实项目中出现了多个对象互相关联交互的情况,它们彼此之间徐奥维持一种复杂的引用关系,A变了 BC也要变,C变了D也要变,D变了可能A又要跟着做相应变化,这个时候如果遇到需求变更,改动起来将会非常麻烦,而且容易出错。
-
这个时候就可以考虑引入一个中介者,让它们彼此的引用拆开,变成都给中介者打交道,A变了如果要改变B或者C,就通知中介者,然后中介去通知BC做改变,当然要建立良好的通知机制和协议。
-
如果中介者最后非常庞大或者复杂,也要考虑继续拆分中介者
模式定义
用中介对象来封装(变化)一些列的对象交互,让原先各个对象不需要显示相互引用(编译时依赖—>运行时依赖),从而使其耦合松散,便于管理变化,而且可以独立的改变它们之间的交互。
结构图解
要点总结
- 将多个对象复杂的关联关系解耦,Mediator模式将多个对象间的控制逻辑进行统一管理,将“多个对象互相关联”变成了"多个对象都和中介者关联“,简化了系统的维护,抵御了可能的变化。
- 如果Mediator变得太多复杂也要对其拆分处理
- facade模式是解耦系统间(单向)的对象关联关系,但Mediator模式是解耦系统内各个对象之间的(双向)关联关系。
状态模式State—状态变化
需求背景、动机
-
在软件构建过程中,某些对象的状态如果改变,其行为也会随之发生变化,比如文档处于只读状态,则支持行为和读写状态的行为可能就完全不同了
-
如何在运行时根据对象的状态来透明的更改对象的行为?而且不会为对象操作和状态转化之间引入耦合?
例子
假设有一个网络状态处理类,大致有3种操作,但是对于每种操作,不同的状态有不同的处理,而且处理后要切换到对应的状态
//枚举值 定义网络的多种状态 未来可能会新增状态的处理
enum NetworkState
{
Network_Open,
Network_Close,
Network_Connect,
};
//网络处理类
class NetworkProcessor{
NetworkState state;
public:
//针对操作1 不同的状态会发生不同的行为,同时操作后状态要切换 这里面后续可能还要增加状态 一来修改可能引入bug,二来修改起来往往比较困难,因为太多ifelse了容易晕
void Operation1(){
if (state == Network_Open){
//**********
state = Network_Close;
}
else if (state == Network_Close){
//..........
state = Network_Connect;
}
else if (state == Network_Connect){
//$$$$$$$$$$
state = Network_Open;
}
}
//针对操作2 不同的状态会发生不同的行为,同时操作后状态要切换
public void Operation2(){
if (state == Network_Open){
//**********
state = Network_Connect;
}
else if (state == Network_Close){
//.....
state = Network_Open;
}
else if (state == Network_Connect){
//$$$$$$$$$$
state = Network_Close;
}
}
//针对操作3 不同的状态会发生不同的行为,同时操作后状态要切换
public void Operation3(){
//。。。。。
}
};
模式定义
- 允许一个对象在其内部状态改变的时候改变它的行为,从而使对象看起来似乎修改了其行为。
结构图解
要点总结
- State模式核心是将所有与一个特定状态相关的行为都放入一个State的子类对象中,在对象状态切换的时候,其实是切换了不同的子类对象指向,但同时维持State的接口(都继承同一个基类),这样就实现了具体的操作和状态转换之间的解耦。
- 为不同的状态引入不同的对象使得状态转换变得更加明确,而且可以保证不会出现状态不一致的情况,因为转换是源自行的,要么彻底转换过来,要么不转换。
- 如果State对象没有实例变量,可以用singetone模式共享同一个对象,从而节省对象开销
代码
//状态基类,将不同的状态改为不同的类(对象),实现同样的接口
class NetworkState{
public:
NetworkState* pNext;
virtual void Operation1()=0;
virtual void Operation2()=0;
virtual void Operation3()=0;
virtual ~NetworkState(){}
};
//打开状态类 要实现不同的操作对应应该怎么处理 同时操作后应该切换到什么状态
class OpenState :public NetworkState{
static NetworkState* m_instance;
public:
static NetworkState* getInstance(){
if (m_instance == nullptr) {
m_instance = new OpenState();
}
return m_instance;
}
//这里将只关注打开状态在Operation1时如何处理,不容易混淆,同时切换状态其实是切换子类指针指向,是有原子性的
void Operation1(){
//**********
pNext = CloseState::getInstance();
}
void Operation2(){
//..........
pNext = ConnectState::getInstance();
}
void Operation3(){
//$$$$$$$$$$
pNext = OpenState::getInstance();
}
};
//关闭状态类 要实现不同的操作对应应该怎么处理 同时操作后应该切换到什么状态
class CloseState:public NetworkState{ };
//..
class NetworkProcessor{
NetworkState* pState;
public:
//状态类初始化后 针对不同的状态 操作接口是类似的,那就是执行想要的操作,切换到下一个状态,
//代码保持稳定,即使新增状态,如果接口不改变 NetworkProcessor类代码无需改变
NetworkProcessor(NetworkState* pState){
this->pState = pState;
}
void Operation1(){
//...
pState->Operation1();
pState = pState->pNext;
//...
}
void Operation2(){
//...
pState->Operation2();
pState = pState->pNext;
//...
}
void Operation3(){
//...
pState->Operation3();
pState = pState->pNext;
//...
}
};
备忘录Memento—-状态变化
需求背景、动机
- 现实软件系统有时候需要保存某个对象的状态,相当于给当下的对象来个快照保存起来,以便未来某个时间恢复这个状态做相应的处理,又不能破坏对象的封装性,不能暴漏公共接口给外部。
- 最开始的时候就想到了备忘录模式,所谓备忘录简单理解就是深拷贝一个对象,保存起来,未来某个时间再拿这个备忘录重建对象,实现某些特定的功能。
模式定义
- 不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态,以实现特定的功能。
结构图解
要点总结
- 备忘录Memento需要存储原发器Originator对象的内部状态,以备需要时恢复原发器Originator的状态
- Memento的核心是信息隐藏
- 因为设计模式是94年提出了,现在这个模式有点过时,因为很多高级语言都提供了序列化、反序列化的技术,但是思想是一样的。
代码
//备忘录类
class Memento
{
string state;
//.. 这里为了保存内部的状态 可能实现比较复杂 仅为示意代码
public:
Memento(const string & s) : state(s) {}
string getState() const { return state; }
void setState(const string & s) { state = s; }
};
//原发器类
class Originator
{
string state;
//....
public:
Originator() {}
//创建备忘录
Memento createMomento() {
Memento m(state);
return m;
}
//用备忘录恢复原始状态
void setMomento(const Memento & m) {
state = m.getState();
}
};
int main()
{
Originator orginator;
//..做相应处理 改变状态
//捕获对象状态,存储到备忘录
Memento mem = orginator.createMomento();
//... 改变orginator状态
//从备忘录中恢复
orginator.setMomento(mem);
}
组合模式Composit—数据结构
需求背景和动机
- 如何客户代码过多的依赖对象容器复杂的内部实现的数据结构,对象容器内部的实现结构(不是抽象接口)如果发生变化,那么客户代码也会频繁变化,就带来了代码的维护性,扩展性等弊端
- 所以需要将客户代码与复杂的对象容器结构解耦,让客户不管是处理什么样的结构都调用统一的接口简单处理,不会因为对象内部数据结构的变化而发生变化
- 假设有一个需求需要处理单个叶子节点,也要复杂节点(list集合 装有多个节点)
模式定义
将对象祖辈成属性结构以表示“部分-整体”的层次结构,Composite是的用户对单个对象还是组合对象的使用具有一致性(稳定)。
- 其实就是单个对象 组合对象都继承同一个基类,实现同一个公共接口,这样用多态实现,客户端的调用就是一致的,不用频繁改变。
结构图解
代码
Composite组合模式c++
#include <iostream>
#include <list>
#include <string>
#include <algorithm>
using namespace std;
//组件基类 定义公用接口 这些接口往往是直接暴漏给客户的
class Component
{
public:
virtual void process() = 0;
virtual ~Component(){}
};
//支持复杂结构的处理 继承组件基类 实现process公共外部接口
class Composite : public Component{
string name;
list<Component*> elements;
public:
Composite(const string & s) : name(s) {}
//因为内部是一个list集合,所以需要add remove方法 add的可以是简单节点 也可以还是一个list
void add(Component* element) {
elements.push_back(element);
}
void remove(Component* element){
elements.remove(element);
}
//实现公共外部接口
void process(){
//1. process current node
//2. process leaf nodes 循环对list内部的每个节点都处理
for (auto &e : elements)
e->process(); //多态调用
}
};
//叶子节点 简单节点 也实现process公共接口
class Leaf : public Component{
string name;
public:
Leaf(string s) : name(s) {}
//实现公共process接口
void process(){
//process current node
}
};
// 客户端的调用
void Invoke(Component & c){
//...
c.process();//这里客户端不用关心组件到底是简单节点还是集合复杂节点 用多态实现了统一的处理
//如果这里不用多态,客户就要区分到底是简单节点还是list集合,针对list集合要做想要的循环遍历处理,把对象的更多细节暴漏给客户
//同时如果list数据结构发生了变化 比如改为了set实现,那么客户端的代码也要跟着改变 这是非常糟糕的。
}
int main()
{
Composite root("root");
Composite treeNode1("treeNode1");
Composite treeNode2("treeNode2");
Composite treeNode3("treeNode3");
Composite treeNode4("treeNode4");
Leaf leat1("left1");
Leaf leat2("left2");
root.add(&treeNode1);
treeNode1.add(&treeNode2);
treeNode2.add(&leaf1);
root.add(&treeNode3);
treeNode3.add(&treeNode4);
treeNode4.add(&leaf2);
//客户端不论是处理复杂节点还是叶子节点 代码保持一直稳定 不用理会组件对象内部数据结构容器实现的变化
Invoke(root);
Invoke(leaf2);
Invoke(treeNode3);
}
要点总结
- Composite模式采用树形结构实现部片存在的对象容器,从而将一对多 变为“一对一”,其实也就是原先对多个对象,现在变为对一个基类,让原先的多个对象都继承同一个基类((拥有同样的公共接口)),这样客户就不用关心某个复杂对象内部的数据结构实现,也不会因为其数据结构的变动而导致客户端代码修改。
- Composite模式在具体实现中,可以让负对象中的子对象反向追溯,如果负对象有频繁的遍历需求,可以使用缓存来改善效率
迭代器模式Iterator—数据结构
需求背景和动机
- 软件构建过程中,集合对象内部结构常常变化各异,但对于这些集合对象,我们不希望暴漏其内部数据结构,而且希望提供一个一致的接口让客户访问,同时如果提供统一的接口,就有可能提供统一的算法
- 使用面向对象技术(也就是多态 有个迭代器基类,定义统一的几个接口,让不同的集合迭代器对象都继承这个基类,实现其方法即可)
模式定义
- 提供一种方法顺序访问一个聚合对象中的各个元素,而不暴漏(稳定)该对象内部的表示(实现细节 比如某些数据结构)
结构图解
代码
//迭代器基类 定义公共操作接口 对客户暴漏
template<typename T>
class Iterator
{
public:
virtual void first() = 0;
virtual void next() = 0;
virtual bool isDone() const = 0;
virtual T& current() = 0;
};
//生成某一个类型的迭代器
template<typename T>
class MyCollection{
public:
Iterator<T> GetIterator(){
//...
}
};
//具体的迭代其子类实现 继承迭代器基类 实现公共接口
template<typename T>
class CollectionIterator : public Iterator<T>{
MyCollection<T> mc;
public:
CollectionIterator(const MyCollection<T> & c): mc(c){ }
void first() override {
}
void next() override {
}
bool isDone() const override{
}
T& current() override{
}
};
//调用
void MyAlgorithm()
{
MyCollection<int> mc;//某一个集合
Iterator<int> iter= mc.GetIterator();//针对某一个集合创建迭代器对象
for (iter.first(); !iter.isDone(); iter.next()){//遍历
cout << iter.current() << endl;
}
//这里每次调用都是多态 就要找到虚函数指针 如果量太大,
//这个效率就会有问题 所以c++ STL的迭代器都是用模版实现了 也就是编译时多态,而不是运行时多态
}
要点总结
- 因为虚函数在性能上的消耗,现代c++ STL库的迭代器都是用模版实现的,也就是编译时多态,而不是运行时多态 但思想时一致的
- 迭代多态为遍历不同的集合结构提供了一个统一的接口,从而支持同样的算法在不同和集合结构上操作
- 使用迭代器一定要考虑健壮性,遍历的同时如果更改了所在的集合结构,很可能出问题,因为内存已经发生变化,大部分迭代器是只读的。
职责链ChainOfResponsibility—数据结构
需求背景、动机
- 如果你的项目中一个请求可能会被多个对象处理,但是每个请求在运行时只能有一个接收者,如果显示指定,将必不可少的带来情趣发送者与接受者的紧耦合
- 如何让请求的发送者不需要指定具体的接受者?
模式定义
- 定义一个请求处理基类,定义公共处理接口,同时有一个基类指针next 运行时可以指向具体的请求处理子类,这样就可以形成一个链条,第一个不处理扔给下一个,对每个具体的处理子类都有这样的处理逻辑,这样做就达到了不必指定具体的处理者。
- 沿着对象的一条链 传递请求,直到一个对象处理它为止,就想数据结构的链表一样。
结构图解
代码
ChainOfResponsibility职责链模式 c++
#include <iostream>
#include <string>
using namespace std;
//请求枚举类型
enum class RequestType
{
REQ_HANDLER1,
REQ_HANDLER2,
REQ_HANDLER3
};
//请求对象定义
class Reqest
{
string description;
RequestType reqType;
public:
Reqest(const string & desc, RequestType type) : description(desc), reqType(type) {}
RequestType getReqType() const { return reqType; }
const string& getDescription() const { return description; }
};
//职责链基类 定义接口 有个指向自己的指针(运行时灵活指派下一个处理责任主体)
class ChainHandler{
ChainHandler *nextChain;
//将请求发送个链条上下一个处理责任主体
void sendReqestToNextHandler(const Reqest & req)
{
if (nextChain != nullptr)
nextChain->handle(req);
}
protected:
virtual bool canHandleRequest(const Reqest & req) = 0;
virtual void processRequest(const Reqest & req) = 0;
public:
ChainHandler() { nextChain = nullptr; }
void setNextChain(ChainHandler *next) { nextChain = next; }
//公共处理接口
void handle(const Reqest & req)
{
if (canHandleRequest(req))//如果自己能处理 则直接处理请求
processRequest(req);
else
sendReqestToNextHandler(req);//否则传递给下一个处理责任主体
}
};
//实际处理主体1 继承职责链处理基类
class Handler1 : public ChainHandler{
protected:
bool canHandleRequest(const Reqest & req) override
{
return req.getReqType() == RequestType::REQ_HANDLER1;
}
void processRequest(const Reqest & req) override
{
cout << "Handler1 is handle reqest: " << req.getDescription() << endl;
}
};
//实际处理主体2 继承职责链处理基类
class Handler2 : public ChainHandler{
protected:
bool canHandleRequest(const Reqest & req) override
{
return req.getReqType() == RequestType::REQ_HANDLER2;
}
void processRequest(const Reqest & req) override
{
cout << "Handler2 is handle reqest: " << req.getDescription() << endl;
}
};
//实际处理主体3 继承职责链处理基类
class Handler3 : public ChainHandler{
protected:
bool canHandleRequest(const Reqest & req) override
{
return req.getReqType() == RequestType::REQ_HANDLER3;
}
void processRequest(const Reqest & req) override
{
cout << "Handler3 is handle reqest: " << req.getDescription() << endl;
}
};
//调用示意
int main(){
Handler1 h1;
Handler2 h2;
Handler3 h3;
h1.setNextChain(&h2);
h2.setNextChain(&h3);
Reqest req("process task ... ", RequestType::REQ_HANDLER3);//这里看着简单 运行着这个对象可能表复杂 或者比较多,都直接显示指定耦合太多
h1.handle(req);//调用的时候不用指定具体的责任主体
return 0;
}
要点总结
- 职责链模式的应用,让对象职责分派更具有灵活性
- 如果最后一个请求处理主体也不能处理,应该有一个告警或者缺省的处理机制。这也是每一个接受请求对象的责任,而不是发送请求对象的责任
- 当然责任链模式很容易直接用数据结构实现了,所以也是这个模式不怎么流行的原因 比较简答 但思想可以借鉴
命令模式Command—行为变化
需求背景、动机
- 软件构建过程中,“行为请求者”和“行为实现者”通常呈现一种紧耦合(对象和成员函数),但在某些场合,比如要对行为进行“记录、撤销、重做(undo/redo)、事务”等处理,这种紧耦合无法抵御变化。
- 怎么样才能将“行为请求者”与“行为实现者”解耦?
模式定义
- 将一个请求(行为、或者直接说成员函数)封装成一个对象,从而使你可用不同的请求对客户进行参数化,请请求排队或者记录日志、撤销等等。
结构图解
代码
Command命令模式c++
#include <iostream>
#include <vector>
#include <string>
using namespace std;
//命令基类定义接口方法
class Command
{
public:
virtual void execute() = 0;
};
//命令1实现
class ConcreteCommand1 : public Command
{
string arg;
public:
ConcreteCommand1(const string & a) : arg(a) {}
void execute() override
{
cout<< "#1 process..."<<arg<<endl;
}
};
//命令2实现
class ConcreteCommand2 : public Command
{
string arg;
public:
ConcreteCommand2(const string & a) : arg(a) {}
void execute() override
{
cout<< "#2 process..."<<arg<<endl;
}
};
//组合命令实现
class MacroCommand : public Command
{
vector<Command*> commands;
public:
void addCommand(Command *c) { commands.push_back(c); }
void execute() override
{
for (auto &c : commands)
{
c->execute();
}
}
};
int main()
{
//执行某些命令操作就变成了 构造某些对象 然后执行其相应方法
ConcreteCommand1 command1(receiver, "Arg ###");
ConcreteCommand2 command2(receiver, "Arg $$$");
MacroCommand macro;
macro.addCommand(&command1);
macro.addCommand(&command2);
macro.execute();
}
要点总结
- Command模式目的是将“行为请求者”和”行为实现者“解耦,在面向对象语言中,常见的手段是"“将行为抽象为对象”
- 这个模式跟c++的函数对象又些类似,相比Command模式接口更规范,但函数对象使用更加灵活,现代工程函数对象用的更多
- 所谓函数对象就是一个class对象重载了()运算符,所以对象名称加上()就相当于调用了一个函数
访问器模式Visitor—-行为变化
需求背景、动机
- 由于需求的改变、某些类层次结构常常需要增加新的行文(对象成员方法),如果考虑基类子类的实现方式,需要在基类、子类都添加对应的方法,如果直接在基类修改,将会给子类带来繁重的负担,甚至破坏原设计
- 如何在不改变类层次结构的前提下,在运行时透明地为类层次结构的各个类动态的添加新的行为操作?
模式定义
- 表示一个作用于某个对象结构中的各元素的操作,使得可以在不改变(稳定)个元素的类的前提下定义(扩展)作用于这些元素的新操作(行为)
- 采用二次分发的方式实现 但前提是基类的子类非常确定 不会经常变化,是子类的行为经常变化。
结构图解
代码
#include <iostream>
using namespace std;
//业务基类 刚开始有2个方法
class Element
{
public:
virtual void Func1() = 0;
virtual void Func2(int data)=0;
virtual void Func3(int data)=0;//随着项目发展 要不停的增加方法 那么就需要修改基类然后修改每一个子类
//...
virtual ~Element(){}
};
class ElementA : public Element
{
public:
void Func1() override{
//...
}
void Func2(int data) override{
//...
}
//这里添加Func3
};
class ElementB : public Element
{
public:
void Func1() override{
//***
}
void Func2(int data) override {
//***
}
//这里添加Func3 Func4....
};
Visitor访问器模式
#include <iostream>
using namespace std;
//先声明
class Visitor;
//业务基类
class Element
{
public:
virtual void accept(Visitor& visitor) = 0; //第一次多态辨析
virtual ~Element(){}
};
//业务子类A
class ElementA : public Element
{
public:
void accept(Visitor &visitor) override {
visitor.visitElementA(*this);
}
};
//业务子类B
class ElementB : public Element
{
public:
void accept(Visitor &visitor) override {
visitor.visitElementB(*this); //第二次多态辨析
}
};
//访问器基类 定义好访问每个子业务类的接口 后面有各个访问器实现(为每个子类增加成员方法)
class Visitor{
public:
virtual void visitElementA(ElementA& element) = 0;
virtual void visitElementB(ElementB& element) = 0;
virtual ~Visitor(){}
};
//==========================上述如果只有2个子类 随便添加方法 以上稳定==============================
//扩展1 为子类增加一个成员方法
class Visitor1 : public Visitor{
public:
void visitElementA(ElementA& element) override{
cout << "Visitor1 is processing ElementA" << endl;
}
void visitElementB(ElementB& element) override{
cout << "Visitor1 is processing ElementB" << endl;
}
};
//扩展2 为子类增加再一个成员方法
class Visitor2 : public Visitor{
public:
void visitElementA(ElementA& element) override{
cout << "Visitor2 is processing ElementA" << endl;
}
void visitElementB(ElementB& element) override{
cout << "Visitor2 is processing ElementB" << endl;
}
};
//扩展3 为子类增加再一个成员方法
class Visitor3 : public Visitor{};
int main()
{
Visitor2 visitor;
ElementB elementB;
elementB.accept(visitor);// double dispatch 目的实现调用elementB对应Visitor2定义的成员方法
ElementA elementA;
elementA.accept(visitor);// double dispatch 目的实现调用elementA对应Visitor2定义的成员方法
return 0;
}
要点总结
- 通过双重分发机制(double dispatch)来实现在不更新业务类(Element)类层次结构的前提下,运行时透明的为各个业务类增加方法
- 最大缺点 业务类的子类数量要确定 这就导致在现实世界很难用到
- 只能用于哪些子类个数不频繁变化,但是每个子类行为频繁变化的情况
解析器模式Interpreter—领域规则
需求背景、动机
- 如果在某一特定领域的问题比较复杂,类似的结构又不断重复出现,如果使用普遍的变成方式实现将面临非常频繁的变化
- 如果能够将这个特定领域的问题表达为某种语法规则,然后构建一个解析器解释这样的情况,从而解决问题
- 比如要实现一个加减法的表达式问题 a+b-c+d
模式定义
- 给定一个语言,定义的文法表示,并实现一个解释器来解决
结构图解
代码
Interpreter解析器模式c++
#include <iostream>
#include <map>
#include <stack>
using namespace std;
//表达式基类
class Expression {
public:
//主要接口 解析表达式
virtual int interpreter(map<char, int> var)=0;
virtual ~Expression(){}
};
//变量表达式
class VarExpression: public Expression {
char key;
public:
VarExpression(const char& key)
{
this->key = key;
}
//变量表达式的解析就直接返回其内存值即可
int interpreter(map<char, int> var) override {
return var[key];
}
};
//符号表达式基类
class SymbolExpression : public Expression {
// 运算符左右两个参数 抽象语法就是有左右2个表达式
protected:
Expression* left;
Expression* right;
public:
SymbolExpression( Expression* left, Expression* right):
left(left),right(right){
}
};
//加法运算表达式实现
class AddExpression : public SymbolExpression {
public:
AddExpression(Expression* left, Expression* right):
SymbolExpression(left,right){
}
//解析规则就是左右两个表达式的解析值相加 这中间会涉及多态递归调用
int interpreter(map<char, int> var) override {
return left->interpreter(var) + right->interpreter(var);
}
};
//减法运算
class SubExpression : public SymbolExpression {
public:
SubExpression(Expression* left, Expression* right):
SymbolExpression(left,right){
}
//解析规则就是左右两个表达式的解析值相减 这中间会涉及多态递归调用
int interpreter(map<char, int> var) override {
return left->interpreter(var) - right->interpreter(var);
}
};
//定义解析器
Expression* analyse(string expStr) {
stack<Expression*> expStack;
Expression* left = nullptr;
Expression* right = nullptr;
for(int i=0; i<expStr.size(); i++)
{
switch(expStr[i])
{
case '+':
// 加法运算
left = expStack.top();
right = new VarExpression(expStr[++i]);
expStack.push(new AddExpression(left, right));
break;
case '-':
// 减法运算
left = expStack.top();
right = new VarExpression(expStr[++i]);
expStack.push(new SubExpression(left, right));
break;
default:
// 变量表达式
expStack.push(new VarExpression(expStr[i]));
}
}
Expression* expression = expStack.top();
return expression;
}
void release(Expression* expression){
//释放表达式树的节点内存...
}
int main(int argc, const char * argv[]) {
string expStr = "a+b-c+d-e";
map<char, int> var;
var.insert(make_pair('a',5));
var.insert(make_pair('b',2));
var.insert(make_pair('c',1));
var.insert(make_pair('d',6));
var.insert(make_pair('e',10));
Expression* expression= analyse(expStr);//将字符串解析成对象
int result=expression->interpreter(var);//多态调用解析最终值 解决问题
cout<<result<<endl;
release(expression);
return 0;
}
要点总结
- 现实项目中用到的不多 只能解决语法相对简单的问题 否则多态虚函数调用过多会产生很多性能问题
23种设计模式总结
目标不能忘记 管理变化,提高复用
两者主要的手段 分解和抽象
深刻立刻8大原则和重构的5种技巧
设计模式最终可能的对象模型
时刻要注意关注变化点和稳定点
哪些场景不太适合用模式
- 代码可读性很差的时候 不建议直接使用 要首先保证代码有良好的可读性 这个是前提条件
- 需求理解的不深刻时 此时不能深刻理解哪些是稳定点和变化点
- 变化没有显现的时候 也是无法深刻理解变化点的
- 根本就不是系统的关键依赖点 可以不用花大的精力来折腾设计模式
- 项目本身没有任何复用价值 没有复用价值也就不用考虑设计模式了
- 项目即将发布的时候 就不要在折腾设计模式了 别再整出一大堆bug
一些经验之谈
- 不要为了模式而模式
- 要重视抽象类和接口
- 理清楚变化点和稳定点非常重要
- 经常审视类之间的依赖关系非常重要
- Framework和Application的区隔思维要有
- 良好的设计都是慢慢演化的结果 不能一蹴而就
成长之路
- “手中无剑,心中无剑”: 见到模式不认识
- “手中有剑,心中无剑”: 可以识别不同的模式 作为应用开发人员可以使用
- “手中有剑,心中有剑”: 可以作为框架开发人员应用设计模式 让别人用你开发的产品框架
- “手中无剑,心中有剑”: 已经忘记了死板的设计模式,深刻理解原则,甚至能够创造模式