C++设计模式(李建忠)
本文是学习笔记,如有侵权,请联系删除。
参考链接
Gtihub源码与PPT:https://github.com/ZachL1/Bilibili-plus
文章目录
- C++设计模式(李建忠)
-
- [8 工厂方法(Factory Method)](#8 工厂方法(Factory Method))
- [9 抽象工厂(Abstract Factory)](#9 抽象工厂(Abstract Factory))
- [10 原型模式(Prototype)](#10 原型模式(Prototype))
- [11 构建器(Builder)](#11 构建器(Builder))
- [12 单例模式(Singleton)](#12 单例模式(Singleton))
- [13 享元模式(Flyweight)](#13 享元模式(Flyweight))
- 后记
"对象创建"模式
通过"对象创建"模式绕开new,来避免对象创建(new)过程 中所导致的紧耦合(依赖具体类),从而支持对象创建的稳定。它是接口抽象之后的第一步工作。
典型模式
·Factory Method
·Abstract Factory
·Prototype
·Builder
8 工厂方法(Factory Method)
Motivation
在软件系统中,经常面临着创建对象的工作;由于需求的变化,需要创建的对象的具体类型经常变化。
如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种封装机制来避免客户程序和这种具体对象创建工作的紧耦合?
代码示例:
MainForm方法调用Splitter将东西进行切分,有二进制文件切分、txt文件切分、图片文件切分、视频文件切分。
第一版:ISplitter * splitter= new BinarySplitter();
这种写法依赖于具体类,违反依赖倒置原则(高层模块不应该依赖于低层模块,而是应该依赖于抽象。同时,抽象不应该依赖于具体实现,具体实现应该依赖于抽象。)
cpp
// 抽象基类
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
{
TextBox* txtFilePath;
TextBox* txtFileNumber;
ProgressBar* progressBar;
public:
void Button1_Click(){
ISplitter * splitter= new BinarySplitter();//依赖具体类
splitter->split();
}
};
第二版:具体调用工厂,MainForm不依赖于具体的类,而是依赖于抽象类和工厂基类。
cpp
class MainForm : public Form
{
SplitterFactory* factory;//工厂
public:
MainForm(SplitterFactory* factory){
this->factory=factory;
}
void Button1_Click(){
ISplitter * splitter=
factory->CreateSplitter(); //多态new
splitter->split();
}
};
ISplitterFactory
cpp
//抽象类
class ISplitter{
public:
virtual void split()=0;
virtual ~ISplitter(){}
};
//工厂基类
class SplitterFactory{
public:
virtual ISplitter* CreateSplitter()=0; // 创建对象
virtual ~SplitterFactory(){}
};
FileSplitter文件:几个具体的工厂继承自SplitterFactory,实现虚函数,实现具体类的创建
cpp
//具体类
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使一个类的实例化延迟到其子类。
具体到上面的代码,用于创建对象的接口SplitterFactory,让子类(BinarySplitterFactory,TxtSplitterFactory等具体工厂)决定实例化哪一个类。
在下列情况下可以使用Factory Method模式:
• 当一个类不知道它所必须创建的对象的类的时候。
• 当一个类希望由它的子类来指定它所创建的对象的时候。
• 当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。(classes delegate responsibility to one of several helper subclasses, and you want to localize the knowledge of which helper subclass is the delegate.)
工厂方法结构
参与者
• Product(ISplitter)
--- 定义工厂方法所创建的对象的接口。
• ConcreteProduct(BinarySplitter,TxtSplitter,PictureSplitter,VideoSplitter)
--- 实现Product接口。
• Creator(SplitterFactory)
--- 声明工厂方法,该方法返回一个Product类型的对象。Creator也可以定义一个工厂方法的缺省实现,它返回一个缺省的ConcreteProduct对象。
--- 可以调用工厂方法以创建一个Product对象。
• ConcreteCreator(BinarySplitterFactory,TxtSplitterFactory,PictureSplitterFactory,VideoSplitterFactory)
--- 重定义工厂方法以返回一个ConcreteProduct实例。
工厂方法总结
Factory Method模式用于隔离类对象的使用者和具体类型之间的耦合关系。面对一个经常变化的具体类型,紧耦合关系(new)会导致软件的脆弱。
Factory Method模式通过面向对象(多态)的手法,将所要创建的具体对象工作延迟到子类,从而实现一种扩展(而非更改)的策略,较好地解决了这种紧耦合关系。
Factory Method模式解决单个对象的需求变化,缺点在于创建方法或者传入的参数要相同。
9 抽象工厂(Abstract Factory)
Motivation
在软件系统中,经常面临着一系列相互依赖的对象的创建工作;同时,由于需求的变化,往往存在更多系列对象的创建工作。如何应对这种变化?如何绕过常规的对象创建方法(new),提供一种封装机制来避免客户程序和这种多系列具体对象创建工作的紧耦合?
代码示例
创建数据访问层,支持多种数据库
第一版:不好的设计,绑定到具体的类
cpp
class EmployeeDAO{
public:
vector<EmployeeDO> GetEmployees(){
SqlConnection* connection =
new SqlConnection();
connection->ConnectionString = "...";
SqlCommand* command =
new SqlCommand();
command->CommandText="...";
command->SetConnection(connection);
SqlDataReader* reader = command->ExecuteReader();
while (reader->Read()){
}
}
};
第二版:使用工厂方法,但是会遇到问题,使用的时候工厂之间有关联性,必须是同一系列的,比如都是oracle数据库的连接、命令、datareader等,不能是mysql的连接、orable的命令这种交叉使用。
cpp
// 三个对象必须是同系列(同组),有关联性
IDBConnectionFactory* dbConnectionFactory;
IDBCommandFactory* dbCommandFactory;
IDataReaderFactory* dataReaderFactory;
工厂方法的代码如下
cpp
//数据库访问有关的基类
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{
};
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); //关联性
IDBDataReader* reader = command->ExecuteReader(); //关联性
while (reader->Read()){
}
}
};
第三版:有关联性的创建行为放在一起,合成一个工厂,这就是抽象工厂Abstract Factory
cpp
//数据库访问有关的基类
class IDBConnection{
};
class IDBCommand{
};
class IDataReader{
};
// 创建一个工厂:有关联性的放在一起
class IDBFactory{
public:
virtual IDBConnection* CreateDBConnection()=0;
virtual IDBCommand* CreateDBCommand()=0;
virtual IDataReader* CreateDataReader()=0;
};
//支持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;
};
//支持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()){
}
}
};
抽象工厂定义
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
抽象工厂结构
参与者
• AbstractFactory (IDBFactory) --- 声明一个创建抽象产品对象的操作接口。
• ConcreteFactory (SqlDBFactory,OracleDBFactory等) --- 实现创建具体产品对象的操作。
• AbstractProduct (IDBConnection,IDBCommand等) --- 为一类产品对象声明一个接口。
• ConcreteProduct (SqlConnection, OracleCommand等) --- 定义一个将被相应的具体工厂创建的产品对象。实现AbstractProduct接口。
• Client --- 仅使用由AbstractFactory和AbstractProduct类声明的接口。
协作
• 通常在运行时刻创建一个ConcreteFactory类的实例。这一具体的工厂创建具有特定实现的产品对象。为创建不同的产品对象,客户应使用不同的具体工厂。
• AbstractFactory将产品对象的创建延迟到它的ConcreteFactory子类。
要点总结
如果没有应对多系列对象构建的需求变化,则没有必要使用Abstract Factory模式,这时候使用简单的工厂即可。
系列对象指的是在某一特定系列下的对象之间有相互依赖或相互作用的关系。不同系列的对象之间不能相互依赖。
10 原型模式(Prototype)
Motivation
在软件系统中,经常面临着某些结构复杂的对象的创建;由于需求的变化,这些对象经常面临着剧烈的变化,但是它们却拥有比较稳定一致的接口。
如何应对这种变化?如何向客户程序隔离出这些易变对象,从而使得依赖这些易变对象的客户程序不随着需求改变而改变?
代码示例
cpp
//抽象类
class ISplitter{
public:
virtual void split()=0;
virtual ISplitter* clone()=0; //通过克隆自己来创建对象
virtual ~ISplitter(){}
};
//具体类
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);
}
};
Client怎么用原型方法
cpp
class MainForm : public Form
{
ISplitter* prototype;//原型对象
public:
MainForm(ISplitter* prototype){
this->prototype=prototype;
}
void Button1_Click(){
ISplitter * splitter=
prototype->clone(); //克隆原型
splitter->split();
}
};
原型方法定义
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型方法结构
参与者
• Prototype(ISplitter)
--- 声明一个克隆自身的接口。
• ConcretePrototype(BinarySplitter、TxtSplitter、PictureSplitter等)
--- 实现一个克隆自身的操作。
• Client(MainForm)
--- 让一个原型克隆自身从而创建一个新的对象。
再给出chatGPT生成的原型方法的示例
原型方法(Prototype Pattern)是一种创建型设计模式,其主要目的是通过复制现有对象来创建新对象,而不是通过实例化类。这样做可以避免直接使用构造函数创建对象,提高对象的创建效率,同时也使得对象的创建更加灵活。原型方法通常包含一个克隆方法,用于复制对象。
以下是一个简单的C++代码示例,演示了原型方法的实现:
cpp
#include <iostream>
#include <string>
// 抽象原型类
class Prototype {
public:
virtual Prototype* clone() const = 0;
virtual void printInfo() const = 0;
virtual ~Prototype() {}
};
// 具体原型类 A
class ConcretePrototypeA : public Prototype {
public:
Prototype* clone() const override {
return new ConcretePrototypeA(*this);
}
void printInfo() const override {
std::cout << "Concrete Prototype A" << std::endl;
}
};
// 具体原型类 B
class ConcretePrototypeB : public Prototype {
public:
Prototype* clone() const override {
return new ConcretePrototypeB(*this);
}
void printInfo() const override {
std::cout << "Concrete Prototype B" << std::endl;
}
};
int main() {
// 创建原型对象
Prototype* prototypeA = new ConcretePrototypeA();
Prototype* prototypeB = new ConcretePrototypeB();
// 克隆对象
Prototype* cloneA = prototypeA->clone();
Prototype* cloneB = prototypeB->clone();
// 打印信息
prototypeA->printInfo();
cloneA->printInfo();
prototypeB->printInfo();
cloneB->printInfo();
// 释放资源
delete prototypeA;
delete cloneA;
delete prototypeB;
delete cloneB;
return 0;
}
在上述示例中,Prototype
是抽象原型类,其中包含了 clone
和 printInfo
纯虚函数。ConcretePrototypeA
和 ConcretePrototypeB
是具体的原型类,分别实现了这两个函数。在 main
函数中,通过创建原型对象,然后克隆对象,最后打印信息,展示了原型方法的基本用法。
适用性
原型方法适用于那些需要频繁创建对象,且对象的创建成本较高或创建过程复杂的情况。通过克隆现有对象来创建新对象,可以提高效率、简化代码,并且灵活应对动态配置和变化。
原型模式和工厂方法的区别
- 创建方式:
- 原型模式通过克隆现有对象来创建新对象。
- 工厂方法模式通过实现工厂方法来由子类决定创建的具体产品。
- 关注点:
- 原型模式关注对象的复制。
- 工厂方法模式关注对象的创建。
- 灵活性:
- 原型模式更加灵活,允许在运行时动态选择复制的对象。
- 工厂方法模式更加固定,创建的对象由具体的子类工厂决定。
在实际应用中,选择使用哪种模式取决于需求和设计目标。原型模式更适合对象创建时变化不大、但需要频繁创建的情况,而工厂方法模式更适合在创建对象的类层次结构变化频繁时。
11 构建器(Builder)
Motivation
在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
如何应对这种变化?如何提供一种"封装机制"来隔离出"复杂对象的各个部分"的变化,从而保持系统中的"稳定构建算法"不随着需求改变而改变?
代码示例:建立各种房子,比如砖瓦房,高楼,别墅等房子
cpp
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{
};
// 石头房子的创建,override HouseBuilder里面的流程
class StoneHouseBuilder: public HouseBuilder{
protected:
virtual void BuildPart1(){
//pHouse->Part1 = ...;
}
virtual void BuildPart2(){
}
virtual void BuildPart3(){
}
virtual void BuildPart4(){
}
virtual void BuildPart5(){
}
};
// 房子具体构建过程
class HouseDirector{
public:
// 有一个HouseBuilder的指针
HouseBuilder* pHouseBuilder;
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();
}
};
int main() {
// 创建石头房子的建造者
StoneHouseBuilder stoneHouseBuilder;
// 创建房子Director,并指定建造者
HouseDirector houseDirector(&stoneHouseBuilder);
// 构建房子
House* stoneHouse = houseDirector.Construct();
// 输出结果
if (stoneHouse != nullptr) {
// 假设 House 类有适当的输出方法
// 这里只是简单示例,实际使用时需要根据具体类的实现调用相应的方法
std::cout << "Stone House constructed." << std::endl;
} else {
std::cout << "Failed to construct Stone House." << std::endl;
}
// 释放资源
delete stoneHouse;
return 0;
}
上述代码涉及了Builder设计模式,包括以下几个类:
-
House
类:- 表示待构建的产品,即房子。在示例中,
StoneHouse
类是House
的具体实现。
- 表示待构建的产品,即房子。在示例中,
-
HouseBuilder
抽象类:- 定义了构建产品(房子)的抽象接口。具体的建造者(例如
StoneHouseBuilder
)需要继承这个抽象类并实现接口中的方法,以完成具体产品的构建。
- 定义了构建产品(房子)的抽象接口。具体的建造者(例如
-
StoneHouseBuilder
类:- 是
HouseBuilder
的具体实现,负责构建石头房子。它通过实现抽象接口中的方法来完成具体的建造流程。
- 是
-
HouseDirector
类:- 起到指导建造的作用,通过构造函数接收一个具体的建造者对象。通过调用建造者的方法,按照一定的构建流程组织建造者完成产品的构建。
-
main
函数:- 在
main
函数中,创建了StoneHouseBuilder
的实例,然后创建了一个HouseDirector
的实例,并将建造者传递给导演。通过导演的Construct
方法,按照一定的构建流程构建了石头房子,并输出结果。
- 在
总体而言,Builder设计模式的目的是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。在这个示例中,HouseBuilder
抽象类定义了构建房子的接口,StoneHouseBuilder
实现了具体的石头房子构建流程,而 HouseDirector
则负责协调建造者完成整个构建流程。
Builder定义
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
Builder结构
参与者
• Builder(HouseBuilder)
--- 为创建一个Product对象的各个部件指定抽象接口。
• ConcreteBuilder(StoneHouseBuilder等)
--- 实现Builder的接口以构造和装配该产品的各个部件。
--- 定义并明确它所创建的表示。
--- 提供一个检索产品的接口。
• Director(HouseDirector)
--- 构造一个使用Builder接口的对象。
• Product(这里没表现出来)
--- 表示被构造的复杂对象。ConcreteBuilder创建该产品的内部表示并定义它的装配过程。
--- 包含定义组成部件的类,包括将这些部件装配成最终产品的接口。
Builder要点总结
Builder 模式主要用于"分步骤构建一个复杂的对象"。在这其中"分步骤"是一个稳定的算法,而复杂对象的各个部分则经常变化。
变化点在哪里,封装哪里------ Builder模式主要在于应对"复杂对象各个部分"的频繁需求变动。其缺点在于难以应对"分步骤构建算法"的需求变动。
在Builder模式中,要注意不同语言中构造器内调用虚函数的差别(C++ vs. C#) 。
chatGPT给出的builder的例子
以下是一个简单的C++示例代码,演示Builder设计模式:
cpp
#include <iostream>
#include <string>
// 产品类
class Product {
public:
void AddPart(const std::string& part) {
parts += part + " ";
}
void Show() const {
std::cout << "Product Parts: " << parts << std::endl;
}
private:
std::string parts;
};
// 抽象建造者类
class Builder {
public:
virtual void BuildPart1() = 0;
virtual void BuildPart2() = 0;
virtual Product* GetResult() = 0;
};
// 具体建造者类
class ConcreteBuilder : public Builder {
public:
ConcreteBuilder() {
product = new Product();
}
void BuildPart1() override {
product->AddPart("Part1");
}
void BuildPart2() override {
product->AddPart("Part2");
}
Product* GetResult() override {
return product;
}
private:
Product* product;
};
// 指导者类
class Director {
public:
Director(Builder* builder) : builder(builder) {}
void Construct() {
builder->BuildPart1();
builder->BuildPart2();
}
private:
Builder* builder;
};
int main() {
// 创建具体建造者
ConcreteBuilder concreteBuilder;
// 创建指导者,并传入具体建造者
Director director(&concreteBuilder);
// 指导者构建产品
director.Construct();
// 获取最终产品
Product* product = concreteBuilder.GetResult();
// 展示产品
if (product != nullptr) {
product->Show();
delete product;
}
return 0;
}
在这个示例中,Product
表示最终构建的复杂对象,Builder
是抽象建造者类,ConcreteBuilder
是具体建造者类,Director
是指导者类。通过指导者调用具体建造者的方法,最终得到构建完成的产品。这个例子中的产品是简单的包含两个部分的字符串,实际应用中可以根据需求定义更复杂的产品结构。
"对象性能"模式
面向对象很好地解决了"抽象"的问题,但是必不可免地要付出 一定的代价。对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。
典型模式
·Singleton
·Flyweight
12 单例模式(Singleton)
Motivation
在软件系统中,经常有一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性,以及良好的效率。
如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?
这应该是类设计者的责任,而不是使用者的责任。
单例模式代码举例:把构造函数设置为private
cpp
class Singleton{
private:
Singleton();
Singleton(const Singleton& other);
public:
static Singleton* getInstance();
static Singleton* m_instance;
};
Singleton* Singleton::m_instance=nullptr;
//线程非安全版本
Singleton* Singleton::getInstance() {
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
//线程安全版本,但锁的代价过高
Singleton* Singleton::getInstance() {
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
return m_instance;
}
//双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance() {
if(m_instance==nullptr){
Lock lock;
if (m_instance == nullptr) {
m_instance = new Singleton();
}
}
return m_instance;
}
//C++ 11版本之后的跨平台实现 (volatile)
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence
if (tmp == nullptr) {
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
tmp = new Singleton;
std::atomic_thread_fence(std::memory_order_release);//释放内存fence
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
单例模式定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式结构
参与者
• Singleton
--- 定义一个 Instance 操作,允许客户访问它的唯一实例。 Instance 是一个类操作(即 Smalltalk 中的一个类方法和 C++ 中的一个静态成员函数)。
--- 可能负责创建它自己的唯一实例。
协作
• 客户只能通过 Singleton 的 Instance 操作访问一个 Singleton 的实例。
下面是对单例模式双检查锁的介绍
在单例模式中使用双检查锁(DCL,Double-Checked Locking)时,可能会遇到一些问题,特别是在多线程环境下。在C++11之前的版本中,由于缺少内存栅栏(memory barrier)的支持,可能导致DCL失效,从而造成线程安全性问题。
主要问题包括:
-
内存写入和读取的重排序问题:
在一些编译器和硬件架构中,对于指令的执行顺序可能会发生重排序,导致在实例化对象时,成员变量的内存分配可能在对象构造之前,从而使其他线程在获取到非空但尚未构造完全的对象。
-
缺乏内存栅栏:
在C++11之前,C++标准库并没有提供内存栅栏的概念,而DCL依赖于内存栅栏来保证写入和读取的顺序性。缺少内存栅栏可能导致指令乱序执行,使得双检查锁失效。
-
编译器优化问题:
一些编译器可能会对代码进行优化,可能会在保证正确性的情况下对代码进行调整,从而导致DCL失效。
在C++11及以后的版本中,引入了std::atomic
等特性,可以更好地保证内存可见性和顺序性,从而避免了上述问题。因此,如果在C++11及以后的环境中,使用std::atomic
可以解决DCL的一些问题。
cpp
// Singleton.h
#pragma once
#include <atomic>
#include <mutex>
class Singleton {
public:
// 获取单例实例的静态方法
static Singleton* Instance();
private:
// 私有构造函数,确保单例不能通过其他方式实例化
Singleton() {}
private:
// 使用std::atomic提供的原子操作,确保多线程环境下的安全性
static std::atomic<Singleton*> m_instance;
// 用于保护实例化过程的互斥锁
static std::mutex m_mutex;
};
// Singleton.cpp
#include "Singleton.h"
// 初始化静态成员变量
std::atomic<Singleton*> Singleton::m_instance(nullptr);
std::mutex Singleton::m_mutex;
Singleton* Singleton::Instance() {
// 使用std::atomic加载实例,确保内存可见性
Singleton* tmp = m_instance.load(std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_acquire);
if (tmp == nullptr) {
// 使用互斥锁保护实例化过程
std::lock_guard<std::mutex> lock(m_mutex);
tmp = m_instance.load(std::memory_order_relaxed);
if (tmp == nullptr) {
// 实例化单例对象
tmp = new Singleton;
// 使用内存顺序和std::atomic_thread_fence确保内存可见性和顺序性
std::atomic_thread_fence(std::memory_order_release);
// 存储单例对象的指针
m_instance.store(tmp, std::memory_order_relaxed);
}
}
return tmp;
}
这里,使用std::atomic
提供的内存顺序和std::atomic_thread_fence
来保证正确的内存可见性和顺序性。在C++11及以后的环境中,这种方式是相对安全的。
13 享元模式(Flyweight)
Motivation
在软件系统采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价------主要指内存需求方面的代价。
如何在避免大量细粒度对象问题的同时,让外部客户程序仍然能够透明地适用面向对象的方式来进行操作?
代码举例:,创建字体对象时,同一个key的字体对象只有一个,而不是有很多
cpp
// Font.h
#pragma once
#include <string>
class Font {
private:
// 字体对象的唯一标识符
std::string key;
// 字体对象的状态(可能包含其他属性,这里未具体展示)
// ....
public:
// 构造函数,根据传入的 key 初始化字体对象
Font(const std::string& key) : key(key) {
// ...
}
};
// FontFactory.h
#pragma once
#include <map>
#include "Font.h"
class FontFactory {
private:
// 存储已创建的字体对象,key 是字体对象的唯一标识符,value 是对应的字体对象指针
std::map<std::string, Font*> fontPool;
public:
// 获取字体对象
Font* GetFont(const std::string& key);
// 清理字体池
void clear();
};
// FontFactory.cpp
#include "FontFactory.h"
Font* FontFactory::GetFont(const std::string& key) {
// 查找字体池中是否已存在对应 key 的字体对象
auto iter = fontPool.find(key);
if (iter != fontPool.end()) {
// 如果存在,则直接返回已有的字体对象
return iter->second;
} else {
// 如果不存在,则创建新的字体对象,并加入字体池
Font* font = new Font(key);
fontPool[key] = font;
return font;
}
}
void FontFactory::clear() {
// 清理字体池中的所有字体对象
for (auto& entry : fontPool) {
delete entry.second;
}
fontPool.clear();
}
享元模式定义
运用共享技术有效地支持大量细粒度的对象。
享元模式结构
参与者
• Flyweight
--- 描述一个接口,通过这个接口 flyweight 可以接受并作用于外部状态。
• ConcreteFlyweight
--- 实现 Flyweight 接口,并为内部状态(如果有的话)增加存储空间。
ConcreteFlyweight 对象必须是可共享的。它所存储的状态必须是内部的;即,它必须独立于 ConcreteFlyweight 对象的场景。
• UnsharedConcreteFlyweight
--- 并非所有的 Flyweight 子类都需要被共享。 Flyweight 接口使共享成为可能,但它并不强制共享。在 Flyweight 对象结构的某些层次, UnsharedConcreteFlyweight 对象通常将 ConcreteFlyweight 对象作为子节点。
• FlyweightFactory
--- 创建并管理 flyweight 对象。
--- 确保合理地共享 flyweight。当用户请求一个 flyweight 时, FlyweightFactory 对象提供一个已创建的实例或者创建一个(如果不存在的话)。
• Client
--- 维持一个对 flyweight 的引用。
--- 计算或存储一个(多个) flyweight 的外部状态。
下图说明如何共享flyweight
chatGPT给出的享元模式的例子
以下是一个简单的 C++ 代码例子,演示了享元模式的实现。这个例子以文本编辑器中的字符格式化为场景。
cpp
#include <iostream>
#include <map>
// 享元接口
class CharacterFormat {
public:
virtual void applyFormat() const = 0;
};
// 具体享元类,代表不同的字符格式
class BoldFormat : public CharacterFormat {
public:
void applyFormat() const override {
std::cout << "Applying Bold Format\n";
}
};
class ItalicFormat : public CharacterFormat {
public:
void applyFormat() const override {
std::cout << "Applying Italic Format\n";
}
};
class UnderlineFormat : public CharacterFormat {
public:
void applyFormat() const override {
std::cout << "Applying Underline Format\n";
}
};
// 享元工厂类,负责创建和管理享元对象
class CharacterFormatFactory {
private:
std::map<std::string, CharacterFormat*> formatPool;
public:
CharacterFormat* getFormat(const std::string& key) {
auto iter = formatPool.find(key);
if (iter != formatPool.end()) {
return iter->second;
} else {
CharacterFormat* format = nullptr;
if (key == "Bold") {
format = new BoldFormat();
} else if (key == "Italic") {
format = new ItalicFormat();
} else if (key == "Underline") {
format = new UnderlineFormat();
}
formatPool[key] = format;
return format;
}
}
};
// 客户端代码
int main() {
CharacterFormatFactory formatFactory;
// 用户输入的字符格式
std::string userInput = "Bold";
// 通过享元工厂获取或创建字符格式对象
CharacterFormat* format = formatFactory.getFormat(userInput);
// 应用字符格式
if (format) {
format->applyFormat();
} else {
std::cout << "Invalid Format\n";
}
// 释放资源
delete format;
return 0;
}
在这个例子中,CharacterFormat
是享元接口,而 BoldFormat
、ItalicFormat
、UnderlineFormat
是具体的享元类。CharacterFormatFactory
是享元工厂类,负责创建和管理享元对象。在客户端代码中,用户输入字符格式的信息,通过享元工厂获取或创建相应的字符格式对象,并应用该格式。这样,相同的字符格式对象可以被多次共享,减少了资源的重复创建。
要点总结
面向对象很好地解决了抽象性的问题,但是作为一个运行在机器 中的程序实体,我们需要考虑对象的代价问题。Flyweight主要解 决面向对象的代价问题,一般不触及面向对象的抽象性问题。
Flyweight采用对象共享的做法来降低系统中对象的个数,从而降 低细粒度对象给系统带来的内存压力。在具体实现方面,要注意对 象状态的处理。
对象的数量太大从而导致对象内存开销加大------什么样的数量才 算大?这需要我们仔细的根据具体应用情况进行评估,而不能凭空 臆断。
后记
截至2024年1月17日14点48分,学习了Factory Method, Abstract Factory, Prototype, Builder, Singleton, Flyweight模式。下午继续学习。