|
此书是很好的一本书:大规模C++程序设计
引言
与主流观点相反,从根本上说,最普通形式的面向对象程序要比对应的面向过程的程序更难测试和校验。通过虚函数改变内部行为的能力可能导致类不变式无效;而对于程序的正确性来说,类不变式是必要的。
第一部分 基础知识
对符号名称的使用,而不是声明本身,导致一个未定义符号被引入到.o目标文件中。
typedef声明是的性质属于"internal linkage",尽管这个说法不严谨,但是很容易理解──文件A无法使用文件B中typedef的类型。
C++中不可能未经定义就声明一个枚举类型,即对于class合法的foward declation,并不适用于enum。
typedef只是提供了”创建新类型“的假象。
类之间的逻辑关系:每一种都暗示着两个逻辑实体之间不同程度的物理依赖
IsA
Uses-In-The-Interface
Uses-In-The-Implementation
继承和分层是逻辑层次结构的两种表现形式
分层:较高抽象层次的类依赖较低抽象层次的类(人依赖于脑、胃,可以理解为组合)
继承:具体的类依赖抽象的类
避免在头文件的file scope中使用枚举、typedef和常量数据;它们都默认为internal linkage。更好的方法是将他们放入头文件的class scope中。
头文件的"外部冗余#include guard"
理论上将,头文件的"内部 #include guard"就足够了,但是对于大型项目,事情并不这么简单
对于具有高密度头文件包含图的大型项目,在每个头文件中的每个#include指令周围,加上一个冗余的”外部#include guard“,对于降低编译时间,能起到显著作用。
注意,对于.cpp文件,外部冗余#include guard并不是必需的。
第二部分 物理设计概念
第三章、组件
3.1 组件和类
从纯粹的逻辑角度看,一个设计可以看作是类和函数的海洋,那里没有物理分区存在──每一个类和自由函数都驻留在一个单一的无缝空间内。
组件 (component)是物理设计的最小单位。
组件不是类,反之亦然。一个组件一般包括一个或多个相关的类,以及和类相关的非成员函数。
在结构上,组件是一个不可分隔的物理单位。一个组件严格的由一个头文件和一个实现文件组成。
组件的逻辑接口:客户可通过编程访问或检测到的东西
组件的物理接口:头文件中的所有东西
从逻辑角度看,组件的实现文件中有或没有使用什么都是封装的细节,并不重要;从物理角度看,这样的使用可能隐含着对其它组件的物理依赖。在大型系统中这些物理依赖会影响可维护性和可重用性。
3.2 物理设计规则
1.在一个组件内部声明的逻辑实体,不应该在该组件之外定义(类声明除外)。
2.组成一个组件的.cpp和.h的文件名应该严格匹配
3.每个组件的.cpp文件应该将#include其对应头文件作为其代码的第一行有效语句,这样每个组件可以确保它自己的头文件对于编译来说都是自我满足的,组件的头文件中不会遗漏组件物理接口的关键信息──如果有的话,在编译组件时就会发现错误。
4.当需要某个类型的定义时,应该直接包含其头文件,而不应该依赖另外一个头文件去包含这个头文件。
一个头文件是否应包含另一个头文件是个物理问题,不是逻辑问题。
5.在组件的.cpp文件中,避免出现具有外部连接、却没有出现在相应的.h文件中声明过的定义──确保外界在只看到物理接口的情况下就能完全了解组件的逻辑接口是很重要的。
6.避免通过一个局部声明来访问另一组件中带有外部连接的实体,而是应该通过包含那个组件的头文件来完成。这么做的主要目的是让对其它组件的依赖显性化,
3.3 依赖关系(Depends-on)
Depends-on的含义是物理依赖,而不是逻辑依赖
组件之间的编译期依赖:编译y.cpp时需要x.h,则组件y对组件x存在编译期依赖
组件之间的连接期依赖:若对象文件y.o需要x.o来解析自身包含的未定义符号,则组件y对组件x存在连接期依赖
一个编译期依赖几乎总是隐含着一个连接期依赖
组件的Depends-on关系具有传递性
3.4 隐含依赖
逻辑关系隐含物理依赖。IsA和HasA这样的逻辑关系,在跨组件实现时总是隐含编译依赖;HoldA和Uses这样的关系可能隐含跨组件边界的连接时依赖
3.5 提取实际的依赖
可以通过提取源文件(.h和.cpp)中的#include命令,直接构建组件依赖图
3.6 友元关系
友元关系和物理设计之间的交互程度强烈的令人惊讶;友元关系表面是个逻辑关系,却会影响物理设计
避免将友元关系授予给定义在另一个组件中的逻辑实体
从严格意义上说,友元声明本身是类接口的一部分,而友元的实现却不是
与类处于同一组件中的友元的存在,并不会破坏类的封装性;
远距离友元关系(对另一个组件中的逻辑实体授予友元关系),则会破坏该类的封装性──客户代码完全可以”冒名顶替“,这可能导致严重的安全问题
友元关系并不隐含物理依赖
第四章、 物理层次结构
中心问题是避免循环物理依赖
4.3 测试”好“接口时的困难
面向对象技术的一种实际有效的应用是把极大的复杂性隐藏在一个小的、定义良好的、易于理解和使用的接口后面,但是,正是这种接口(如果被不成熟的实现)会导致子系统测试起来及其困难
4.4 易测试性设计(Design for Testability)
DFT的重要性在IC工业界是公认的
对整个设计的层次结构进行分布式测试,比只在最高层接口进行测试有效的多
虽然大型软件的复杂度要比IC复杂很多,但本质上软件测试要比硬件测试要容易──类的不同实例肯定有一致的行为,然而不同的晶体管却未必都是合格的
4.6 非循环物理依赖
对于一个能够有效测试的设计来说,一定能够将其分解为复杂性可以控制的功能单元
不含循环的组件依赖图非常有利于实现易测试性;非循环物理依赖的系统远比循环物理依赖的系统容易进行有效的测试
对于非循环物理依赖的系统,至少存在一个隔离测试的合理顺序
4.7 层次号
每个有向非循环图的结点都可被赋予唯一的层次号;一个循环的图则不能
一个可被赋予唯一层次号的物理依赖图被称为可层次化的
子系统中组件的层次号
层次0:软件包之外的组件
层次1:没有局部物理依赖的组件
层次N:依赖于层次N-1以下组件的组件
总是可以对层次为1的组件进行独立测试
大多数情况下,如果大型系统要被有效的测试,它们必须是可层次化的。
注意,层次化这一术语适用与物理实体而不是逻辑实体
可层次化分析的最大价值在于,基于组件依赖图,能够对物理设计的完整性做出实质性的定性评论;且可层次化的分析是很容易自动化的。
4.8 分层次测试和增量测试
分层次测试:为每个组件提供一个独立的测试驱动程序
增量测试:只测试一个组件中直接实现的功能,对通过转发给低层次组件完成的功能不进行测试
4.11 循环物理依赖
设计经常开始于非循环依赖,随着设计的演化,在系统功能增强的过程中,循环依赖会悄悄的混进来
紧密相关的类之间相互依赖很平常,但是它们应该完全驻留在同一个组件中
4.12 累计组件依赖(CCD:Cummulative component dependent)
CCD:对一个系统中的所有组件执行增量式测试时需要的组件数量的总和
连接大型程序要花费很长的时间
当依赖关系形成完美二叉树时,对于CCD是最佳情况,为N*logN级别的
对于循环依赖,CCD则是N*N
4.13 物理设计的质量
CCD可用于衡量物理设计的质量
尽管物理依赖的可层次性是很好的设计特性,但是不同的可层次性结构之间也存在优劣
例如”垂直“的链状可层次结构,,具有高度的耦合性,CCD的值较大;与之相比,二叉树形式的可层次化结构,CCD的值较小
通过使得依赖图更平而不是更高,我们增强了灵活性。设计越扁平,独立重用的潜力就越大
平均组件依赖(ACD)=子系统的CCD与组件数N的比值
将一给定组件集合的CCD最小化是一个设计目标
标准累计组件依赖(NCCD)=包含N个组件的子系统的CCD值与相同规模平衡二叉树对应CCD的比值
通过NCCD,能够把子系统的物理依赖区分为水平、树状、垂直或循环的
第五章、 层次化
在衡量一个系统的物理设计质量时,以CCD量化的连接时依赖扮演主要角色。系统质量的其它方面如可理解性、可维护性和可重用性,都紧密的依赖于物理设计的质量。
好的逻辑设计和好的物理设计之间是存在协同作用的
5.2 升级
如果组件y的层次高于组件x,且y在物理上依赖于x,则称组件y支配组件x
支配的意义在于能提供简单的层次好之外的附加信息:当且仅当组件u不支配v时,添加v对u的依赖才不会引入循环依赖。
可以通过将原有相互依赖关系转换并上推到更高层次的组件中,将循环依赖变成受欢迎的向下依赖。
5.3 降级
将循环依赖组件中的共用功能下移到物理层次更低的组件中,被称为降级──原来的组件依赖于这个新组件
将共有代码降级可促成独立重用
5.4 不透明指针
实质使用 VS 名称上使用
如果一个指针所指向的类型的定义不在当前编译单元内,这个指针被称为不透明的(opaque)
不透明指针的意义在于让一个对象只在名称上使用另一个对象
5.5 哑数据
哑数据是对不透明指针概念的一种范化
哑数据可以用来打破in-name-only依赖,但是不透明指针可以同时保持类型安全和封装,而哑数据通常是不能的。
哑数据的使用是典型的低层次实现细节,通常不会暴露在较高层次子系统的接口中
5.6 冗余
任何种类的复用都隐含着某种形式的耦合;冗余指的是为了避免由重用导致的不必要的物理依赖而故意重复代码或数据的技术
如果重用所提供的功能只有很少部分是我们所需要的,然而却导致不合比例的大量耦合的话,冗余就是必要的、也是更合理的机制。
简而言之,重用很少没有开销(编译器和连接期),在它带来的益处抵得过开销时,才是一种良好的选择。
5.7 回调
回调的意义在于允许较低层次的组件,使用客户提供的函数,在更全局的上下文中执行特定任务
回调是消除物理耦合的强有力工具,但是应该只在必要的时候使用它们
在面向对象中,通常利用虚函数来实现“回调”;这对应面向过程中,利用函数指针来实现回调。基于虚函数机制的优点在于保证类型安全
回调,如同递归一样,比传统的函数调用更难以理解
5.8 管理类
管理类的意义建立一个拥有和协调较低层次对象的类
在协同操作对象之间建立清晰的所有权关系,是良好设计的基础。如果两个或更多对象之间存在所有权的相互依赖,则应将所有权的管理功能提升到一个管理类中。
经典范例:图中Node和Edge的相互关系
5.9 分解
分解(factoring)的意思是提取小块的内聚功能,并把它们移到一个较低的层次,以便可以被独立的测试和重用
分解是减轻循环依赖的类所强加的负担的一种非常普通而高效的技术
分解和降级类似,只是分解不必然消除任何循环,取而代之的是它只减少参与到循环中的功能的数量。通过分解可以将循环依赖升级到一个更高层次,在那里它们的不利影响较不显著。
5.10 升级封装
将大量有用的低层次类藏在一个单个组件的接口后面,这样的组件被称为包装器(wrapper),或者理解成应用Facade模式
第六章、绝缘
好的物理设计的另一个方面就是避免不必要的编译依赖
一般来说,对驻留在一个组件的物理接口中的、客户无权访问的实现细节的修改,将强迫所有的客户程序重新编译。
绝缘是指避免或消除不必要的编译时耦合的过程。
6.1 从封装到绝缘
绝缘是一个物理设计问题,它是逻辑设计中封装的对应物。
封装:围绕着类的实现的极薄的透明膜,只能防止通过编程来访问类的实现,但是客户仍能看到部分实现
绝缘:无限厚不透明的障碍,它排除了客户与组件的实现进行直接交互作用的任何可能性
在大型系统中,修改绝缘组件要比非绝缘组件容易的多
6.2 C++结构和编译时耦合
有时组件的逻辑和物理分解是自然一致的,例如类的非内联成员函数,逻辑接口(声明)存在于物理接口中,逻辑实现(函数体)存在于物理实现中。这种情况下,声明并没有暴露除接口外的更多信息。
C++并不要求所有逻辑实现的细节都存放于.cpp文件中。
以下列出的各种情况,都会使得逻辑实现成为组件物理接口的一部分
6.2.1 继承与编译耦合
一个类只要继承自另一个类,即使是私有继承,也可能没有办法把客户程序与这个事实绝缘
6.2.2 分层(HasA/HoldsA)和编译耦合
后者比前者的导致的耦合程度要弱
6.2.3 内联函数与编译耦合
无论何时将一个组件的实现的一部分放到头文件中,都无法再将客户与组件的这部分绝缘
6.2.4 私有成员和编译耦合
类个每个私有数据成员──尽管封装好了──也没有与该类的客户程序绝缘
私有成员函数是类的封装细节,然而它们并不是绝缘的实现细节──对私有函数原型的修改会迫使客户代码重新编译
6.2.5 保护成员和编译耦合
同私有成员一样,它们也不是绝缘的实现细节
6.2.6 编译期生成的函数与编译耦合
同样不是绝缘的
6.2.7 #include和编译耦合
在头文件中应谨慎使用 #include指令,避免引入不必要的编译依赖
6.2.8 默认参数与编译耦合
不具备绝缘的性质
6.2.9 枚举类型和编译时耦合
枚举类型不具备外部连接,如果要在多个组件中被使用,只能将定义放在头文件中
6.3 部分绝缘技术
绝缘不是一个“要么全有,要么全无”的命题
6.3.1 消除私有继承
私有继承意味着实现细节
6.3.2 消除嵌入数据成员
将HasA关系转为HoldsA,可以实现内部嵌入数据和客户类之间的绝缘
6.3.3 消除私有成员函数
私有成员函数尽管封装了类的逻辑实现细节,但仍然是组件物理接口的一部分
6.3.4 消除保护成员
6.3.5 消除私有成员数据
6.3.6 消除编译时产生的函数
6.3.7 消除#inlucde指令
组件头文件中出现#include指令,通常有三种合理的原因:
1) IsA关系,需要包含基类的头文件
2) HasA关系,需要包含嵌入类的头文件
3)inline函数,该组件中的某个inline函数实质使用到了另一个组件中的类
除此之外,在头文件中包含#include指令,很少是有道理的
解决方法很简单:将头文件中不必要的#include指令移动到对应的.cpp文件中,并替换为适当的前置声明(注意,这里的观点存在争议,C++编程规范第23条认为“头文件应该自己自足,为此需要包含其内容所依赖的所有头文件")
6.3.8 消除默认参数
6.3.9 消除枚举类型
6.4 整体的绝缘技术
分清楚哪些是公共接口哪些不是公共接口,这种知识可以帮助我们决定哪些接口应该绝缘,哪些接口不应该绝缘
6.4.1 协议类
理想情况下,一个绝缘良好的接口绝对不会定义任何实现细节:
1) 不含任何数据
2)非内联的虚析构函数(函数体为空)
3)所有成员函数都是纯虚的
一个协议类几乎是一个完美的绝缘器
6.4.2 完全绝缘的具体类
满足如下条件的具体类,是完全绝缘的
1) 只包含一个数据成员,为不透明指针,即所谓的pImpl惯用法
2)不继承任何其它类
3)不声明任何内联成员函数
所有完全绝缘的(具体)类的物理结构在外表上都是一样的──都可以在不影响任何头文件的情况下进行修改
6.4.3 绝缘的包装器
包装器不仅可以进行封装,还可以进行绝缘
6.5 过程接口
当主要目标是把客户程序与在一个绝缘层下(为一个非常复杂的系统所建立的)之下的东西都绝缘的话,必需采取某种妥协:放弃更强的的逻辑封装,以换取更好的绝缘。
组织方式:所有公共可访问的接口函数都是彼此独立的,它们之间不存在层次化问题,不应该彼此依赖
过程接口的目的不是封装组件的使用,而是把客户程序和它们使用的对象定义绝缘。
6.6 绝缘或不绝缘
绝缘是会导致性能开销的
如果某个组件不会被广泛使用的话,绝缘就不是关键特性
对于轻量级、广泛使用的对象进行绝缘,很可能导致严重的性能下嫁
对于大型的、广发使用的对象,要尽早进行绝缘──后期如果有必要,可以选择性的去除
在缺少压倒性理由的情况下,无论如何请记住:在开发过程的后期,绝缘的去除比绝缘的安装更经济
第三部分 逻辑设计
逻辑设计是比物理设计更为成熟和更容易理解的规则
设计模式关注的焦点是逻辑设计
本部分的内容只讨论单个组件的设计与实现
第八章、构建一个组件
8.2 组件设计规则
1. 私有接口应该是充分(sufficient)的
对于被设计为特定子系统一部分的组件,我们只要求接口对于可预期的客户来说是充分的就可以了。”充分性“指的是,该接口对于解决某个特定领域的问题来说是合适的
2. 公共接口应该是完整(complete)的
对于在整个大系统中会被用于各种目的的组件来说,我们希望组件的借口是完备的。”完备性“指的是,该借口适用于解决那个领域中的任意一个问题。
3. 类接口应该是基本的(primitive)──如果一个操作的有效实现需要访问类的私有细节,那么这个操作就是基本的。有用但不是基本的操作硬尽可能的通过类外的自由函数来实现。
4. 组件接口应该是最小化和易于使用的
在任何可行的地方,延缓不必要功能的实现可以降低开发和维护成本,而且可以避免过早的进行精确的接口和行为设计。
在充分性和完整性之间,存在了广阔的中间地带
让功能保持在一个可行的最小范围内可以增强可用性和可重用性
在一个组件中尽可能少的使用在外部组件中定义的类型,可以促进在更多情况下的重用
耦合这个术语对逻辑设计和物理设计都适用。物理耦合来源于将逻辑实体放在同一组件中。逻辑耦合来源于在一个组件接口中使用由其它组件定义的类型。
逻辑耦合常常导致不受欢迎的物理耦合
8.3 封装程度
和绝缘一样,封装的实现往往会带来性能开销
有时,选择不完全的封装是正确的选择
第九章、设计一个函数
9.2 接口中的基本类型
避免使用short,用int代替──理由,整形提升
避免使用无符号数,使用有符号数──理由,signed到unsigned的自动转换
避免使用long,用int代替──理由,可移植性
对于整数类型,只使用int
对于浮点类型,只使用double
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/lovekatherine/archive/2008/04/13/2287980.aspx |
|