精通代码复用:设计原则与最佳实践
精通代码复用:设计原则与最佳实践
在你开始设计的所有层次上,从单一函数、类,到整个库和框架,都需要从一开始就考虑到代码复用。在接下来的文本中,所有这些不同的层次都被称为组件。以下策略将帮助你合理地组织你的代码。注意,所有这些策略都专注于使你的代码具有通用性。设计可复用代码的第二个方面,即提供易用性,更多地与你的接口设计相关,将在后面进行讨论。
避免合并无关或逻辑独立的概念
当你设计一个组件时,应该让它专注于一个单一任务或一组任务,即,你应该追求高内聚。这也被称为单一职责原则(SRP)。不要合并无关的概念,例如随机数生成器和XML解析器。即使你没有专门为了复用而设计代码,也要牢记这一策略。整个程序很少会被单独复用。相反,程序的部分或子系统会直接被纳入其他应用,或者被改编以用于稍微不同的用途。因此,你应该设计你的程序,以便你将逻辑上独立的功能划分为可以在不同程序中复用的独立组件。每个这样的组件都应具有明确定义的职责。这种程序策略模仿了现实世界中的离散、可互换部件的设计原则。
例如,你可以编写一个Car
类,并将发动机的所有属性和行为都放入其中。然而,发动机是可分离的组件,不与汽车的其他方面绑定在一起。发动机可以从一辆汽车中取出,放入另一辆汽车中。一个合适的设计应该包括一个Engine
类,其中包含所有与发动机相关的功能。一个Car
实例然后只包含一个Engine
实例。
将程序划分为逻辑子系统
你应该将你的子系统设计为可以独立复用的离散组件,即,追求低耦合。例如,如果你正在设计一个网络游戏,应该将网络和图形用户界面方面分开。这样,你就可以在不拖入另一个组件的情况下复用其中一个组件。例如,你可能想写一个非网络游戏,在这种情况下,你可以复用图形界面子系统,但不需要网络方面。同样地,你可以设计一个P2P文件共享程序,在这种情况下,你可以复用网络子系统,但不需要图形用户界面功能。确保遵循每个子系统的抽象原则。把每个子系统看作一个微型库,并为其提供一个连贯且易于使用的接口。即使你是唯一使用这些微型库的程序员,你也将从设计良好的接口和实现中受益,这些接口和实现将逻辑上不同的功能进行了分离。
使用类层次结构以分离逻辑概念
除了将程序划分为逻辑子系统外,你还应避免在类级别合并无关的概念。例如,假设你想为自动驾驶汽车编写一个类。你决定从一个基础的汽车类开始,并直接将所有自动驾驶逻辑加入其中。然而,如果你的程序中只需要一个非自动驾驶汽车呢?在这种情况下,与自动驾驶有关的所有逻辑都是无用的,可能会要求你的程序链接到它本来可以避免的库,如视觉库、LIDAR库等。一个解决方案是创建一个类层次结构,在其中自动驾驶汽车是通用汽车的派生类。这样,你就可以在不需要自动驾驶功能的程序中使用汽车基类,而不会招致这种算法的成本。
当有两个逻辑概念时,如自动驾驶和汽车,这种策略效果很好。当有三个或更多概念时,情况就变得更复杂了。例如,假设你想提供一辆卡车和一辆汽车,每辆都可能是自动驾驶或非自动驾驶的。从逻辑上讲,卡车和汽车都是车辆的特殊情况,因此它们应该是车辆类的派生类。同样,自动驾驶类可以是非自动驾驶类的派生类。你不能用一个线性层次结构提供这些分离。一个可能性是将自动驾驶方面作为一个混合类。通过使用多重继承在C++中实现了混合类的一种方式。例如,一个PictureButton
可以从Image
类和Clickable
混合类继承。然而,对于自动驾驶设计,最好使用一种不同类型的混合实现,即使用类模板。基本上,SelfDrivable
混合类可以定义如下:
template <typename T>
class SelfDrivable : public T {
};
这个SelfDrivable
混合类提供了实现自动驾驶功能所需的所有算法。一旦你有了这个SelfDrivable
混合类模板
,你就可以为汽车和卡车分别实例化一个:
SelfDrivable<Car> selfDrivingCar;
SelfDrivable<Truck> selfDrivingTruck;
这两行代码的结果是,编译器将使用SelfDrivable
混合类模板创建一个实例,其中所有的T都被替换为Car
,因此是从Car
派生的,另一个实例的T被替换为Truck
,因此是从Truck
派生的。
使用聚合以分离逻辑概念
聚合在接下来的内容中讨论,它模拟了“有一个”关系:对象包含其他对象以执行其某些方面的功能。当继承不适当时,你可以使用聚合来分离无关或相关但独立的功能。
无论你的设计在哪个层次,都应避免合并无关的概念,即,追求高内聚。例如,在方法级别,单一方法不应执行逻辑上无关的事情,混合变异(set)和检查(get)等。
例如,假设你想写一个Family
类来存储一个家庭的成员。显然,树状数据结构将是理想的存储这些信息的方式。你应该写一个单独的Tree
类,而不是在你的Family
类中集成树结构的代码。然后,你的Family
类可以包含和使用一个Tree
实例。用面向对象的术语来说,Family
has-a Tree
。采用这种技术,树状数据结构在另一个程序中更容易被复用。
消除用户界面依赖性
如果你的库是一个数据操作库,你会希望将数据操作与用户界面分开。这意味着对于这种类型的库,你绝对不应该假设库将在哪种类型的用户界面中使用。库不应使用任何标准输入和输出流,如cout
、cerr
和cin
,因为如果库是在图形用户界面的环境中使用,这些流可能没有意义。例如,一个基于Windows GUI的应用程序通常不会有任何形式的控制台I/O。即使你认为你的库只会在基于GUI的应用程序中使用,你也绝不应弹出任何类型的消息框或其他类型的通知给最终用户,因为这是客户端代码的责任。客户端代码决定如何向用户显示消息。这种类型的依赖性不仅导致可复用性差,而且还阻止了客户端代码适当地响应错误,例如,静默处理它。
模型-视图-控制器(MVC)范式是一个用于分离数据存储和数据可视化的著名设计模式。使用这个范式,模型可以在库中,而客户端代码可以提供视图和控制器。
使用模板进行通用数据结构和算法设计
C++有一个叫做模板(Templates)的概念,它允许你创建对类型或类具有通用性的结构。例如,你可能已经为整数数组编写了代码。如果你随后想要一个双精度浮点数数组,你需要重写和复制所有代码以适应双精度浮点数。模板的概念是,类型变成了规范的一个参数,你可以创建一个可以在任何类型上工作的单一代码体。模板允许你编写在任何类型上工作的数据结构和算法。
最简单的例子是std::vector
类,它是C++标准库的一部分。要创建一个整数向量,你写std::vector<int>
;要创建一个双精度浮点数向量,你写std::vector<double>
。模板编程通常非常强大,但也可能非常复杂。幸运的是,可以创建相对简单的模板用法,根据类型进行参数化。
无论何时有可能,你都应该使用通用设计来编写数据结构和算法,而不是编码某个特定程序的细节。不要编写只存储书籍对象的平衡二叉树结构。使其通用,以便它可以存储任何类型的对象。这样,你可以在书店、音乐商店、操作系统或任何需要平衡二叉树的地方使用它。
为什么模板比其他通用编程技术更好
模板并不是编写通用数据结构的唯一机制。另一种、尽管更老的方法是在C和C++中存储void*
指针,而不是特定类型的指针。客户端可以通过将其转换为void*
来存储他们想要的任何东西。然而,这种方法的主要问题是它不是类型安全的:容器不能检查或强制存储元素的类型。
与直接在你的通用非模板数据结构中使用void*
指针相比,你可以使用自C++17以来可用的std::any
类。std::any
类的底层实现在某些情况下确实使用了void*
指针,但它还跟踪了存储的类型,所以一切都保持了类型安全。
另一种方法是为特定类编写数据结构。通过多态性,该类的任何派生类都可以存储在结构中。模板,另一方面,在正确使用时是类型安全的。每个模板实例只存储一种类型。如果你尝试在同一个模板实例中存储不同的类型,你的程序将无法编译。此外,模板允许编译器为每个模板实例生成高度优化的代码。
模板的问题
模板并不完美。首先,它们的语法可能令人困惑,尤其是对于那些以前没有使用过它们的人。其次,模板需要同质的数据结构,在单一结构中只能存储相同类型的对象。这就是模板的类型安全性直接导致的限制。
从C++17开始,有一种标准化的方法来绕过这种同质性限制。你可以编写你的数据结构以存储std::variant
或std::any
对象。一个std::any
对象可以存储任何
类型的值,而一个std::variant
对象可以存储一系列类型中的一个值。std::any
和std::variant
在后文讨论。
模板的缺点:代码膨胀
模板的另一个可能的缺点是所谓的代码膨胀:最终二进制代码的大小增加。每个模板实例的高度专门化代码比稍慢的通用代码需要更多的代码。然而,通常来说,如今代码膨胀并不是一个很大的问题。
模板与继承
程序员有时发现决定是否使用模板或继承有点棘手。以下是一些帮助你做出决策的提示。
-
当你想为不同类型提供相同的功能时,使用模板。例如,如果你想编写一个适用于任何类型的通用排序算法,使用函数模板。如果你想创建一个可以存储任何类型的容器,使用类模板。
-
当你想为相关类型提供不同的行为时,使用继承。例如,在一个绘图应用程序中,使用继承来支持不同的形状,如圆形、正方形、线条等。特定的形状然后从一个基类(例如,
Shape
)派生。
值得注意的是,你可以组合继承和模板。你可以编写一个从基类模板派生的类模板。
提供适当的检查和保护措施
有两种相反的编写安全代码的风格。最佳的编程风格可能是两者之间的健康组合。
-
契约式设计(Design-by-Contract):这意味着函数或类的文档代表了一份合同,详细描述了客户端代码的责任和你的函数或类的责任。契约式设计有三个重要方面:前置条件、后置条件和不变量。
-
安全最大化设计:这一准则的最重要方面是在你的代码中进行错误检查。例如,如果你的随机数生成器需要种子在特定范围内,不要只是信任用户传递一个有效的种子。检查传入的值,并在无效时拒绝调用。
为可扩展性设计
你应该努力以这样一种方式设计你的类,使它们可以通过从它们派生另一个类来进行扩展,但它们应该是封闭的,即行为应该是可扩展的,而无需你修改其实现。这被称为开闭原则(OCP)。
作为一个例子,假设你开始实施一个绘图应用程序。第一个版本应该只支持正方形。你的设计包含两个类:Square
和Renderer
。
class Square { /* Details not important for this example. */ };
class Renderer {
public:void render(const vector<Square>& squares) {for (auto& square : squares) {/* Render this square object... */}}
};
接下来,你添加对圆形的支持,所以你创建了一个Circle
类。
class Circle { /* Details not important for this example. */ }
为了能够渲染圆形,你必须修改Renderer
类的render()
方法。
在这个设计中,如果你想添加对新类型形状的支持,你只需要编写一个从Shape
派生并实现render()
方法的新类。你不需要在Renderer
类中修改任何内容。因此,这个设计可以在不修改现有代码的情况下进行扩展;也就是说,它是开放的,用于扩展和封闭的,用于修改。
参考:Professional C++ (English Edition) 5th Edition by Marc Gregoire
公众号:coding日记
相关文章:

精通代码复用:设计原则与最佳实践
精通代码复用:设计原则与最佳实践 在你开始设计的所有层次上,从单一函数、类,到整个库和框架,都需要从一开始就考虑到代码复用。在接下来的文本中,所有这些不同的层次都被称为组件。以下策略将帮助你合理地组织你的代…...

【static + 代码块+toString打印对象】
文章目录 static成员static修饰成员变量static成员变量初始化代码块 对象的打印写show方法打印对象调用toString打印对象 总结 static成员 举例:一个班的学生,在实例化每个人的名字,年龄,学号等学员信息时都不一样,但…...

【vue3 】 创建项目vscode 提示无法找到模块
使用命令创建 vue3 创建新应用 npm create vuelatest会看到一些可选功能的询问? √ 请输入项目名称: … vue-project √ 是否使用 TypeScript 语法? … 否 / 是 √ 是否启用 JSX 支持? … 否 / 是 √ 是否引入 Vue Router 进行单…...

盘点算法比赛中常见的AutoEDA工具库
在完成竞赛和数据挖掘的过程中,数据分析一直是非常耗时的一个环节,但也是必要的一个环节。 能否使用一个工具代替人来完成数据分析的过程呢,现有的AutoEDA工具可以一定程度上完成上述过程。本文将盘点常见的AutoEDA工具,欢迎收藏转…...

ICLR 2023丨3DSQA:3D 场景中的情景问答
来源:投稿 作者:橡皮 编辑:学姐 论文链接:https://arxiv.org/pdf/2210.07474.pdf 主页链接:http://sqa3d.github.io 图 1:3D 场景中情景问答 (SQA3D) 的任务图示。给定场景上下文 S(例如&#…...

ChatGPT的前世今生:从概念到现实的AI之旅
ChatGPT的前世今生:从概念到现实的AI之旅 随着技术的飞速发展,人工智能已经从科幻小说中的概念转变为我们日常生活中不可或缺的一部分。其中,ChatGPT无疑是这个领域的佼佼者。那么,让我们一起探索ChatGPT的发展历程,从…...

MINA架构DEMO
参考:Java中的MINA框架_java mina_小陈拾光的博客-CSDN博客 MINA:一个简洁易用的基于TCP/IP通信的JAVA框架。 <dependency><groupId>org.apache.mina</groupId><artifactId>mina-core</artifactId><version>2.1.5&…...

Linux基础:2:shell外壳+文件权限
shell外壳文件权限 一.shell原理:1.对比:windo GUI 和 shell1.windo GUI2. shell 2.为什么?是什么?怎么办?1.为什么有shell2.是什么?3.怎么办?4.补充: 二.linux权限管理:…...

webpack 解决:TypeError: merge is not a function 的问题
1、问题描述: 其一、存在的问题为: TypeError: merge is not a function 中文为: 类型错误:merge 不是函数 其二、问题描述为: 想执行 npm run dev 命令,运行起项目时,控制台报错 TypeErro…...

datahub 中血缘图的实现分析,在react中使用airbnb的visx可视化库来画有向无环图
背景 做大数据的项目,必不可少的是要接触到数据血缘图,它在大数据项目中有着很重要的作用。 之前在公司也做过一些案例,也看过很多友商的产品,阿里的DataWork,领英的Datahub, datawork的血缘图使用的是 G6…...

二、判断语句
文章目录 1.if语句1)if判断语句基本格式2) 网吧上网3)if语句使用逻辑运算 2.if-else语句1)if-else的使用格式2)网吧上网 3.多重判断elif语句1) 多重判断elif2)例子3)注意点 4.if嵌套…...

龙智汽车行业客户案例:Jira数据中心版助客户解锁高效项目管理
龙智技术支持部负责人、Atlassian认证专家叶燕秀分享了她帮助某汽车企业落地Jira的故事,并详解了该公司选择Jira数据中心版的理由以及工具链的集成情况,为有同样需求的公司提供实践参考。 本文由叶燕秀口述内容整理而成 需求管理:从Excel表格…...

03 vi编辑器
vi编辑器的三种模式: 不同的模式下机键动作解释的意义是不一样的 编辑模式 插入模式 末行模式 文件的打开和关闭保存 移动光标...

Web界面自动化操作工具 - Selenium常见用法
Selenium是一个用于自动化浏览器操作的工具,常用于Web应用程序的测试和爬虫开发。 下面是一些Python Selenium的常见用法和代码示例: 1. 导入Selenium库和WebDriver: from selenium import webdriver2. 创建WebDriver实例: # …...

Openssl数据安全传输平台009:加密理论基础:哈希/非对称加密RSA/对称加密AES
文章目录 0. 代码仓库代码编译时候可能出现的错误 1. 哈希1.1 哈希算法的种类:1.2 使用的头文件1.3 哈希算法API1.3.1 详解md5 API1.3.2 sha1/sha224/sha256/sha384/sha512常用API 1.5 sha1代码测试1.4 在VS中添加预处理器定义1.5 哈希算法C代码封装的思路 2. 非对称加密RSA2.1…...

iPhone开发--Xcode15下载iOS 17.0.1 Simulator Runtime失败解决方案
爆句粗口,升级后公司网络下载iOS 17.0.1 Simulator Runtime一直出错,每次出错后都得重新开始下载,oh,f**k。上一次在在家里的网络升级成功。 解决办法一: 进入网址:https://developer.apple.com/download…...

Galaxy生信云平台|Maftools高效地汇总、分析、注释和可视化肿瘤基因突变MAF文件...
2023-10-25,Galaxy中国镜像站 UseGalaxy.cn 平台新增 5 个工具。 MAF Tools Maftools-突变景观图: 绘制肿瘤基因突变景观图Maftools-突变汇总: 汇总MAF文件中的突变信息Maftools-共突变与互斥突变: 计算共突变和互斥突变Maftools-队列比较:比较两个队列之…...

JS三种常见的存储机制
1.localStorage localStorage是HTML5引入的一种持久化存储机制,用于在浏览器中长期保存数据。localStorage中存储的数据没有过期时间,除非被显式清除或代码删除。存储在localStorage中的数据对于同一个域名下的所有页面都是共享的。localStorage可以存储…...

【Python机器学习】零基础掌握BaggingClassifier集成学习
何提高分类模型的稳定性和准确性? 在金融风控、医疗诊断或者社交媒体推荐等场景中,分类问题是常见的难题。但是,单一的分类模型(如SVM)在处理复杂或不均衡的数据集时可能会表现不佳。那么,有没有一种方法能够提高模型的稳定性和准确性呢? 假设一家银行想要通过机器学习…...

[晕事]今天做了件晕事26;gcc对strcmp/strncmp的优化
今天做了一件晕事,写了一个测试小程序,开头的程序例如下面片段。在后续又写了一些代码,进行编译,使用gdb查看可执行文件,怎么都得不到想要的结果,非常的纳闷,非常的奇怪。 int main() {char a[3]="ab";int b = strncmp(0, a, 1<...

【深度学习】使用Pytorch实现的用于时间序列预测的各种深度学习模型类
深度学习模型类 简介按滑动时间窗口切割数据集模型类CNNGRULSTMMLPRNNTCNTransformer 简介 本文所定义模型类的输入数据的形状shape统一为 [batch_size, time_step,n_features],batch_size为批次大小,time_step为时间步长,n_feat…...

ts | js | 爬虫小公举分享
Curl转Code 快速将curl转为各种语言的代码; 便于提取请求头之类, 或者微改直接使用 https://curlconverter.com/node-axios/ (有点慢, 但是很全)https://www.lddgo.net/convert/curl-to-code (没有axios, 我喜欢用axios) 使用… 抓取地址, 使用浏览器或者其他抓包工具都可, 这…...

实现el-table打印功能,样式对齐,去除滚动条
实现el-table打印功能,样式对齐,去除滚动条 // 整个页面打印 function printTable(id) {// let domId #js_index// if (id) {// domId #${ id };// }// let wpt document.querySelector(domId);// let newContent wpt.innerHTML;// let oldContent document.…...

2022年09月 Python(一级)真题解析#中国电子学会#全国青少年软件编程等级考试
Python等级考试(1~6级)全部真题・点这里 一、单选题(共25题,每题2分,共50分) 第1题 表达式len(“学史明理增信 ,读史终生受益”) > len(" reading history will benefit you ")的…...

【面试经典150 | 链表】两数相加
文章目录 写在前面Tag题目来源题目解读解题思路方法一:模拟 其他语言python3 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法,两到三天更新一篇文章,欢迎催更…… 专栏内容以分析题目为主,并附带一些对于本题涉及到…...

vue路径中“@/“代表什么
举例: <img src"/../static/imgNew/adv/tupian.jpg"/>其中,/是webpack设置的路径别名,代表什么路径,要看webpack的build文件夹下webpack.base.conf.js里面对于是如何配置: 上图中代表src,上述代码就…...

java springboot2.7 写一个本地 pdf 预览的接口
依赖方面 创建的是 接口web项目就好了 然后包管理工具打开需要这些 import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; imp…...

RustDay06------Exercise[81-90]
81.宏函数里面的不同的匹配规则需要使用分号隔开 // macros4.rs // // Execute rustlings hint macros4 or use the hint watch subcommand for a // hint.// I AM NOT DONE#[rustfmt::skip] macro_rules! my_macro {() > {println!("Check out my macro!");};($…...

【Docker从入门到入土 6】Consul详解+Docker https安全认证(附证书申请方式)
Part 6 一、服务注册与发现的概念1.1 cmp问题1.2 服务注册与发现 二、Consul ----- 服务自动发现和注册2.1 简介2.2 为什么要用consul?2.3 consul的架构2.3 Consul-template 三、consul架构部署3.1 Consul服务器Step1 建立 Consul 服务Step2 查看集群信息Step3 通过…...

PCIe架构的处理器系统介绍
不同的处理器系统中,PCIe体系结构的实现方式不尽相同。PCIe体系结构以Intel的x86处理器为蓝本实现,已被深深地烙下x86处理器的印记。在PCIe总线规范中,有许多内容是x86处理器独有的,也仅在x86处理器的Chipset中存在。在PCIe总线规…...