物理设计概念 -- 包

本文探讨了在IT技术中包的组织原则,包括包前缀、名称空间的使用,以及包层次化的重要性和技术。还介绍了多种初始化策略,如唤醒初始化、显式init函数和灵巧计数器,以优化大型程序的启动时间和内存管理。
摘要由CSDN通过智能技术生成

包将相关的组件的集合聚集为一个逻辑上内聚的物理单位。每个包都有一个与之关联的注册前缀,它直接将文件和文件作用域逻辑结构标识为属于哪个包。

从组件到包

一个包就是被组织成一个物理内聚单位的组件的集合

术语包通常是指一个非循环的、层次化的组件的集合,这些组件共同有着一个内聚的语义目标。在物理上,一个包由一个头文件的集合和一个包含相应的目标文件中的信息的单个库文件组成。一个包可能由低层次、可重用的组件的一个松散耦合构成。一个包也可能由一个有特殊用途的、只打算给一个客户使用的子系统构成。

如果包x中的一个或者多个组件依赖另一个包y中的一个或者多个组件,则成包x依赖包y

包是物理设计的一个聚集单位。就像一个组件一样,包是一个完成共同目的的相关功能的内聚单位。包既是体系结构设计者的抽象又是开发人员的分区。包的构成由若干因素决定,包括语义内聚度、物理依赖的性质、开发队伍的组织以及独立重用的潜能。

注册包前缀

对前缀的需求

这里采用的方法能确保唯一的全局类名称,它要求每一个包都与一个唯一的由2-5个字符组成的注册前缀相联系。当一个包第一次被创建时,其前缀在某个和公司范围的权限或服务上注册,这样其他包开发这就不会无意中重用它。头文件中的每一个在文件作用域中声明的结构都将被附加包前缀。实现这个组件的源文件和头文件也都要附加同样的前缀。通过对每个全局名称都附加这个注册的前缀,我们能够保证定义在不同的包里的相似名称不至于冲突

为每个全局标识符都附加它的包前缀

为每一个源文件名称附加上它的包前缀

对于许多系统来说,对文件名称长度有严厉的限制使附加唯一的前缀很困难。如果限制使8位或者更少的字符,那么文件名可能会变得相当的隐晦。在某些系统上,除了对可放在一个库档案文件中的.o文件的名称长度有陈旧的约束外,文件名长度并不是一个问题。对应的源文件名称可能需要限制在某一个相对较小的长度内。

名称空间

前缀的主要用途使唯一标识定义组件或者类的物理包

保持前缀的完整性

理想状态是,包前缀不仅表示定义组件或者类的物理库,还将按时包内聚的逻辑特征和组织特征

包层次化

使包层次化的重要性

避免包之间的循环依赖

避免包之间的循环依赖由于一下原因也成为一条主要的设计规则:

  • 开发:当连接整个系统或它的任意一个部分时,必须指定包库调用的顺序以解析未定义的符号。如果单个包内的组件之间的依赖封套是非循环的,那么至少会有一个顺序能保证正在连接过程中解析的所有的符号。
  • 推销:通常一个系统会有一个基本功能以及若干可自由选择的附加功能包。如果系统本身依赖于任何一个附加功能包,那么附加包就不是可自由选择的了,它必须和系统一起销售。
  • 易用性:即便推销不是一个问题,用户也不希望只为了使用基本系统的某个简单的功能而必须连接一个巨大的库
  • 产品:为了支持超大型系统的并行开发,有一个分阶段的发布过程使行之有效的。包的非循环层次结构收集更大型的体系结构单位群中,然后群层次化将这些群划分为层,层在被自下而上以层次话的顺序发布。允许包之间的循环依赖会妨碍形成群和进行分阶段发布的能力
  • 可靠性:易测试性设计要求有一种方法可增量式、分层次地测试大型系统。避免系统宏观部分之间的循环依赖只是这种范例的一种自然结果

包层次化技术

给被一个多包子系统的客户直接使用的组件子集指定一个单一包前缀并不是一定可能的

划分一个系统

在把一个新组件加入到包中时,这个组件的逻辑特性和物理特性都应该考虑

多地点开发

将一个系统划分成包的科层化的集合,对于大型项目是成功的关键。除了由于远距离友元关系导致的耦合之外,使我们能够减少CCD的同样的推理也可以用来减少包之间的的依赖的开销。

包绝缘

包呈现了一种比组件更高层次的抽象。

最小化输出头文件的数量和大小,可以提高可用性

对于定义在一个给定包的一个特定组件,若一下的问题的答案都是是,则意味着该组件的头必须输出

  1. 为了使用这个包整体提供的功能的任何一个部分,这个包的客户都必须访问这个组件吗?
  2. 这个包的任何其他输出的组件都不能使其客户与这个组件定义绝缘吗?
  3. 其他包需要访问这个组件吗?

包群

包群就是一个包的集合,它被组织成一个物理上内聚的单位

如果包群g中的一个或者多个包依赖另一个包群h的一个或者多个包,则称包群g依赖包群h

在一个包群内将协议降级和包装器升级,有助于避免输出包和非输出包之间的循环依赖

包群和组件包是类似的。群应该由逻辑上内聚或者其他有理由组成单个内聚物理单位的包构成。和包一样,一个群定义的目的应该决定它的内容;不切题的东西不应为群的一部分。当然,包群之间的依赖应该形成一幅有向非循环图。虽然一个包的大小适合于一个开发者拥有和实现,一个群却有可能被一个项目经理拥有和由一个开发组实现。从客户的角度来看,群看起来就像是一个单个的、有着紧密相关前缀集合的巨型包;但是,在所有情况下,每个群中的每个独立的包的完整性和唯一性都应该保持。在开发过程中,可能需要访问单个的特殊用途的包级库

发布过程

一个层对应于在系统的一个给定的层次上的所有包群

版本(release)结构

最小化修改之后的源代码的重新编译时间,可以显著的减少开发开销

补丁

补丁是对前一次发布软件的局部修改,以修补组件内的不完善或者效率很低的功能

补丁一定不能影响任何已经存在的对象的内部布局

理想状态下,一个补丁根本不会要求修改任何头文件。修改一个输出头文件中的信息会潜在地影响无限数量的客户;所以这样的修改最好避免。虽然危险,我们还是可以进行许多不会使我们的版本失效的修改,即使它可能意味着改变已有输出的头文件。如果我们能保证这些局部修改的影响四连接兼容的,并且不会使版本失效,我们就可以节省发布第二个版本的可观开支和工作量。

下列几种修改是相对安全的:

  • 改变一个非内联函数的函数体
  • 改变源文件中任何有内部链接的结构
  • 给版本增加一个新的输出头文件
  • 给一个类增加一个友元声明
  • 放宽一个已有的访问限定符
  • 给一个类增加一个新的非虚函数(危险)
  • 给一个头增加一个类或者自由运算符(危险)

注意,最后四种情况需要修改头文件。在进行了这样的一个修改之后,这个头文件应该被人工回溯,以防止客户进行不必要的重编译。最后这两个修改是危险的,因为有可能从已被某个客户包含的头文件的函数或者运算符重载中引入二义性。如果最后这个修改是对一个新的单独的头文件进行的,那么这个结构就不会影响任何已经存在的用法。

下列修改可能会破坏一个版本:

  • 增加、重排序、修改或者删除任何数据成员
  • 增加、重排序或者删除任何虚函数
  • 改变任何函数的基调或者返回值
  • 增加、重排序、修改或者删除任何继承关系
  • 改变头中的任何有着内部连接的结构
  • 缩小一个类成员的访问权限
  • 在临近的数据成员之间引入访问限定符

这里介绍是不完整的,但应该给出了可以通过补丁来局部完成的修改种类的概念和风格,真正的要求只有:

  • 确保修补后的连接时兼容性
  • 避免因为在头文件时间戳上的修改而引起的客户重新编译
  • 确信如果我们想重建系统的话一定会成功

main程序

从一个定义了main的编译单元中分解出独立可测试的和潜在可重用的功能,本质上能够使程序的整个实现在一个更大型的程序中重用

通常,避免赋予一个组件一种可能被其他组件拥有的特权,否则将对作为一个整体的系统产生不利影响

只有定义了main的源文件才有权重新定义全局的new和delete

设计一个大型系统时没有顶。main的用途只是提供一个具有命令行接口、解释环境变量和管理全局资源的C++子系统–别无其它。将main提供的功能分解成单独的组件有助于分层次测试,并且可以更容易地集成到更大型的系统。定义main的源文件拥有全局名称空间,并且不需要遵守某些适用于普通组件的设计规则。对于没有main定义的组件,应该小心不要接受这样的特权;如果该特权也被其他组件接受了,可能会危及整个系统的安全。

启动

程序第一次调用和控制线程进入main之间所经历的时间叫启动。正是在这段时间,每个编译单元中的所有非局部静态对象被潜在的构造。

启动时间也成为启用时间,就是程序初次被调用和控制线程进入main之间的时间

宁愿要模块而不要对象的非局部静态实例,尤其是在一下情况下:

  • 需要在一个编译单元之外对结构进行直接访问

  • 该结构在启动期间不需要或者启动之后不是马上需要,并且初始化结构本身的时间是显著的。

程序中的每一个非局部静态对象结构都会潜在的增加启动时间

初始化策略

至少有四种不同的技术可以用来确保一个模块在使用之前初始化:

  • 唤醒初始化
  • 显式的init函数
  • 灵巧计数器
  • 每次检查

这些初始化策略每种都有自己的优缺点;最好的选择取决于如下若干因素:

  • 初始化该模块需要的时间
  • 该模块真正投入使用的可能性
  • 每个模块函数调用所做的工作量
  • 对模块函数进行调用的频率
  • 直接使用该模块的组件的数量
  • 在程序退出之前是否有释放/再分配资源的需求
唤醒初始化技术

到目前为止,初始化一个模块的最好方式就是设法让模块在一种初始化的状态中唤醒

显式init函数技术

不是所有的模块都可以唤醒初始化。更普遍的是,一些组件可能定义模块或者包含静态结构,这些模块或者惊天结构必须在运行时在被使用之前初始化。使这种初始化能够完成的一种方法是给每个这样的组件提供一个init函数,必须在这个init函数被调用之后才能使用该组件的静态结构。

初始化你不另外直接依赖的组件会显著的增加CCD

灵巧计数器技术

当静态对象使用其他静态对象时候,初始化会变得更加复杂。

为了取代容易出错的init函数方法,我们使用灵巧计数器方法。这种方法中,一个初始化类的伪静态实例被放在一个组件的头文件作用域中。这个静态实例的部分用途是计算包含在这个组件内的其他组件的数量。被一个编译单元包含的这个伪对象的每一个静态实例都将在启动时候被构造。该伪对象的一个静态实例被构造时,静态计数从0增大到1,并且该伪对象知道要去初始化它的组件。随后,每一次构造伪对象,唯一的影响就是这个静态计数增加。

程序退出时候正好相反

每次检查技术

每次检查我们不需要显式初始化组件。相反,我们务必确保依赖于内部静态后遭的每个函数首先检查是否该组件被初始化,如果没有则立刻初始化该组件。

清理

提供一种机制来释放分配给一个组件内静态结构的任何动态内存。

回顾

在启动时初始化模块和非局部静态对象,可能使启用一个大型程序的时间长的无法接受。虽然我们不能影响这些静态实例在程序中被初始化的时间点,但是总是有可能将一个对象的单一全局实例转换成一个模块。一种确保没有运行时开销的初始化的有效方法是,通过使其只有基本的静态数据成员,将模块或组件设计成唤醒初始化。减少启用时间的另一种方法是推迟初始化,知道真正需要时再初始化。这种被推迟的初始化可以用独立的init函数或者将初始化检查嵌入每一个访问来实现。一个init函数方法是灵活的,也是最容易引起错误的,但是当独立访问函数是轻量级的并且被频繁调用时,它可能是必要的。当试图连接存储在一个unix类型的库中的自我初始化组件时,显式的初始化也是有必要的。每次检查方法对客户来说很简单,尤其适合于当每个函数调用所做的工作量已经比较大的时候。最后,如果我们知道我们很可能需要一个再启用之后立即初始化的组件,他的函数是轻量级的且被频繁调用,那么灵巧计数器的方法也许是最好的选择。在所有情况下,提供一个机制来释放由静态结构拥有的任何动态内存将有助于对内存泄漏进行回归测试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

turbolove

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值