C++ 设计 笔记
什么是
提供了对某个问题的抽象,以及客户与解决该问题的软件组件之间进行交互的方式
概括的说,定义了一些复用的模块,使得各个模块化功能块可以嵌入到最终用户的应用程序中去
是一个明确定义的接口,可以为其他软件提供特定的服务
契约 承包人
通常会包含如下的元素:
- 头文件
- 一组头文件。头文件定义了接口,使得客户端代码能够针对该接口进行编译。开源还包括实现的源代码
- 类库
- 一个或多个静态库或动态库文件,它们提供了的具体实现。客户端可以把它们的代码和这些库文件进行链接,从而为它们的应用程序添加相应的功能
- 文档
- 如何使用的概述信息,通常包括为中所有的类和函数自动生成的文档
是软件组件的逻辑接口,隐藏了实现这个接口所需的内部细节
接口是开发者编写的最重要的代码。因为比起相关的实现代码出现问题,修复接口出现的问题代价要大得多
开发直接关系到项目的成败,举例说明一些关键因素:
- 是为开发者设计的接口
- 多个应用程序可以共享一个
- 修改的同时,必须尽可能保证向后兼容
- 出于向后兼容的需求,一定要具有变更控制流程
- 的生存周期一般都比较长
- 在编写时,良好的文档必不可少,特别是当不提供实现的源代码时
- 自动化测试同样很重要
描述了其他工程师构建他们的应用软件所使用的软件。因此,必须拥有良好的设计、文档、回归测试,并且保证发布之间的稳定性
为什么使用
更健壮的代码
- 隐藏实现
- 延长寿命
- 促进模块化
- 减少代码重复
- 消除硬编码假设
- 易于改变实现
- 易于优化
代码复用
并行开发
何时应当避免使用
许可证限制
功能不匹配
缺少源代码
缺乏文档
每当你创建一个文件格式或者客户端/服务端协议时,同时也要为其创建。这样,规范的细节以及未来的任何变更都将是集中且隐蔽的
例如:如果你为应用程序的数据指定了文件格式,那么你应该编写读写此文件格式的
特征
优质的应该设计精妙且实用性强
问题域建模
提供良好的抽象
关键对象的建模
隐藏实现细节
物理隐藏:声明与定义
声明告诉编译器某个标识符的名称及类型。定义提供该标识符的完整细节,即它是一个函数体还是一片内存区域
逻辑隐藏:封装
封装是将的公有接口与其底层实现分离的过程
隐藏成员变量
类的数据成员应该始终声明为私有的,而不是公有的或保护的
直接访问公有成员变量的代码比通过方法访问成员变量的代码快2-3倍(但也不应该暴露成员变量)
即使你再编写注重性能的,谨慎的使用内联,结合现代编译器优化实现,通常会完全消除方法调用的开销,使你获得与暴露成员变量等同的性能
隐藏实现方法
信息隐藏的原则是:将类的固定接口与其内部设计实现相分离
永远不要返回私有数据成员的非const指针或引用,这会破坏封装性
强烈建议在中采用惯用法,这样就可以将所有的实现细节完全和头文件分开。如果你不想这么做,至少也要将头文件内不需要的私有方法移到文件中,并将它们转换为静态函数。但只有当私有方法仅访问类的公有成员或者根本不访问任何类成员时才能这么做(例如接收文件名字符串,然后返回改文件名的扩展名的程序)
隐藏实现类
除了隐藏类的内部方法和变量之外,还应该尽力隐藏那些纯粹是实现细节的类
最小完备性
优秀的设计应该是最小完备的。即它应该尽量简洁,但不要过分简洁
谨记奥卡姆剃刀原理:若无必要,勿增实体
不要过度承诺
中每个公有元素都是一项承诺,它承诺了该功能在的生命周期中都将得到支持
当不确定是否需要某个接口时,就不要提供此接口
- 你想要添加的通用性可能永远不会用到
- 如果某天用到了想要添加的通用性,那时你可能已经掌握了更多设计知识,并可能有了与最初设想方案不同的解决方案
- 如果你确实需要添加新功能,那么简单的比复杂的更容易添加新功能
因此,应该保证尽量简单:类及类中的公有成员暴露得越少越好。这样做的好处是更容易于理解,更易于用于记住模型,而且更易于调试
疑惑之时,果断弃之!精简中公有的类和函数
谨慎添加虚函数
继承(将某个成员函数设置为虚函数)暴露出的功能可能会超出预期,而且这种方式并不易察觉。客户可以通过继承中的类重新实现任意虚方法
- 对基类看似无害的修改可能会给客户带来不利的影响
- 客户可能会以你根本无法预料的方式使用
- 客户可能采用不正确的或易于出错的方式扩展
- 重写函数可能破坏类的内部完整性
- 虚函数的调用必须在运行时查询虚函数表决定,而非虚函数调用在编译时就能确定。这就使得虚函数调用比非虚函数调用慢。事实上这部分开销可以忽略
- 使用虚函数一般需要维护指向虚函数表的指针,进而增加了对象的大小。创建需要很多实例的小对象时,这可能会成为一个问题。尽管如此,与各种成员变量实际所占用的内存空间相比,这可能是微不足道的
- 添加、重排或移除虚函数会破坏二进制兼容性。因为虚函数调用通常用类的虚函数表的整型偏移量表示,所以改变虚函数的顺序,或引起其他虚函数的顺序发生变化的操作,都需要重新编译现有代码,以确保客户仍然调用正确的方法
- 不是所有的虚函数都能内联,因为将虚函数声明为内联是没有任何意义的。因为虚函数时运行时决定的,而内联是在编译时进行优化。但是在有一定约束的情况下,编译器可以内联虚函数。即便如此,同内联的非虚函数的情况相比,内联虚函数的例子少之又少
- 重载虚函数时需要技巧的。尽量避免重载虚函数
没有虚函数的类比有虚函数的类更健壮且更易于维护
- 如果类包含任一虚函数,那么必须将析构函数声明为虚函数。这样子类就可以释放其可能申请的额外资源
- 一定要编写文档,说明类的方法是如何相互调用的
- 绝不在构造函数或者析构函数中调用虚函数,这些调用不会指向子类
避免将函数声明为可以重写的函数(虚函数),除非你有合理且迫切的需求
便捷
便捷包装(指封装了多个调用的实用程序,它能提供更简单的高层操作)
不要将便捷与核心混在同一个类中。也就是说应该创建补充类包装核心的某些公有功能
便捷类应该与核心完全隔离,比如将它们放在不同的源文件中甚至是完全独立的库中
- 确保便捷只依赖于核心的公有接口,而不依赖于任何内部的方法和类
- 应该通过易用接口来呈现基本功能,同时将高级功能隐藏在另一个独立的层次中
基于最小化的核心,以独立的模块或库的形式构建便携
易用性
优秀的设计应该是简单的任务更简单,使人一目了然
遵循最小惊奇原则:采用现有的模型和模式实现最小惊奇,这样用户就能集中精力处理手头的任务,而不会因为接口的新奇或繁杂分散精力
可发现性
可发现的要求用户能够自身明白如何使用它们,而不需参阅任何解释或文档
不易误用
最常见的误用的方式是向方法传递错误的参数或非法值
使用枚举类型代替类型,提高代码可读性
对于不能解决的复杂情况,为了确保每个参数都有唯一的类型,甚至可以引入新类(p35)
避免编写拥有多个相同类型的参数
一致性
优秀的应该采用一致性的设计方法,以便于用户记住其风格,进而更容易被用户采用
- 命名约定
- 参数顺序
- 标准设计模式的使用
- 内存模型语义
- 异常的使用
- 错误的处理
使用一致的函数命名和参数顺序
通过多态,可以毫不费力的达到一致性目标:将共享的功能放到一个公共的基类中。但是,让所有的类都继承该公共基类并没有意义,而且不应该纯粹为了达到一致性而引入基类,因为它增加了接口的复杂性以及类的数目
正交
在设计中,正交性意味着方法没有副作用。调用设置特定属性的方法应该仅改变那个属性,而不能额外改变其他可以公共访问的属性
- 增加独立性,确保暴露得概念没有重叠。任何重叠的概念都应该分解到它们的基础组件中
健壮的资源分配
常见误用指针或引用:
- 对解引用:尝试对指针使用或操作
- 二次释放:对一块内存调用两次或
- 访问非法内存区域:对尚未分配或已被释放的指针进行操作
- 混用内存分配器:用释放由分配的内存,或用释放分配的内存
- 数组释放不正确:使用操作符而非释放数组
- 内存泄露:内存使用完没有释放
智能指针:
- 共享指针:引用计数指针,即当一段代码要保留该指针时,引用计数加一,反之减一;当计数为0则自动释放
- 弱指针:弱指针包含一个指向对象的指针,通常是共享指针,但是并不增加其指向的对象的引用计数。如果一个共享指针和一个弱指针引用了相同对象,那么在共享指针被销毁时,弱指针的值会立即变为。通过此方式,弱指针可以检测其所指向的对象是否已经过期:即其指向对象的引用计数是否为0
- 作用域指针:作用域指针仅属于单一对象,并且当作用域指针超出其作用域时自动释放所指向对象
将资源的申请与释放当做对象的构造与析构
平台独立
几乎不会因平台而异
不要将平台相关的或语句放在公有中,因为这些语句暴露了实现细节,并使因平台而异
松耦合
耦合:软件组件之间相互连接的强度的度量,及系统中每个组件对其他组件的依赖程度p44
内聚:单个软件组件内的各种方法相互关联或聚合强度的度量
优秀的软件设计应该是低耦合且高内聚的,即最小化不同组件之间功能的关联性和连通性
一种理解耦合的方式是给定A和B两个组件,当A改变时需要改变B中多少代码。评估组件之间的耦合度可以采用下面几种不同的度量方式:
- 尺度:与组件之间的连接数有关,包括类的数目、方法的数目、每个方法的参数数目等
- 可见度:指组件之间的连接的显著程度。例如,改变一个全局变量以便间接影响另一个组件的状态,这种可见度低
- 密切度:指组件之间连接的直接性。如果AB耦合,且BC耦合,那么A间接与C耦合。继承某个类比引入该类的成员变量的耦合度要高,因为继承还支持访问类中的所有受保护的成员
- 灵活度:与改变组件之间连接的难易程度相关
仅通过名字耦合
除非确实需要类的完整定义,否则应该为类使用前置声明
如果类A仅需要知道类B的名字,不需要知道类B的大小或调用类B的任何方法,那么类A就不需要依赖类B的完整声明
降低类耦合
如果情况允许,那么优先声明非成员、非友元的函数,而非成员函数。这么做在促进封装的同时还降低了这些函数和类的耦合度
在设计类的时候,一个原则就是对于不改变数据成员的函数都要在后面加上,而对于改变数据成员的函数不能加
刻意的冗余
优秀的软件工程实践的目标是去除冗余,即确保每个重要的知识点或者行为有且仅有一次实现。而代码复用意味着耦合,因此略微增加重复以断绝过分的耦合关系有时是值得的
管理器类
管理器类可以通过封装几个低层次类降低耦合
回调、观察者和通知
重入:当编写需要未知的用户代码调用时,需要考虑该代码可能回调此
生命周期管理:给用户提供一种能够干净利落的与此断开连接的方式,即声明它们不再接收更新消息
事件顺序:回调或通知的序列应该明确告知用户
回调的特性可以使得底层代码能够执行与其不能有依赖关系的高层代码。因此,在大型项目中,回调是一种用于打破循环依赖的常用技术
回调和观察者适用于特定的任务,它们的使用机制通常定义在执行实际回调的对象中。一个替代方案是,在系统中不连通的部分之间构建集中发送通知机制或事件。发送者事先不需要知道接收者,这样可以降低发送者和接收者之间的耦合度
信 和槽:在实际应用中,低层次的类可以创建并拥有信 ,然后允许任何不连通的类成为该信 的槽。然后低层次的类可以在任意恰当的时间发出信 ,进而所有已连接的槽都会被调用
稳定的、文档详细且经过测试的
- 优秀的设计应该是稳定的且具有前瞻性
- 稳定并不意味着不会改变,而是应该将接口版本化,并且在版本升级时保证向后兼容
- “前瞻性”表示应该设计为可扩展的,以便它能优雅的升级而不是被修改得一塌糊涂
- 优秀的设计也应该有很好的文档支持,以便用户获取的功能、行为、最佳实践以及错误条件的明确信息
- 应该为的实现编写可扩展的自动化测试程序,确保新的变更不会破坏现有的用例
设计模式
对用来解决特定场景下解决一般设计问题的类和互相通信的对象的描述
- 模式名:一个助记名,用一两个词来描述模式的问题、解决方案和效果
- 问题:描述了应该何时使用模式
- 解决方案:描述了设计的组成部分、他们之间的相互关系及各自的职责和协作方式
- 效果:描述了模式应用的效果及使用模式应该权衡的问题
创建型模式
抽象工厂模式
-
创造一组相关的工厂
-
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类
建造者模式
- 将复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示
工厂方法模式
- 将类的实例化推迟到子类中
- 定义一个用于创建对象的接口,让子类决定将哪一个类实例化
原型模式
- 指定类的原型实例,克隆该实例可以生成新的对象
- 用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象
单例模式
- 确保一个类只有一个实例,并提供一个访问它的全局访问点
结构性模式
适配器模式
- 将类的接口转换为客户希望的另一种接口。该模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作
桥接模式
- 将抽象部分与它的实现部分分离,使它们都可以独立的变化
组合模式
- 将对象组合成树形结构,表示“部分——整体”的层次结构
- 该模式使得客户对单个对象和复合对象的使用具有一致性
装饰模式
- 动态地给一个对象添加一些额外的行为(职责)
- 就扩展功能而言,该模式比生成子类的模式更为灵活
外观模式
- 为子系统中的一组接口提供统一的高层次接口,这个接口使得这一子系统更加容易使用
享元模式
- 利用共享技术高效的支持大量细粒度的对象
代理模式
- 提供另一个对象的替代物或占位符,以便控制对该对象的访问
行为模式
职责链模式
- 使多个接收者对象有机会处理来自发送者对象的请求
- 为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它
命令模式
- 将一个请求或操作封装成一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作
解释器模式
- 指定如何对某种语言的语句进行表示和判断
- 给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子
迭代器模式
- 提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示
中介者模式
- 定义一个中介对象,用于封装一组对象的交互
- 中介者使各对象不需要显式的相互引用,从而使其耦合松散,而且可以独立的改变它们之间的交互
备忘录模式
- 捕获对象的内部状态,以便将来可将该对象恢复到保存的状态
- 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到保存的状态
观察者模式
- 定义对象间的一种一对多的依赖关系,当对象的状态发生改变时,所有依赖于它的对象都将得到通知并自动刷新
状态模式
- 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类
策略模式
- 定义一组算法并封装每个算法,使他们在运行时可以相互替换
- 定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。该模式使得算法的变化可独立于使用它的客户
- 何时使用
- 当一个系统有很多类,而区分它们的只是它们直接的行为时就可以使用该模式
- 缺点
- 策略类数量会增多,每个策略都是一个类,复用的可能性很小
- 所有的策略都需要对外暴露
- 使用场景
- 多个类只有算法或行为上稍有不同的场景
- 算法需要自由切换的场景
- 需要屏蔽算法规则的场景
- 注意事项
- 如果一个系统的策略多于4个,就需要考虑混合模式来解决策略类膨胀的问题
模板方法模式
- 定义一个操作中的算法的框架,将其中一些步骤推迟到子类中。该模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤
访问者模式
- 表述对某对象结构的元素所执行的操作
- 表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!