目录
什么是设计模式?
设计模式是指在软件开发中,经过验证的,用于解决在特定环境下,重复出现的,特定问题的解决方案。面向对象设计模式通常以类或者对象来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类别或对象。设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力。
设计模式的基本原则
1. 依赖倒置原则
高层模块不应该依赖低层模块,二者都应该依赖抽象;
抽象不应该依赖具体实现,具体实现应该依赖于抽象;
举个例子,自动驾驶系统公司是高层,汽⻋生产厂商为低层,它们不应该互相依赖,一方变动另一方也会跟着变动;而应该抽象一个自动驾驶行业标准,高层和低层都依赖它;这样以来就解耦了两方的变动;自动驾驶系统、汽⻋生产厂商都是具体实现,它们应该都依赖自动驾驶行业标准(抽象)。
2. 开放封闭原则
一个类应该对扩展开放,对修改关闭。也就是对一个类尽量做扩展而不是修改,扩展一般是用继承或者组合的方式。
3. 面向接口编程
不将变量类型声明为某个特定的具体类,而是声明为某个接口。比如某个类成员是另一个类的具体对象,那么应该改成指向这个类的指针,因为根据多态原理,这个类指针实际上成为了一个接口。
客户程序无需获知对象的具体类型,只需要知道对象所具有的接口。
减少系统中各部分的依赖关系,从而实现“高内聚、松耦合”的类型设计方案。
4. 封装变化点
将稳定点和变化点分离,扩展修改变化点;让稳定点与变化点的实现层次分离。
5. 单一职责原则
一个类应该仅有一个引起它变化的原因。
6. 里式替换原则
子类型必须能够替换掉它的父类型;主要出现在子类覆盖父类实现,原来使用父类型的程序可
能出现错误;覆盖了父类方法却没实现父类方法的职责。也就是子类如果声明了父类存在的函数 就要把功能实现完全,父类有的子类也一定要有。
7. 接口隔离原则
不应该强迫客户依赖于他们不用的方法。
一般用于处理一个类拥有比较多的接口,而这些接口涉及到很多职责。
所以要正确使用public,protected,private这三种权限。
8. 对象组合优于类继承
继承耦合度高,组合耦合度低(继承不能换爹,组合可以换爹)。
如何找到设计模式
从重构中获得:
1. 静态转变为动态
2. 早绑定转为晚绑定
3. 继承转为组合
4. 编译时依赖转变为运行时依赖
5. 紧耦合转变为松耦合
掌握设计模式的原则比具体的设计模式更重要,只有当你对业务非常熟悉的时候,才能信手拈来的写出适合的设计模式。
常见的设计模式和使用场景
1. 模板模式
定义一个操作中的算法的⻣架 ,而将一些步骤延迟到子类中。 Template Method使得子类可以不 改变一个算法的结构即可重定义该算法的某些特定步骤。
下面来看一个例子:
class IGame
{
public:
Play()
{
Process1();
Process2();
Process3();
Process4();
}
protected:
virtual void Process1(){}
virtual void Process2(){}
virtual void Process3(){}
virtual void Process4(){}
};
class Game1:public IGame
{
protected:
virtual void Process2(){}
virtual void Process4(){}
};
class Game2:public IGame
{
protected:
virtual void Process1(){}
virtual void Process3(){}
};
某个游戏IGame类有固定的流程,然后Game1和Game2分别继承自IGame,但是在具体某个步骤上各有不同,因此将Process1~4写成虚函数,由各个子类自己去实现。
这样写的前提当然是主框架流程是固定的,Play函数对外开放给用户使用,但是具体流程的函数是protected,提供子类去修改。这体现了接口隔离原则。
流程的接口在基类都抽象成了虚函数,由各个子类具体实现,这体现了依赖倒置原则。
2. 观察者模式
观察者模式在分布式架构中应用广泛,比如actor框架,skynet,redis,zoomkeeper,订阅和发布等。
观察者模式定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有 依赖于它的对象都得到通知并自动更新。
// 终端基类
class ITerminal
{
public:
virtual void show(int data){}
};
class DataCenter
{
public:
void Attach(ITerminal* ob){} // 注册绑定
void Detach(ITerminal* ob){} // 解绑
void Notify()
{
for(auto it = obs.begin(); it != obs.end(); ++it)
{
it->show();
}
}
protected:
virtual int GetData(){return 0;} // 获取数据
private:
std::vector<ITerminal*> obs;
};
class Terminal1:public ITerminal
{
public:
void show(int data){}
}
class Terminal2:public ITerminal
{
public:
void show(int data){}
}
上面是一个发布和订阅的简单例子,假设有个数据中心提供数据给不同的终端展示,数据中心是一个稳定点,不同的终端设备是变化点,因此将终端封装成基类,提供一个展示的接口,不同的设备继承去实现不同的展示方式。
数据中心用一个vector或者set把注册过来的终端记录起来,需要展示的时候通过notify接口逐个通知展示。这样数据中心不必关注具体有哪些设备订阅了数据,是否订阅的操作交给了各个终端设备上。
3. 策略模式
定义一系列算法,把它们一个个封装起来,并且使它们可互相替换。该模式使得算法可独立于使用 它的客户程序而变化。
策略模式提供了一系列可重用的算法,从而可以使得类型在运行时方便地根据需要在各个算法 之间进行切换; 策略模式消除了条件判断语句;就是在解耦合; 充分体现了开闭原则;单一职责; 其本质是分离算法,选择实现。
来看一个简单的例子,假设某电商网站一件商品在不同节假日的价格算法使不同的,可能会这么写:
enum VacationEnum
{
chunjie, // 春节
wuyi; // 五一
qixi; // 七夕
guoqing; // 国庆
};
class Context
{
};
class Promotion
{
VacationEnum vac;
public:
double calc()
{
if(vac == chunjie){}
else if(vac == wuyi){}
else if(vac == qixi){}
else if(vac == guoqing){}
}
};
这样写并不符合设计模式的单一职责原则和开放封闭原则。所以可以改成这样:
class Context
{};
// 变化的 扩展的
class BaseVac
{
public:
virtual calc(Context &ctx){}
};
class Vacchunjie: public BaseVac
{
public:
virtual calc(Context &ctx){}
};
class Vacwuyi: public BaseVac
{
public:
virtual calc(Context &ctx){}
};
class Vacxiqi: public BaseVac
{
public:
virtual calc(Context &ctx){}
};
// 稳定的 单一职责的
class Promotion
{
public:
Promotion(BaseVac *ss):s(ss){}
double calc(Context &ctx)
{
s->calc(ctx);
}
private:
BaseVac *s;
};
int main()
{
BaseVac *v = new Vacxiqi();
Promotion *p = new Promotion(v);
return 0;
}
把变化点(不同节假日的不同算法)隔离出去,只保留单一的职责:获取价格。
这样做非常符合设计模式的原则,但是前提是对业务非常熟悉,因为别人来扩展维护的话,必须先了解之前已有的扩展有哪些。
4. 责任链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成 一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
这个模式的一些要点:
a.解耦请求方和处理方,请求方不知道请求是如何被处理,处理方的组成是由相互独立的子处理构成,子处理流程通过链表的方式连接,子处理请求可以按任意顺序组合;
b.责任链请求强调请求最终由一个子处理流程处理;通过了各个子处理条件判断;
c.责任链扩展就是功能链,功能链强调的是,一个请求依次经由功能链中的子处理流程处理;
d.充分体现了单一职责原则;将职责以及职责顺序运行进行抽象,那么职责变化可以任意扩展, 同时职责顺序也可以任意扩展;
本质是分离职责,动态组合。
责任链模式是比较过时的,在c语言里用的比较多,c++里有更好的数据结构(list)可以代替这个模式。
来看一个具体例子,公司请假审批规则是小于3天只给人事审批,大于3天且小于5天给经理审批,5天以上的给老板审批。下面是第一个版本的代码:
class Context
{
public:
std::string name;
int day;
};
class LeaveRequest
{
public:
void HandleRequest(const Context &cxt)
{
if(cxt.day < 3)
{
HandleByHR(cxt);
}
else if(cxt.day < 5);
{
HandleByMgr(cxt);
}
else
{
HandleByBoss(cxt);
}
}
private: // 接口隔离原则
void HandleByHR(const Context &cxt){}
void HandleByMgr(const Context &cxt){}
void HandleByBoss(const Context &cxt){}
};
怎么重构这个代码,从封装变化点的原则来看,先把HandleRequest里的各种条件拆分出来,然后新增一个类成员指针指向上一级,子类只需要判断自己是否应该处理,不能处理的就通过指针转移到上级去处理。
class Context
{
public:
std::string name;
int day;
};
class IHandler
{
public:
virtual ~ IHandler(){}
void SetNext(IHandler *n){bext = n;}
virtual bool HandleRequest(const Context &cxt)
{
if(CanHandle(cxt))
{
}
else if(GetNext())
{
GetNext()-> HandleRequest(cxt);
}
else
{
// error
}
}
protected:
virtual bool CanHandle(const Context &cxt){}
IHandler * GetNext(){return next;}
private:
IHandler *next;
};
class HandleByMainProgram : public IHandler {
protected:
virtual bool HandleRequest(const Context &ctx){
//
}
virtual bool CanHandle() {
//
}
};
class HandleByProjMgr : public IHandler {
protected:
virtual bool HandleRequest(const Context &ctx){
//
}
virtual bool CanHandle() {
//
}
};
class HandleByBoss : public IHandler {
public:
virtual bool HandleRequest(const Context &ctx){
//
}
protected:
virtual bool CanHandle() {
//
}
};
int main () {
IHandler * h1 = new MainProgram();
IHandler * h2 = new HandleByProjMgr();
IHandler * h3 = new HandleByBoss();
h1->SetNextHandler(h2);
h2->SetNextHandler(h3);
Context ctx;
h1->handle(ctx);
return 0;
}
把不同部门的职责拆出来作为子类继承,主流程封装进基类里,上面这个版本同时使用了模板模式和责任链模式。
子类的接口都是protected的,这体现了接口隔离原则和依赖倒置原则。
责任链模式可以变形成功能链模式,比如nginx阶段处理,不同之处在于责任链每次只有一个模块会处理,功能链是按顺序执行不同的模块。
5. 装饰器模式
动态地给一个对象增加一些额外的职责。就增加功能而言,装饰器模式比生成子类更为灵活。
这个模式看起来会和责任链模式很像。
通过采用组合而非继承的手法, 装饰器模式实现了在运行时动态扩展对象功能的能力,而且可以根据需要扩展多个功能。 避免了使用继承带来的“灵活性差”和“多子类衍生问题”。 不是解决“多子类衍生的多继承”问题,而是解决“父类在多个方向上的扩展功能”问题; 装饰器模式把一系列复杂的功能分散到每个装饰器当中,一般一个装饰器只实现一个功能,实现复用装饰器的功能;
装饰器模式本质是动态组合。
举个例子,普通员工有销售奖金,累计奖金,部⻔经理除此之外还有团队奖金;后面可能会添加环比增⻓奖 金,同时可能针对不同的职位产生不同的奖金组合。那么代码可以这样写:
class Context {
public:
bool isMgr;
// User user;
// double groupsale;
};
class Bonus {
public:
double CalcBonus(Context &ctx) {
double bonus = 0.0;
bonus += CalcMonthBonus(ctx);
bonus += CalcSumBonus(ctx);
if (ctx.isMgr) {
bonus += CalcGroupBonus(ctx);
}
return bonus;
}
private:
double CalcMonthBonus(Context &ctx) {
double bonus/* = */;
return bonus;
}
double CalcSumBonus(Context &ctx) {
double bonus/* = */;
return bonus;
}
double CalcGroupBonus(Context &ctx) {
double bonus/* = */;
return bonus;
}
};
int main() {
Context ctx;
// 设置 ctx
Bonus *bonus = new Bonus;
bonus->CalcBonus(ctx);
}
重构成装饰器模式后的代码:
class Context {
public:
bool isMgr;
// User user;
// double groupsale;
};
// 试着从职责出发,将职责抽象出来
class CalcBonus {
public:
CalcBonus(CalcBonus * c = nullptr) {}
virtual double Calc(Context &ctx) {
return 0.0; // 基本工资
}
virtual ~CalcBonus() {}
protected:
CalcBonus* cc;
};
class CalcMonthBonus : public CalcBonus {
public:
CalcMonthBonus(CalcBonus * c) : cc(c) {}
virtual double Calc(Context &ctx) {
double mbonus /*= 计算流程忽略*/;
return mbonus + cc->Calc(ctx);
}
};
class CalcSumBonus : public CalcBonus {
public:
CalcSumBonus(CalcBonus * c) : cc(c) {}
virtual double Calc(Context &ctx) {
double sbonus /*= 计算流程忽略*/;
return sbonus + cc->Calc(ctx);
}
};
class CalcGroupBonus : public CalcBonus {
public:
CalcGroupBonus(CalcBonus * c) : cc(c) {}
virtual double Calc(Context &ctx) {
double gbnonus /*= 计算流程忽略*/;
return gbnonus + cc->Calc(ctx);
}
};
class CalcCycleBonus : public CalcBonus {
public:
CalcGroupBonus(CalcBonus * c) : cc(c) {}
virtual double Calc(Context &ctx) {
double gbnonus /*= 计算流程忽略*/;
return gbnonus + cc->Calc(ctx);
}
};
int main() {
// 1. 普通员工
Context ctx1;
CalcBonus *base = new CalcBonus();
CalcBonus *cb1 = new CalcMonthBonus(base);
CalcBonus *cb2 = new CalcSumBonus(cb1);
cb2->Calc(ctx1);
// 2. 部门经理
Context ctx2;
CalcBonus *cb3 = new CalcGroupBonus(cb2);
cb3->Calc(ctx2);
}
在计算奖金的形式上,有点像递归,因为构造函数里设置的基类成员是给用户自己设置的,可以 灵活组合。而不是第一个版本那样用ifelse去判断是否要添加奖金 ,这样也很容易扩展和修改。
打个比方,责任链的模式看起来就像一个链表的结构,而装饰器模式看起来像一个俄罗斯套娃。
6. 单例模式
保证一个类仅有一个实例,并提供一个该实例的全局访问点。
先了解一个知识点,由C/C++编译的程序占用的内存分为以下几个部分:
1、栈区(stack): 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其
操作方式类似于数据结构中的栈。
2、堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束后由系统释放。
4、文字常量区 :常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区:存放函数体的二进制代码。
下面来看看几种实现单例模式的例子。
版本一:
#include <mutex>
class Singleton
{
// 懒汉模式 lazy load public:
static Singleton * GetInstance()
{
//std::lock_guard<std::mutex> lock(_mutex); // 这里加锁粒度太大 切换线程开销大
if (_instance == nullptr)
{
std::lock_guard<std::mutex> lock(_mutex); // 在这里加锁
if (_instance == nullptr) // 双重判断
{
_instance = new Singleton();
atexit(Destructor);
}
}
return _instance;
}
private:
static void Destructor()
{
if (nullptr != _instance)
{
delete _instance;
_instance = nullptr;
}
}
Singleton(){} //构造
Singleton(const Singleton &cpy){} //拷⻉构造
Singleton& operator=(const Singleton&) {}
static Singleton * _instance;
static std::mutex _mutex;
};
Singleton* Singleton::_instance = nullptr;//静态成员需要初始化
std::mutex Singleton::_mutex; //互斥锁初始化
这个例子有几个需要注意的点,第一个是加锁的粒度,当单例指针是空的时候再去加锁,减少锁粒度可以减少线程切换的开销。第二个是双重判断,防止多线程重入导致多次分配的问题。第三这是个懒汉模式,用到的时候才决定分配初始化。
但是这个例子有个问题。
c++的new操作里分为了三个步骤:分配内存,调用构造函数,赋值操作。在多线程环境下,cpu会进行指令重排,前面三个步骤可能执行起来是132的顺序。所以可能会出现某个线程判断单例指针不为空,但是还没调用构造函数,就立即返回,最后导致程序崩溃。所以上面这个例子还是有问题的。(如果在判断前就加锁是没问题的,代价是增加了开销)
版本二:
#include <mutex>
#include <atomic>
class Singleton
{
public:
static Singleton * GetInstance()
{
Singleton* tmp = _instance.load(std::memory_order_relaxed); //取出原子对象
std::atomic_thread_fence(std::memory_order_acquire);//获取内存屏障
if (tmp == nullptr)
{
std::lock_guard<std::mutex> lock(_mutex);
tmp = _instance.load(std::memory_order_relaxed);
if (tmp == nullptr)
{
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);//释放内存屏障
_instance.store(tmp, std::memory_order_relaxed);// 赋值给原子对象
atexit(Destructor);
}
}
return tmp;
}
private:
static void Destructor()
{
Singleton* tmp = _instance.load(std::memory_order_relaxed);
if (nullptr != tmp)
{
delete tmp;
}
}
Singleton(){}
Singleton(const Singleton&) {}
Singleton& operator=(const Singleton&) {}
static std::atomic<Singleton*> _instance;
static std::mutex _mutex;
};
std::atomic<Singleton*> Singleton::_instance;//原子对象 静态成员需要初始化 std::mutex Singleton::_mutex; //互斥锁初始化
// g++ Singleton.cpp -o singleton -std=c++11
上面代码用c++11特性内存屏障来解决cpu指令重排的问题。
版本三:
class Singleton
{
public:
~Singleton(){}
static Singleton& GetInstance()
{
static Singleton instance;
return instance;
}
private:
Singleton(){}
Singleton(const Singleton&) {}
Singleton& operator=(const Singleton&) {}
};
// g++ Singleton.cpp -o singleton -std=c++11
这个版本需要在c++11或者更高级版本的编译后才能用。
c++11 magic static 特性:如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。这就解决了多线程场景带来的问题。
该版本的优点:
a. 利用静态局部变量特性,延迟加载;
b. 利用静态局部变量特性,系统自动回收内存,自动调用析构函数;
c. 静态局部变量初始化时,没有 new 操作带来的cpu指令reorder操作;
d. c++11 静态局部变量初始化时,具备线程安全;
版本四:
template<typename T>
class Singleton
{
public:
static T& GetInstance()
{
static T instance; // 这里要初始化DesignPattern,需要调用DesignPattern 构造函数,同时会调用父类的构造函数。
return instance;
}
protected:
virtual ~Singleton() {}
Singleton() {} // protected修饰构造函数,才能让别人继承 Singleton(const Singleton&) {}
Singleton& operator =(const Singleton&) {}
};
class DesignPattern : public Singleton<DesignPattern>
{
friend class Singleton<DesignPattern>; // friend 能让 Singleton<T> 访 问到 DesignPattern构造函数
private:
DesignPattern(){}
DesignPattern(const DesignPattern&) {}
DesignPattern& operator=(const DesignPattern&) {}
}
这个版本将单例模式封装成了模板,需要注意两个地方:
a.单例模板类的构造函数用protected修饰是为了它本身能被继承。
b. 单例模板里的GetInstance接口要获取类型T的构造函数,因此继承这个模板的类必须声明模板类为友元。
7. 工厂方法模式
定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method使得一个类的实例化 延迟到子类。
一般这些业务会使用工厂模式:解决创建过程比较复杂,希望对外隐藏这些细节;比如连接池,线程池; 隐藏对象真实类型; 对象创建会有很多参数来决定如何创建; 创建对象有复杂的依赖关系;
线程池的初始化需要由机器的内核数来决定线程数量,但是用户并不需要关心内核数,用户只需要获取到可用的线程就行。因此可以用工厂模式。
其本质是延迟到子类来选择实现;
举个例子,实现一个导出数据的接口,让客户选择数据的导出方式:
#include <string>
// 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv
class IExport {
public:
virtual bool Export(const std::string &data) = 0;
virtual ~IExport(){}
};
class ExportXml : public IExport {
public:
virtual bool Export(const std::string &data) {
return true;
}
};
class ExportJson : public IExport {
public:
virtual bool Export(const std::string &data) {
return true;
}
};
class ExportTxt : public IExport {
public:
virtual bool Export(const std::string &data) {
return true;
}
};
// =====1
int main() {
std::string choose/* = */;
if (choose == "txt") {
IExport *e = new ExportTxt();
e->Export("hello world");
} else if (choose == "json") {
IExport *e = new ExportJson();
e->Export("hello world");
} else if (choose == "xml") {
IExport *e = new ExportXml();
e->Export("hello world");
}
}
对于数据导出有多种不同格式的类,在业务代码层面上,第一个版本里需要使用者区分判断多种格式,我们希望把这种复杂的创建操作封装起来,因此创建一个工厂类,这个类绑定了导出数据的操作和指向具体数据格式的类成员,使用者只需要创建某个具体的工厂子类,调用导出的接口就完成任务了。
重构后的代码:
#include <string>
// 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv
class IExport {
public:
virtual bool Export(const std::string &data) = 0;
virtual ~IExport(){}
};
class ExportXml : public IExport {
public:
virtual bool Export(const std::string &data) {
return true;
}
};
class ExportJson : public IExport {
public:
virtual bool Export(const std::string &data) {
return true;
}
};
class ExportTxt : public IExport {
public:
virtual bool Export(const std::string &data) {
return true;
}
};
class ExportCSV : public IExport {
public:
virtual bool Export(const std::string &data) {
return true;
}
};
class IExportFactory {
public:
IExportFactory() {
_export = nullptr;
}
virtual ~IExportFactory() {
if (_export) {
delete _export;
_export = nullptr;
}
}
bool Export(const std::string &data) {
if (_export == nullptr) {
_export = NewExport();
}
return _export->Export(data);
}
protected:
virtual IExport * NewExport(/* ... */) = 0;
private:
IExport* _export;
};
class ExportXmlFactory : public IExportFactory {
protected:
virtual IExport * NewExport(/* ... */) {
// 可能有其它操作,或者许多参数
IExport * temp = new ExportXml();
// 可能之后有什么操作
return temp;
}
};
class ExportJsonFactory : public IExportFactory {
protected:
virtual IExport * NewExport(/* ... */) {
// 可能有其它操作,或者许多参数
IExport * temp = new ExportJson;
// 可能之后有什么操作
return temp;
}
};
class ExportTxtFactory : public IExportFactory {
protected:
IExport * NewExport(/* ... */) {
// 可能有其它操作,或者许多参数
IExport * temp = new ExportTxt;
// 可能之后有什么操作
return temp;
}
};
class ExportCSVFactory : public IExportFactory {
protected:
virtual IExport * NewExport(/* ... */) {
// 可能有其它操作,或者许多参数
IExport * temp = new ExportCSV;
// 可能之后有什么操作
return temp;
}
};
int main () {
IExportFactory *factory = new ExportTxtFactory();
factory->Export("hello world");
return 0;
}
8. 抽象工厂模式
提供一个接口,让该接口负责创建一系列“相关或者相互依赖的对象”,无需指定它们具体的类。
抽象工厂类是基于工厂类实现的,实际上是一回事,抽象工厂类拥有更多的更复杂的操作接口,但是对于使用者来说,仍然只需要创建时指定某个子工厂类,无需关心具体的操作内容。
举个例子,实现一个拥有导出导入数据的接口,让客户选择数据的导出导入方式:
#include <string>
// 实现导出数据的接口, 导出数据的格式包含 xml,json,文本格式txt 后面可能扩展excel格式csv
class IExport {
public:
virtual bool Export(const std::string &data) = 0;
virtual ~IExport(){}
};
class ExportXml : public IExport {
public:
virtual bool Export(const std::string &data) {
return true;
}
};
class ExportJson : public IExport {
public:
virtual bool Export(const std::string &data) {
return true;
}
};
class ExportTxt : public IExport {
public:
virtual bool Export(const std::string &data) {
return true;
}
};
class ExportCSV : public IExport {
public:
virtual bool Export(const std::string &data) {
return true;
}
};
class IImport {
public:
virtual bool Import(const std::string &data) = 0;
virtual ~IImport(){}
};
class ImportXml : public IImport {
public:
virtual bool Import(const std::string &data) {
return true;
}
};
class ImportJson : public IImport {
public:
virtual bool Import(const std::string &data) {
return true;
}
};
class ImportTxt : public IImport {
public:
virtual bool Import(const std::string &data) {
return true;
}
};
class ImportCSV : public IImport {
public:
virtual bool Import(const std::string &data) {
// ....
return true;
}
};
class IDataApiFactory {
public:
IDataApiFactory() {
_export = nullptr;
_import = nullptr;
}
virtual ~IDataApiFactory() {
if (_export) {
delete _export;
_export = nullptr;
}
if (_import) {
delete _import;
_import = nullptr;
}
}
bool Export(const std::string &data) {
if (_export == nullptr) {
_export = NewExport();
}
return _export->Export(data);
}
bool Import(const std::string &data) {
if (_import == nullptr) {
_import = NewImport();
}
return _import->Import(data);
}
protected:
virtual IExport * NewExport(/* ... */) = 0;
virtual IImport * NewImport(/* ... */) = 0;
private:
IExport *_export;
IImport *_import;
};
class XmlApiFactory : public IDataApiFactory {
protected:
virtual IExport * NewExport(/* ... */) {
// 可能有其它操作,或者许多参数
IExport * temp = new ExportXml;
// 可能之后有什么操作
return temp;
}
virtual IImport * NewImport(/* ... */) {
// 可能有其它操作,或者许多参数
IImport * temp = new ImportXml;
// 可能之后有什么操作
return temp;
}
};
class JsonApiFactory : public IDataApiFactory {
protected:
virtual IExport * NewExport(/* ... */) {
// 可能有其它操作,或者许多参数
IExport * temp = new ExportJson;
// 可能之后有什么操作
return temp;
}
virtual IImport * NewImport(/* ... */) {
// 可能有其它操作,或者许多参数
IImport * temp = new ImportJson;
// 可能之后有什么操作
return temp;
}
};
class TxtApiFactory : public IDataApiFactory {
protected:
virtual IExport * NewExport(/* ... */) {
// 可能有其它操作,或者许多参数
IExport * temp = new ExportTxt;
// 可能之后有什么操作
return temp;
}
virtual IImport * NewImport(/* ... */) {
// 可能有其它操作,或者许多参数
IImport * temp = new ImportTxt;
// 可能之后有什么操作
return temp;
}
};
class CSVApiFactory : public IDataApiFactory {
protected:
virtual IExport * NewExport(/* ... */) {
// 可能有其它操作,或者许多参数
IExport * temp = new ExportCSV;
// 可能之后有什么操作
return temp;
}
virtual IImport * NewImport(/* ... */) {
// 可能有其它操作,或者许多参数
IImport * temp = new ImportCSV;
// 可能之后有什么操作
return temp;
}
};
int main () {
IDataApiFactory *factory = new CSVApiFactory();
factory->Import("hello world");
factory->Export("hello world");
return 0;
}
9. 适配器模式
将一个类的接口转换成客户希望的另一个接口。Adapter模式使得原本由于接口不兼容而不能一起 工作的那些类可以一起工作。
原来的接口是稳定的,新的外来的需求是变化的,那么可以通过继承原来的接口,让原来的接 口继续保持稳定,在子类通过组合的方式来扩展功能。
其本质是转换匹配,复用功能。
举个日志系统的例子,原来是通过写磁盘的方式进行存储,后来因为查询不便,需要额外添加往数据库写日志 的功能(写文件和数据库并存):
#include <string>
#include <vector>
using namespace std;
class LogSys {
public:
LogSys() {}
void WriteLog(const vector<string> &) {
// ... 日志id 时间戳 服务器id 具体日志内容 roleid
}
vector<string>& ReadLog() {
// ...
vector<string> data /* = ...*/;
return data;
}
};
class DB; // 面向接口编程 而不是具体类 强依赖 耦合性高 mysql mongo
class LogSysEx : public LogSys {
public:
LogSysEx(DB *db) : _db(db) {}
void AddLog(const vector<string> &data) {
LogSys::WriteLog(data);
/*
这里调用 _db 的方法将 data 数据存储到数据库
*/
}
void DelLog(const int logid) {
vector<string>& data = LogSys::ReadLog();
// 从 vector<string> 中删除 logid的日志
LogSys::WriteLog(data);
// 调用 _db 的方法将 logid的日志删除
}
void UpdateLog(const int logid, const string &udt) {
vector<string>& data = LogSys::ReadLog();
// 从 vector<string> 中更新 logid的日志 udt
LogSys::WriteLog(data);
// 调用 _db 的方法将 logid的日志更改
}
string& LocateLog(const int logid) {
vector<string>& data = LogSys::ReadLog();
string log1 /* = from log file*/;
string log2 /* = from db */;
string temp = log1 + ";" + log2;
return temp;
}
private:
DB* _db;
};
咱就是说在对一个类进行扩展修改的时候,尽量不要动原来的代码,而是通过继承组合的方式创建新的类,在这个新的类里实现新功能和包含旧功能。
10. 代理模式
代理模式是为其他对象提供一种代理以控制对这对象的访问。
比如远程代理(隐藏一个对象存在不同的地址空间的事实),虚代理(延迟加载lazyload),保护 代理(在代理前后做额外操作,权限管理,引用计数等); 在分布式系统中,actor模型(skynet)等会常用到代理模式。
其本质是控制对象访问。
举个例子,在有些系统中,为了某些对象的纯粹性,只进行了功能相关封装(稳定点),后期添加了其他功能 需要对该对象进行额外操作(变化点),为了隔离变化点(也就是不直接在稳定点进行修改,这样 会让稳定点也变得不稳定),可以抽象一层代理层:
class ISubject {
public:
virtual void Handle() = 0;
virtual ~ISubject() {}
};
// 该类在当前进程,也可能在其他进程当中
class RealSubject : public ISubject {
public:
virtual void Handle() {
// 只完成功能相关的操作,不做其他模块的判断
}
};
// 在当前进程当中 只会在某个模块中使用
class Proxy1 : public ISubject {
public:
Proxy1(ISubject *subject) : _subject(subject) {}
virtual void Handle() {
// 在访问 RealSubject 之前做一些处理
//if (不满足条件)
// return;
_subject->Handle();
count++;
// 在访问 RealSubject 之后做一些处理
}
private:
ISubject* _subject;
static int count;
};
int Proxy1::count = 0;
// 在分布式系统当中 skynet actor
class Proxy2 : public ISubject {
public:
virtual void Handle() {
// 在访问 RealSubject 之前做一些处理
// 发送到数据到远端 网络处理 同步非阻塞 ntyco c协程
//IResult * val = rpc->call("RealSubject", "Handle");
// 在访问 RealSubject 之后做一些处理
}
private:
/*void callback(IResult * val) {
// 在访问 RealSubject 之后做一些处理
}*/
};
上面代码中proxy类可以继承ISubject类也可以不继承,写成继承的样式是为了让程序员明白这是该类的一个代理模块,如果不是继承的,看起来会和适配器模式很像。
proxy类中的基类指针可以改成一个容器,管理多个对象,这在游戏服务器的网关里经常看到。
以上就是c++常见的设计模式,个人感觉单例模式的多个版本实现都很巧妙,需要记住,其他模式一般都是基于对业务流程十分熟悉,并且在重构的基础上才能挑选出合适的模式。了解和掌握各种设计模式,对我们阅读别人的代码或者自己写代码都十分有帮助。
推荐书籍
《设计模式-可复用面向对象软件的基础》
《重构与模式》