《C++API设计》读书笔记(3):模式

2023-09-17 15:20:17

本章内容

本章涵盖了一些与C++API设计相关的设计模式惯用法

“设计模式(Design Pattern)”表示软件设计问题的一些通用解决方案。该术语来源于《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software

本书不会涵盖所有模式,只讨论一些和API设计有关的。
还会涉及一些C++的惯用法,它们并非是真正的通用设计模式,但却是C++API设计的重要技巧。
本章讨论的技巧如下:

1. Pimpl

Pimpl 意思是 Pointer to Implementation,指向实现的指针。
(Pimpl 不是严格意义上的“设计模式”,而是受制于C++语法特定限制的变通方案,可以看作是 桥接 设计模式的一种特例)

它主要解决的是C++语法中的一个问题:类的private成员其实只在内部使用,但是在定义时还需要写在.h文件中公开。因此,Pimpl 的做法是只在.h中定义一个指向实现的指针,那就可以将一些private成员放在.cpp中的实现中,这样就在.h文件中隐藏了private成员。
举例:
在这里插入图片描述

使用Pimpl的方法

举例代码:
.h文件中:

class AutoTimer
{
public:
	explicit AutoTimer();
	~AutoTimer();
private:
	class Impl;
	Impl* mImpl;
}

随后,在.cpp中可以定义Impl类并实现具体的逻辑。

一个值得考虑的设计问题是:Impl类中放置多少逻辑?有以下选择:

  1. 仅私有变量
  2. 私有变量+私有方法
  3. 公有类的所有方法。(而共有类的方法只是对Impl类中方法的简单包装)

每种选择都适应于不同情况。一般情况下推荐 2 。

另外,使用 Pimpl 时需要注意:

  1. virtual 函数不能放在Impl类中,否则公有类的子类无法继承。
  2. Impl类可能还需要一个公有类的指针以方便其调用公有类的方法。
使用Pimpl的类的复制

使用 Pimpl 的类无法进行默认的复制,因为复制出的对象会和原对象指向同一个Impl类变量。
这个问题有两种方法解决:

  1. 禁止复制
  2. 显示定义复制语义
Pimpl与智能指针

使用 Pimpl 时容易犯错的一点是:构造时忘记分配它,析构时忘记销毁它。
为此,可以采用 智能指针 或者 作用域指针。

Pimpl的优点
  • 信息隐藏。
  • 降低耦合
  • 加速编译
  • 更好的二进制兼容性。(就算Impl类实现发生变化,公用类的对象也不会改变二进制数据)
  • 惰性分配(可以选择只在需要时分配)
Pimpl的缺点
  • 额外的分配与销毁Impl类对象会增加性能开销
  • 给开发者带来了不便:很多函数调用时需要加上mImpl->,如果Impl类需要调用公有类的方法也需要通过指针。
  • 编译器不能再检查constImpl类的成员更改,公有类的const无法检查出。

2. 单例

“单例”设计模式确保一个类仅存在一个实例,并提供唯一的全局访问点。

它可以看作是一种更优雅的全局变量,但是相比全局变量有一些优点:

  • 确保这个类只能创建一个实例。
  • 控制对象的分配与销毁。
  • 可以支持线程安全。
  • 避免污染全局命名空间。

其基本实现很简单,而本篇重点讨论的是:

  • 如何更健壮地实现。
  • 它也有些缺点,但很多人都有滥用单例的趋向。为此这里也提供一些替代方法。
在C++中实现单例

其基本实现很简单:

class Singleton 
{
public:
	static Singleton &GetInstance();
};
Singleton &Singleton::GetInstance()
{
	static Singleton instance;
	return instance;
}

有一些做法可以增加健壮性:

  • 声明私有默认构造函数:防止用户创建新的实例。
  • 声明私有复制构造函数和赋值操作:防止用户复制。
  • 声明私有析构函数:防止用户删除。
  • GetInstance()返回引用而非指针:防止用户删除。
单例的线程安全

上面的 GetInstance() 并非线程安全的。

常规的处理方式是加互斥锁。但这样会增加开销。
要优化此类激进的加锁行为,可以采用DCLP(Double Check Locking Pattern),即加互斥锁前先判断instance是否存在。
但是DCLP不能保证任何编译器和处理器下都能正常工作。

所以,也许你不应该尝试保证GetInstance()是线程安全的(毕竟对使用C++这样对并发缺乏内在支持的语言来说,实现线程安全总会遇到这些困难)。如果你真的需要它线程安全并且性能最高,可以考虑避免惰性实例化模型(即不要在需要他时再实例化),比如:

  1. 静态初始化:在cpp文件中main函数调用之前调用GetInstance()
  2. 显式API初始化:在一开始就调用GetInstance(),调用时可以加互斥锁。而GetInstance()的内部就不用加互斥锁了。
替代方案:依赖注入

初始化时传入需要的实例的指针,而不是内部再使用GetInstance()获得实例。

替代方案:单一状态

假设状态的初始化不需要控制,或者不需要使用单例对象存储,那么就可以使用“单一状态”,即:
类本身不保持是单例,但是其所有成员(或者说“状态”)都是static。

替代方案:会话上下文

《设计模式》作者指出,单例有可能导致拙劣的设计。使用时候需要思考,“单例”是否真的是正确的模式?

需求是会变的,未来有些对象可能会需要支持多个实例。

因此,需要尽早考虑引入 “会话(session)” 或 “上下文(context)” 的概念。这是在强调:使用单一的实例维护所有相关的状态,而非使用多个单例

3. 工厂模式

工厂模式是关于创建的设计模式,本质上是构造函数的泛化,可以回避C++构造函数的限制:

  • 没有返回值。这样无法返回错误等其他信号。
  • 命名限制。这样相同参数的构造函数只能有一个。
  • 静态绑定创建。这样无法在运行时动态决定类型。
  • 不允许虚构造函数。限制同上。

从使用层面上看,工厂方法仅是一个普通的方法,调用时返回对应类的实例:

class RendererFactory
{
public:
	IRenderer* CreateRenderer(string type)
}

但这里的 IRenderer 是一个抽象基类(Abstract Base Class,简称ABC)。抽象基类是包含纯虚函数的类,不能被实例化。(另外要注意,抽象基类的析构函数需要声明为虚的)。

这样,用户可以使用参数动态决定要创建的类型。

另外作为扩展,可以让工厂类提供注册函数,这样用户可以自己添加新的类型

static void RegisterRender(string type, CreateCallback cb);

4. API包装器

基于另一组API来包装接口是一项常见的API设计任务。比如,你在维护一个遗留的代码库,相比重构代码,你更愿意封装一套新的,更简洁的API,以隐藏所有的底层遗留代码。

下面,按照包装器层和原始接口的差异程度递增地划分:

4.1 代理(Proxy)

一对一地,将函数调用转发到具有相同形式的另一个接口。

案例:

  • 实现原始对象的惰性实例。
  • 实现对原始对象的访问控制。
  • 支持 “调试” 模式或者 “演习(DryRun)” 模式。
  • 保证原始对象的线程安全
  • 可以让多个代理对象共享相同的原始对象。
  • 应对原始对象将来被修改的情况。
4.2 适配器

一对一地,将接口转换为一个兼容但是不完全相同的另一个接口。

优点:

  • 可以转换数据类型。
  • 强制API始终保持一致性。
  • 包装API的依赖库
4.3 外观模式

外观模式能够为一组类提供简化的接口。它实际上定义了一个更高层次的接口,使得底层类更易于使用且对用户隐藏。
(一个例子:用一个“酒店助手类”简化了“预定房间”、“预定晚餐”、“预定出租车”等事务。)

用途:

  • 隐藏遗留代码。
  • 创建更便捷的API。
  • 支持简化功能或替代功能的API。

5. 观察者模式

观察者模式为了解决这样的问题:
实现复杂的任务通常需要多个对象一起合作完成。为了让A可以调用B,较为简单的方法就是A.cpp包含B.h,但是这样产生了编译时依赖,迫使想要复用A时也必须引入B。

观察者模式就是 “发布/订阅” 范式的一个具体实例。

实现观察者模式的典型做法是引入两个概念:

  • Subject,主体,也就是发布者。
  • Observer,观察者,也就是订阅者。

代码上,Subject仅知道Observer接口类即IObserver,他将维护IObserver列表

class IObserver
{
	virtual void Update() = 0;
}

class ISubject
{
std::vector<IObserver*> ObserverList
}

随后,观察者通过继承IObserver来实现观察者对象。

然后,在使用时,Subject就可以订阅(Subscribe)若干Observer,并在需要的时候通知(Notify)它们。

这样,Subject和Observer就没有编译时依赖关系,它们的关系是运行时动态创建的。

更多推荐

.bat批处理命令处理文件

批处理命令处理文件找到上级目录,并删除文件与文件夹参考资料:[BAT]如何获取bat的上一级目录、上两级目录..._bat上层目录_Risun_Lee的博客-CSDN博客@echooffsetcurrPath=%~dp0setparentPath=setparentparentPath=:beginfor/f"toke

华为云云耀云服务器L实例评测|部署功能强大的开源物联平台ThingsBoard

华为云云耀云服务器L实例评测|部署功能强大的开源物联平台ThingsBoard应用场景ThingsBoard介绍ThingBoard是一个开源的物联网(IoT)平台,旨在为智能设备、传感器和执行器的远程管理和控制提供一站式解决方案。它提供了一套功能强大的Web界面,允许用户监控和控制其物联网设备,同时支持设备之间的自定

开源在物联网(IoT)中的应用

🌷🍁博主猫头虎带您GotoNewWorld.✨🍁🦄博客首页——猫头虎的博客🎐🐳《面试题大全专栏》文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺🌊《IDEA开发秘籍专栏》学会IDEA常用操作,工作效率翻倍~💐🌊《100天精通Golang(基础入门篇)》学会Golang语言,畅玩云原生,走遍大

【Docker】Docker持续集成与持续部署(四)

前言:Docker与持续集成/持续部署(CI/CD)的作用是通过容器化技术实现环境一致性、快速构建和隔离性,从而加快软件交付速度、提高部署效率,确保持续集成和持续部署的顺利实施。持续集成(ContinuousIntegration)持续集成是一种开发实践,通过频繁地将代码集成到共享的主干(版本控制仓库)中,然后自动运行

ElasticSearch系列-索引原理与数据读写流程详解

索引原理倒排索引倒排索引(InvertedIndex)也叫反向索引,有反向索引必有正向索引。通俗地来讲,正向索引是通过key找value,反向索引则是通过value找key。ES底层在检索时底层使用的就是倒排索引。索引模型现有索引和映射如下:{"products":{"mappings":{"properties":{

C语言指针笔试题讲解

大家好,我们来学习一些C语言的指针笔试题。对于C语言指针的模块想必大家都非常的头疼吧,那么我们就来就来看看一些关于C语言指针的笔试题。首先让我们看到我们今天的第一题。intmain(){inta[5]={1,2,3,4,5};int*ptr=(int*)(&a+1);printf("%d,%d",*(a+1),*(pt

springboot集成Logback 日志写入数据库

springboot集成Logback日志写入数据库引入maven依赖<!--logback日志--><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.2.3</version>

Mybatis学习笔记3 在Web中应用Mybatis

Mybatis学习笔记2增删改查及核心配置文件详解_biubiubiu0706的博客-CSDN博客技术栈:HTML+Servlet+Mybatis学习目标:掌握mybatis在web应用中如何使用Mybatis三大对对象的作用域和生命周期关于Mybatis中三大对象的作用域和生命周期、官网说明ThreadLocal原理

基于Java+SpringBoot+Vue前后端分离智慧生活商城系统设计和实现

博主介绍:✌全网粉丝30W+,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌🍅文末获取源码联系🍅👇🏻精彩专栏推荐订阅👇🏻不然下次找不到哟2022-2024年最全的计算机软件毕业设计选题

深度学习100例 | 第31天-卷积神经网络(DenseNet)识别生活物品

🚀我的环境:语言环境:Python3.6.5编译器:jupyternotebook深度学习环境:TensorFlow2.4.1显卡(GPU):NVIDIAGeForceRTX3080数据:📌【传送门】🚀本文选自专栏:《深度学习100例》🚀深度学习新人必看:《小白入门深度学习》小白入门深度学习|第一篇:配置深度学

基于Java生活缴费系统设计实现(源码+lw+部署文档+讲解等)

博主介绍:✌全网粉丝30W+,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌🍅文末获取源码联系🍅👇🏻精彩专栏推荐订阅👇🏻不然下次找不到哟2022-2024年最全的计算机软件毕业设计选题

热文推荐