C++中的常用设计模式
目录
- 1. 简单工厂模式(Simple Factory Pattern)
- 2 工厂方法模式(Factory Method Pattern)
- 3 抽象工厂模式(Abstract Factory)
- 4 单例模式
- 5 IMPL模式
相关资源:
- 图说设计模式:https://design-patterns.readthedocs.io/zh_CN/latest/index.html
- github:https://github.com/me115/design_patterns/blob/master/index.rst
本博客主要用于学习C++中常用的几种设计模,设计模式提供经过验证的解决方案,促进代码的重用和可维护性,减少了重复代码,提高了代码的可扩展性和可维护性,非常值得学习。后面将通过伪代码的形式,简述各种设计模式的优缺点。
ps:为了简化学习,深入理解设计模式,这里不考虑代码规范,内存管理等方面的优化。
1. 简单工厂模式(Simple Factory Pattern)
1.1 动机
假设一个简单的应用场景,一个项目需要提供多个检测算法,算法在不同地区检测的目标不一样, 这些算法都源自同一个基类,不过在继承基类后不同的子类修改了部分属性从而使得它们可以检测不同的目标。
如果我们希望用户在使用这些算法时,不需要知道这些具体算法的实现,只需要知道表示该算法类的一个参数,并提供一个调用方便的方法,把该参数传入方法即可返回一个相应的算法对象,此时,就可以使用简单工厂模式。
1.2 结构
简单工厂模式包含如下角色:
- Factory:工厂角色工厂角色负责实现创建所有实例的内部逻辑
- Product:抽象产品角色抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口
- ConcreteProduct:具体产品角色具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。
1.3 实例
1.3.1 main
用户(main)带着需求(“A")通过工厂(Factory)获得产品(Product)
#include <iostream>
#include "Factory.h"
#include "Product.h"
using namespace std;
int main(int argc, char *argv[])
{
Product * prod = Factory::createProduct("A");
prod->Use();
delete prod;
return 0;
}
1.3.2 Factory
Factory根据main的参数生产Product
Factory.h
#if !defined(EA_4C08AF19_2960_47a1_B769_9B60CFA50FE0__INCLUDED_)
#define EA_4C08AF19_2960_47a1_B769_9B60CFA50FE0__INCLUDED_
#include "Product.h"
#include <string>
using namespace std;
class Factory
{
public:
Factory();
virtual ~Factory();
static Product * createProduct(string proname);
};
#endif // !defined(EA_4C08AF19_2960_47a1_B769_9B60CFA50FE0__INCLUDED_)
Factory.cpp
#include "Factory.h"
#include "ConcreteProductA.h"
#include "ConcreteProductB.h"
Factory::Factory(){
}
Factory::~Factory(){
}
Product* Factory::createProduct(string proname){
if ( "A" == proname )
{
return new ConcreteProductA();
}
else if("B" == proname)
{
return new ConcreteProductB();
}
return NULL;
}
1.3.3 Product
为了能让工厂返回的不同产品能被用户使用,需要利用多态性质设计公共接口。
为了能让工厂返回不同的产品,需要单独对每个产品的公共接口进行设计。
基类产品Product:
Product.h (抽象类)
#if !defined(EA_9126430A_5CDD_4424_AA90_549F255E0D2D__INCLUDED_)
#define EA_9126430A_5CDD_4424_AA90_549F255E0D2D__INCLUDED_
class Product
{
public:
Product();
virtual ~Product();
virtual void Use() = 0;
};
#endif // !defined(EA_9126430A_5CDD_4424_AA90_549F255E0D2D__INCLUDED_)
Product.cpp
#include "Product.h"
Product::Product(){
}
Product::~Product(){
}
基于Product,可以设计A产品和B产品
ConcreteProductA.h A产品实现
#if !defined(EA_7A08FFBB_5AD4_4f9c_AE53_998AC5E88F34__INCLUDED_)
#define EA_7A08FFBB_5AD4_4f9c_AE53_998AC5E88F34__INCLUDED_
#include "Product.h"
class ConcreteProductA : public Product
{
public:
ConcreteProductA();
virtual ~ConcreteProductA();
virtual void Use();
};
#endif // !defined(EA_7A08FFBB_5AD4_4f9c_AE53_998AC5E88F34__INCLUDED_)
ConcreteProductA.cpp
#include "ConcreteProductA.h"
#include <iostream>
#include <string>
using namespace std;
ConcreteProductA::ConcreteProductA(){
}
ConcreteProductA::~ConcreteProductA(){
}
void ConcreteProductA::Use()
{
cout << "use productB" << endl;
}
ConcreteProductB.h B产品实现
#if !defined(EA_52558B6D_9609_4377_944C_C57B380F7229__INCLUDED_)
#define EA_52558B6D_9609_4377_944C_C57B380F7229__INCLUDED_
#include "Product.h"
class ConcreteProductB : public Product
{
public:
ConcreteProductB();
virtual ~ConcreteProductB();
virtual void Use();
};
#endif // !defined(EA_52558B6D_9609_4377_944C_C57B380F7229__INCLUDED_)
ConcreteProductB.cpp
#include "ConcreteProductB.h"
#include <iostream>
#include <string>
using namespace std;
ConcreteProductB::ConcreteProductB(){
}
ConcreteProductB::~ConcreteProductB(){
}
void ConcreteProductB::Use()
{
cout << "use productB" << endl;
}
至此,用户(main)可以根据自己提供的参数(“A”),从工厂(Factory)里面,获得自己想要的产品(Product),并实现功能(use)。
1.4 总结
-
创建型模式对类的实例化过程进行了抽象,能够将对象的创建与对象的使用过程分离。
-
简单工厂模式又称为静态工厂方法模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
-
简单工厂模式包含三个角色:工厂角色负责实现创建所有实例的内部逻辑;抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口;具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。
-
简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。
-
简单工厂模式最大的优点在于实现对象的创建和对象的使用分离,将对象的创建交给专门的工厂类负责,但是其最大的缺点在于工厂类不够灵活,增加新的具体产品需要修改工厂类的判断逻辑代码,而且产品较多时,工厂方法代码将会非常复杂。
举个例子,如果有10个产品,则工厂方法部分实现如下:
Product* Factory::createProduct(string proname){ if ( "A" == proname ) { return new ConcreteProductA(); } else if("B" == proname) { return new ConcreteProductB(); } else if("C" == proname) { return new ConcreteProductB(); } else if("D" == proname) { return new ConcreteProductB(); } else if("E" == proname) { return new ConcreteProductB(); } else if("F" == proname) { return new ConcreteProductB(); } else if("G" == proname) { return new ConcreteProductB(); } else if("H" == proname) { return new ConcreteProductB(); } return NULL; }
可以看到,维护代码将会变得非常冗余,且每次增加一个产品,就要编译一下Factory。
-
简单工厂模式适用情况包括:工厂类负责创建的对象比较少;客户端只知道传入工厂类的参数,对于如何创建对象不关心。
2 工厂方法模式(Factory Method Pattern)
2.1 动机
现在对该项目进行修改,不再设计一个算法工厂类来统一负责所有产品的创建,而是将具体算法的创建过程交给专门的工厂子类去完成,我们先定义一个抽象的算法工厂类,再定义具体的工厂类来生成螺栓检测算法,异物检测算法等,它们实现在抽象算法工厂类中定义的方法。
这种抽象化的结果使这种结构可以在不修改具体工厂类的情况下引进新的产品,如果出现新的检测类型,只需要为这种新类型的算法创建一个具体的工厂类就可以获得该新算法的实例,这一特点无疑使得工厂方法模式具有超越简单工厂模式的优越性,更加符合“开闭原则”。
2.2 结构
工厂方法模式包含如下角色:
- Product:抽象产品
- ConcreteProduct:具体产品
- Factory:抽象工厂
- ConcreteFactory:具体工厂
2.3实例
2.3.1 main
用户(main)根据自己的需求,联系工厂(Factory)生产新产品(Product)
#include "Factory.h"
#include "ConcreteFactory.h"
#include "Product.h"
#include <iostream>
using namespace std;
int main(int argc, char *argv[])
{
Factory * fc = new ConcreteFactory();
Product * prod = fc->factoryMethod();
prod->use();
delete fc;
delete prod;
return 0;
}
2.3.2 工厂
工厂根据用户的需求联系子类工厂(ConcreteFactory)进行研发和生产新的具体产品(ConcreteProduct)。为了使不同的子工厂能提供产品,需要借助基类工厂统一产品提供的公共接口
Factory.h
#if !defined(EA_BB1E9945_CCF9_4d5c_8B7D_C3D86C0C2856__INCLUDED_)
#define EA_BB1E9945_CCF9_4d5c_8B7D_C3D86C0C2856__INCLUDED_
#include "Product.h"
class Factory
{
public:
Factory();
virtual ~Factory();
virtual Product* factoryMethod();
};
#endif // !defined(EA_BB1E9945_CCF9_4d5c_8B7D_C3D86C0C2856__INCLUDED_)
Factory.cpp
#include "Factory.h"
#include<stdio.h>
Factory::Factory(){
}
Factory::~Factory(){
}
Product* Factory::factoryMethod(){
return NULL;
}
子类工厂(派生类) 对新的需求的产品进行生产
基于Factory,可以设计具体的工厂,这些工厂只生产一种产品(专一)
ConcreteFactory.h
#if !defined(EA_99AEC7F3_304D_41c6_A35C_A81D7E5B280F__INCLUDED_)
#define EA_99AEC7F3_304D_41c6_A35C_A81D7E5B280F__INCLUDED_
#include "Product.h"
#include "Factory.h"
class ConcreteFactory : public Factory
{
public:
ConcreteFactory();
virtual ~ConcreteFactory();
virtual Product* factoryMethod();
};
#endif // !defined(EA_99AEC7F3_304D_41c6_A35C_A81D7E5B280F__INCLUDED_)
ConcreteFactory.cpp
#include "ConcreteFactory.h"
#include "ConcreteProduct.h"
ConcreteFactory::ConcreteFactory(){
}
ConcreteFactory::~ConcreteFactory(){
}
Product* ConcreteFactory::factoryMethod(){
return new ConcreteProduct();
}
2.3.3 产品
同样的
为了能让工厂返回的不同产品能被用户使用,需要利用多态性质设计公共接口。
为了能让工厂返回不同的产品,需要单独对每个产品的公共接口进行设计。
基类产品Product:
// h
#if !defined(EA_071D24B8_F0A3_4f19_955A_8F5511036EF0__INCLUDED_)
#define EA_071D24B8_F0A3_4f19_955A_8F5511036EF0__INCLUDED_
class Product
{
public:
Product();
virtual ~Product();
virtual void use();
};
#endif // !defined(EA_071D24B8_F0A3_4f19_955A_8F5511036EF0__INCLUDED_)
// cpp
#include "Product.h"
Product::Product(){
}
Product::~Product(){
}
void Product::use(){
}
基于基类产品,可以设计各种各样的产品,以供特定的工厂提供给用户使用
ConcreteProduct
// h
#if !defined(EA_EF9F81B2_79BC_4b45_BB45_60E30EE545C4__INCLUDED_)
#define EA_EF9F81B2_79BC_4b45_BB45_60E30EE545C4__INCLUDED_
#include "Product.h"
class ConcreteProduct : public Product
{
public:
ConcreteProduct();
virtual ~ConcreteProduct();
virtual void use();
};
#endif // !defined(EA_EF9F81B2_79BC_4b45_BB45_60E30EE545C4__INCLUDED_)
//cpp
#include "ConcreteProduct.h"
#include <iostream>
using namespace std;
ConcreteProduct::ConcreteProduct(){
}
ConcreteProduct::~ConcreteProduct(){
}
void ConcreteProduct::use(){
cout << "use prodect A" << endl;
}
至此,用户(main)能通过工厂(Factory)获取所需的特定的产品(ConcreteProduct)。
2.4 总结
-
工厂方法模式又称为工厂模式,它属于类创建型模式。在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象,这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
-
工厂方法模式包含四个角色:抽象产品是定义产品的接口,是工厂方法模式所创建对象的超类型,即产品对象的共同父类或接口;具体产品实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,它们之间往往一一对应;抽象工厂中声明了工厂方法,用于返回一个产品,它是工厂方法模式的核心,任何在模式中创建对象的工厂类都必须实现该接口;具体工厂是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户调用,返回一个具体产品类的实例。
-
工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。
-
工厂方法模式的主要优点是增加新的产品类时无须修改现有系统,并封装了产品对象的创建细节,系统具有良好的灵活性和可扩展性;其缺点在于增加新产品的同时需要增加新的工厂,导致系统类的个数成对增加,在一定程度上增加了系统的复杂性。
举个例子,如果需要新增一个新的产品,则工厂方法部分实现如下
// 新增一个ConcreteFactoryB class ConcreteFactoryB : public Factory { public: ConcreteFactoryB (); virtual ~ConcreteFactoryB (); virtual Product* factoryMethod(); }; // 新增一个ConcreteProductB class ConcreteProductB: public Product { public: ConcreteProductB(); virtual ~ConcreteProductB(); virtual void use(); };
然后对其进行编译生产相关lib或dll,并将ConcreteFactoryB的头文件提供给用户即可使用,无需重新编译Factory。
-
工厂方法模式适用情况包括:一个类不知道它所需要的对象的类;一个类通过其子类来指定创建哪个对象;将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定。
3 抽象工厂模式(Abstract Factory)
// 挖坑;todo
4 单例模式
4.1 结构
单例模式包含如下角色:
- Singleton:单例
4.2实例
用户main
#include <iostream>
#include "Singleton.h"
using namespace std;
int main(int argc, char *argv[])
{
Singleton * sg = Singleton::getInstance();
sg->singletonOperation();
return 0;
}
Singleton 单例模式实现
// h
#if !defined(EA_5A12F734_0177_4e67_9117_77C147875E5A__INCLUDED_)
#define EA_5A12F734_0177_4e67_9117_77C147875E5A__INCLUDED_
class Singleton
{
public:
virtual ~Singleton();
Singleton *m_Singleton;
static Singleton* getInstance();
void singletonOperation();
private:
static Singleton * instance;
Singleton();
};
#endif // !defined(EA_5A12F734_0177_4e67_9117_77C147875E5A__INCLUDED_)
// cpp
#include "Singleton.h"
#include <iostream>
using namespace std;
Singleton * Singleton::instance = NULL;
Singleton::Singleton(){
}
Singleton::~Singleton(){
delete instance;
}
Singleton* Singleton::getInstance(){
if (instance == NULL)
{
instance = new Singleton();
}
return instance;
}
void Singleton::singletonOperation(){
cout << "singletonOperation" << endl;
}
4.3 总结
- 单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。
- 单例模式只包含一个单例角色:在单例类的内部实现只生成一个实例,同时它提供一个静态的工厂方法,让客户可以使用它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有。
- 单例模式的目的是保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例类拥有一个私有构造函数,确保用户无法通过new关键字直接实例化它。除此之外,该模式中包含一个静态私有成员变量与静态公有的工厂方法。该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。
- 单例模式的主要优点在于提供了对唯一实例的受控访问并可以节约系统资源;其主要缺点在于因为缺少抽象层而难以扩展,且单例类职责过重。
- 单例模式适用情况包括:系统只需要一个实例对象;客户调用类的单个实例只允许使用一个公共访问点。
5 IMPL模式
5.1 动机
如果有一个算法,需要提供给其他同事或厂家使用,包含头文件和动态库,假设头文件如下:
// 版本1
class Function{
public:
int use(std::string param);
···
private:
int param1;
char param2[1024];
std::vector<int> param3;
···
};
用户直接 include 头文件,链接库文件即可。方法上没有问题,但问题是头文件中暴露的信息太多了,比如 private 成员变量,而且如果以后的版本中需要增加或删除某些变量,还需要通知用户修改头文件,太麻烦了。
为了解决这个问题,实现接口与实现分离,所以引入了 IMPL 模式。
5.2 结构
IMPL模式包含如下角色:
- Function:功能
- FunctionIMPL:功能IMPL
5.3 实例
5.3.1 结构体隐藏
案例:
// Function.h
// 版本2
class Function{
public:
int use(const std::string param);
private:
struct Impl;
std::shared_ptr<Impl> impl;
};
// Function.cpp
// 版本2
struct Function::Impl {
int param1;
char param2[1024];
std::vector<int> param3;
···
};
int Function::use(const std::string param) {
// TODO ...
return true;
}
这样就做到了隐藏类中的成员变量了,核心思想就是 将成员变量打包放在一个结构体中 ,无论以后的版本中有无删减成员变量,都不会对头文件造成任何影响。
5.3.2 使用虚函数和继承隐藏
但是上一种模式还是会有 private 的成员变量,如果是想要完全隐藏则需要进一步改进。
案例:
// Function.h
// 版本3
class Function{
public:
virtual int use(const std::string param) = 0;
// 创建和销毁函数
static Function* New();
static void Delete(Function *net);
};
// network.cpp
// 版本3
class FunctionImpl : public Function{
public:
int use(const std::string param) override {
// todo
return true;
}
···
};
// 创建和销毁函数
Function* Function::New() {
return (new FunctionImpl());
}
void Function::Delete(Function *net) {
delete (FunctionImpl*)net;
}
这样就做到了完全隐藏,其中增加的new和delete可以用智能指针代替,Function和FunctionImpl 的实现也可以分开。
5.4 总结
优点就是做到了:
- 虚函数开销 :虚函数需要使用虚函数表指针间接调用,运行时才能确定调用哪一个函数,无法在编译期间内联优化。在上一版中,在编译期就能确定调用哪一个函数,根本用不到虚函数的特性。
- 二进制兼容 :虚函数是按照索引查询虚函数表来调用的,新增或调整虚函数顺序会造成索引变化,导致新接口在二进制层面不能兼容老接口,就是在末尾增加虚函数,也会有风险。二进制兼容参考文章: https://blog.csdn.net/myw31415926/article/details/127723128?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~Rate-1-127723128-blog-127722899.235^v38^pc_relevant_anti_vip&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~Rate-1-127723128-blog-127722899.235^v38^pc_relevant_anti_vip&utm_relevant_index=1