物理设计概念 -- 绝缘

绝缘

好的物理设计的另一个重要方面是避免不必要的编译时依赖。过渡编译时耦合会严重地削弱我们维护一个系统的能力。一般来说,对驻留在一个组件的物理接口中的、通过编程不能访问的实现细节进行修改将强迫所有客户程序重新编译。甚至对于一定规模的大型项目,重新编译整个系统的开销也将抑制对低层次组件的物理接口的修改,甚至限制我们对它们的是爱你封装细节做局部的修改。

绝缘过程通常和封装的过程类似,绝缘是指避免或者消除不必要的编译时耦合的过程

对暴露的实现细节进行绝缘的技术如下:

  • 私有基类
  • 嵌入数据成员
  • 私有成员函数
  • 保护成员函数
  • 枚举类型
  • 编译器产生的函数
  • 包含指令
  • 私有数据成员
  • 默认参数

进行绝缘的大规模技术:

  • 协议类
  • 完全绝缘的具体类
  • 绝缘包装器组件器

从封装到绝缘

绝缘是一个物理设计问题,它的逻辑相似物一般称为封装。之前我们基于类和组件以及层次结构讨论了封装:

  • 某个细节是某个实体的一部分
  • 从该实体的接口不能编程访问到该细节

一个被包含的实现细节如果被修改、添加或者删除时不会迫使客户程序重新编译,则称这样的实现细节被绝缘了

可以形象的将术语封装比喻成围绕一个类的实现的极薄的透明膜,用于防止只通过编程来访问类的实现。而术语绝缘却意味着是一个无限厚的不透明的障碍,它排除了与组件的实现进程直接交互作用的任何可能性。

绝缘的价值的最后一个实证是它允许我们透明的替换动态装载的库。动态装载的库不是连接到一个单个的可执行代码中,而是事实地链入到一个运行的程序中。如果提供一个完全绝缘的实现库,则可以在增强性能或者是修复故障的时候完全不需要打扰客户。给客户发送一个更新版本不会迫使用户程序重新编译或者连接。客户所需要做的就是重新配置环境以指向新的动态加载的库即可。

C++结构和编译时耦合

有时候组件的逻辑和物理分解彼此是自然一致的。考虑到一个类的非内联成员函数,其逻辑接口驻留在物理接口中,而逻辑实现驻留在物理实现中。在这种情况下,声明只是描述接口而没有暴露比需要或希望的更多的信息。

C++并不要求所有有关逻辑实现的细节都存在于源文件中。由于性能方面的原因,C++允许这种紧密的编译时耦合。对于一个小的轻量级的组件,通过对其实现完全绝缘来避免编译时耦合在实践中对性能影响很大。这种轻量级的组件一般很快可以达到一个稳定状态,并且之后很少改动。

就是提供高层次功能的组件而言,每个接口函数调用的有效工作的总量非常大。在这种情况下,对实现进行绝缘的运行时开销通常既不能测量也不是成比例的。

在C++中很容易不经意地把实现细节引入一个组件的物理接口中。无论何时将一个组件的实现的一部分放到它的头文件中,我们都无法再将客户程序与这部分实现绝缘。通过使用下列结构,逻辑实现可以成为物理接口的一部分:

  • 继承
  • 分层
  • 内联函数
  • 私有成员
  • 保护成员
  • 编译器生成函数
  • 包含指令
  • 默认参数
  • 枚举类型

在下面我们将分别讨论与编译时耦合有关的上述结构中的每个结构的隐含意义。

继承和编译时耦合

一个类无论 何时派生自另一个类,即使是私有派生,也可能没有办法把客户程序与这个事实绝缘。即使私有继承被认为是派生类封装了实现细节,派生对象的物理设计也会迫使包含派生类的定义的每个客户程序都要见到基类的定义,因此对一个派生类而言,把含有基类的头文件显示的包含在类的头文件中是合适的。无论何时修改基类的头文件,将该基类的派生类的客户程序连接位任何新的可执行程序之前unix工具都会重新编译所有的客户程序。

分层和编译时耦合

当一个类在其定义中嵌入了另一个用户自定义的类型的一个实例时,这个类的物理布局就会变的紧密依赖于该类型的布局。其结果是,如果没有见到嵌入到某个对象的每个分层子对象的定义,则一个客户程序不可能包含该对象的类定义。因此,若某些头文件包含了物理上嵌入到那个类的每个分层对象的定义,则一个复合对象显式的包含这些头文件是合适的。

于此相对的是,当一个类拥有一个对象的地址时,这个类不必依赖这个被拥有对象的物理布局。如果是这样的话,包含这个类的头文件不去包含被拥有对象的头文件而只是声明其类型是合适的。

内联函数和编译时耦合

如果某函数要在当前组件之外替换为内联的,俺么声明为inline的这个函数必须定义在头文件中。该需求迫使将内联函数体放在组件的物理接口上。由于除了通过其自身的逻辑接口来调用内联函数之外不能通过编程来访问它,所以说一个内联函数是封装的。但是,对象的此逻辑实现部分没有和客户程序绝缘,因此有以下几个后果:

  • 能使用该组件的任何程序员都能看见内联实现
  • 改变一个内联函数的实现会迫使定义内联函数的组件的所有客户程序重新编译
  • 将一个函数改为内联函数或者从内联函数改回来,也会迫使定义该函数的组件的所有客户程序重新编译
  • 从一个内联函数通过值返回的一个对象被实质的使用在头文件中,因而绝不会与客户程序绝缘。对一个在内联函数的函数体中被实质使用的对象也是这样。所以,当一个用户自定义对象传入,被使用在一个内联函数中或者通过值从一个内联函数返回时,显式地将定义了被使用对象的文件头包含在定义了该内联函数的头文件中是合适的

私有成员和编译时耦合

一个类的每个私有数据成员–尽管封装好了–也没有约该类的客户程序绝缘。私有成员函数是一个类的封装细节,但是他们并不是已绝缘的实现细节–甚至他们没有声明为inline的时候。仅改变一个类的私有成员函数的基调也足以迫使定义该类的组件的所有客户重新编译程序

保护成员和编译时耦合

当考虑保护成员时,基类的作者现在必须面对两类不同的观众:派生类的作者和一般用户。保护函数在特别为派生类而编写的接口中,但是准备让一般用户作为实现细节来对待。注意,保护成员数据很少是合适的,尤其是在把绝缘作为一个设计目标的广泛使用的接口中。

表面上保护接口为预期的派生类作者提供了一个方便的地方,可以查看并决定什么事需要的。但是就像私有成员一样,保护接口是在类定义中声明的,因而就一般用户而言,它不是一个绝缘的实现细节。以任何方式修改的一个基类的保护接口,将迫使一下单个方面的重新编译:

  • 基类的全部客户程序
  • 所有的派生类
  • 派生类的所有客户程序

编译器产生的成员函数和编译时耦合

一个基本的成员函数是由编译器自动产生的,除非这些函数在一个类中被显式的声明。特别的,除非指定了一个拷贝构造函数,否则编译器将产生一个具有像函数成员那样的拷贝语义的拷贝构造函数。即,生成的构造函数将会按照各自的初始语义来拷贝每个成员对象和基类对象

如果需要,一个隐含的赋值运算符也将以类似的方式产生,并根据它自己的赋值语义拷贝每个成员对象和基类部分。一个析构函数也将在需要的时候产生,以调用各层的和基类对象的析构函数。

在许多情况下,编译器产生的构造函数、赋值运算符和析构函数所做的正是所需要的工作。但是,如果一个类的作者确定的需求与编译器产生的定义不同,那么有必要将合适的成员声明引入到类的定义中。这样的声明引入不能认为是绝缘的,并且来的任何客户程序都将被迫重新编译。

包含命令和编译时耦合

包含命令会重新编译所有递归包含的类(在这之前冗余卫哨会减少编译时候的开销)。

默认参数和编译时耦合

一个单个的算法经常依赖于数个参数–一些参数带有合理的默认值。将这些默认值放到了定义函数接口的头文件中可以简化自我建档,这只是因为他们将更多的信息放进了头文。

但是这样的默认值会随着接口一起编译,而对这些值的任何修改将强迫客户程序重新编译

枚举类型和编译时耦合

枚举类型、cpp宏、typedef和非成员const数据都没有被外部连接。同样的,如果他们被其他组件使用,那么这些结构必须出现在组件的头文件中。

如果把这些组件防止到共同的头文件中。无论何时需要一个新的定义被加载到该组件中,系统中的几乎所有组件都会被重新编译。

最终全局定义代价昂贵。因此不在该共有文件中放置定义,而是将它们保持为局部的或者私有的;不在把特定的新的返回状态添加到枚举类型中,而是反复使用以前已经存在的代码。即使他们是模糊的甚至是不适当的。

部分绝缘技术

不是每个组件都应该视图将其客户程序与每个实现细节绝缘。但是,在其他条件都相同的情况下,将客户程序与实现细节绝缘比不这么做要好–即使只是为了减少物理接口中的混乱。

幸好绝缘不必是一个要么全有要么全无得命题。绝缘一个实现细节可能是所希望的–甚至在别的实现细节仍然是不绝缘的时候。一个组件的实现越绝缘,实现的改变迫使组件的客户程序重新编译的可能性就越小。

有时候对一个实现细节进行绝缘处理与不对他进行绝缘处理一样容易。另一些时候,绝缘可能需要大量的和深思熟虑的工作。值得花费在绝缘某个给定的实现细节上的工作量,取决于该细节的变化可能影响客户的程度。

消除私有继承

与公有继承不同,私有继承是一个实现细节。与分层相比,私有继承的优点之一是,可以更方便地表示,通过访问声明或使用声明有选择地向派生类的客户程序暴露一些私有基类中的函数。

使用私有继承而不是分层的一个原因可能是向利用基类的虚拟表。通过覆盖声明在私有基类中的虚拟函数的行为,我们也许能够对那些在基类层次上依赖于被覆盖行为的其他行为进行定制或者编程。另外,也有可能为了派生目的而创建一个哑元类,然后使用该哑元类继续进行分层。如果绝缘不是问题,那么私有继承可能是合适的。但是如果该类将成为一个更一般的公共接口的一部分,那么从继承转换到分层是恰当的。

消除嵌入数据成员

即使性能需求阻止我们完全绝缘一个类,我们仍然可以通过将这个实现类的所有嵌入实例转换为指向那个类的指针,然后在类的构造函数、析构函数和赋值运算符中显式地管理这些指针,使客户程序与单个实现类绝缘。

消除私有数据成员

私有成员函数尽管封装了一个类的逻辑实现细节,但是仍然是一个组件的物理接口的一部分。非内联私有成员函数有外部连接;这使声明为该类的友元并且在其他编译单元中定义的函数和类能够调用他们。但是正如之前所说的那样,视任何在一个组件之外定义的函数或类为友元,将使得不受约束的客户利用我们的类的私有细节。避免远距离的友元关系意味着只有定义在一个组件爱你馁的函数和类才能访问私有成员。幸好C++支持一个更严格的在组件范围的访问控制形式。这种访问控制形式不再让函数成为类的私有成员,而是使它成为一个在一个组件的源文件作用域内声明的静态自由函数。

有时候函数成为私有成员不是因为他们需要私有访问,而是因为头文件的私有部分是一个存储在这些被分解的助手函数的好地方。即,一些私有的助手函数只使用类的公共接口就可以完成他们的工作。在这些情况下,从私有成员到静态自由函数的变换是很容易得,并且分两步:

  • 第一步是添加合适的可写指针或者只读引用参数到函数中,将每个私有成员函数转换为第一个私有静态成员
  • 第二步是从头文件中整个删除这些函数声明,在源文件的函数定义中删除这些成员的标识法,并且最后在这些定义中的每一项前面加上static

有时候私成员函数可以转变为静态自由函数,这些自由函数不依赖于定义在当前组件中的类型。不要创建一个单个的带有不可访问的静态自由函数的组件,而是考虑构造两个组件–一个组件带有公共静态成员,他们可以用于实现另一个组件。

尽管静态函数在编译时耦合方面比私有函数更优越,但是性能可能会成为一个问题,尤其是当在文件作用域内有许多必须传入或者传出静态函数的私有状态信息时。在这些情况下,其他的更一般形式的绝缘更可取。

消除保护成员

以一个基类的保护成员函数的形式为派生类提供支持,会将派生类的未绝缘的实现细节暴露给基类的公共客户

消除一个类的所有保护成员是不可行的。例如当一个派生类需要访问由一个基类提供的保护性服务以便能够覆盖虚函数。

定义某些共享功能的抽象基类有时被称为部分实现。这种分解的实现类型允许派生类作者共享一个公共的实现,但是保护的功能再次给基类的一般用户添加了负担,因为它把未绝缘的实现细节暴露给一般用户。

消除私有成员数据

消除私有静态成员数据相对容易

消除编译时产生的函数

改变任何编译器产生的函数的定义意味着修改类的定义来添加相应的声明,任何这样的修改都将迫使该类的所有客户程序重新编译。而允许编译器产生一个拷贝构造函数、赋值运算符以及一个析构函数可能会很方便,一个真正绝缘的类必须显式地定义这些成员。通常这些显示定义的函数将复制他们的默认行为。特比的,析构函数经常被定义为一个空的实现。这是灵活性的代价。

消除include指令

不必要的include指令可能会在本不存在的编译时耦合的地方引起编译耦合,一个include指令出现在一个组件的头文件中,通常有三种情况:

  • IsA:
  • HasA:
  • inline

转换是简单的:将头文件中不必要的include移动到源文件中,并且使用适当的前置声明来替换它们。

消除默认参数

很容易从一个接口消除默认参数并且用等价的几个函数替换他们:

class Circle
{
public:
    Circle(double x = 0, double y = 0, double r = 1);
}

class Circle
{
public:
    Circle(double x);
    Circle(double x, double y);
    Circle(double x, double y, double r);
}

消除枚举类型

接口中的枚举类型很自然也会引起编译时耦合。枚举类型、typedef以及其他在接口上有内部连接的接口的合理使用对于获得良好的绝缘是非常重要的。

给较高层次的客户程序授予修改较低层次共享资源的接口的权利会隐含地耦合所有的客户程序

显示的三种枚举类型如下:

class WhatEver
{
    enum {AAA = 100};
public:
    enum {BBB = 200};
    enum Status { A, B, C, D, E, F };
}

第一个是私有实现细节,第二个是一个公共可访问的常量,第三个是一种命名的返回状态值的枚举列表。

第一种枚举类型放的不合适。这种枚举类型要么移动到源文件中或者变成类的私有的const成员。

第二个类型至少应该变成一个私有的静态const成员,并且将一个公共的静态访问成员函数定义为返回该值。

第三个是接口的一部分。也许不是所有这些状态都通过这个组件中的函数返回,而是选择该组件来为其他组件保存状态值。但是为了减少编译时依赖,一个更好的方法是讲这些状态值分散到合适的组件中并且不要试图去修改他们。

一般来说绝缘的目标是保护客户程序不受某种编译时依赖的影响,这种编译时依赖是因为知道了不必要的、封装的实现细节而引起的;绝缘并不意味着不能通过编程访问接口来访问客户程序,也不是为了保证类型安全。

整体的绝缘技术

在一个认真规划、精心构造的系统中,我们可以预先知道那些接口是公共的和那些接口不是公共的。这种知识可以帮助我们决定那些接口应该绝缘和哪些接口不应该绝缘。从一开始就把一个接口设计为绝缘的,总是要比过后再去绝缘它更容易i,代价也比较低

协议类

在理想的情况下,一个绝缘良好的接口绝对不会定义实现细节。它只是定义一个接口,通过这个接口,客户程序可以访问和操纵具体的派生类的实例。

满足下列条件的抽象类是一个协议类:

  • 它既不包含也不继承那些包含成员数据、非虚拟函数或任何种类的私有(或者保护)成员的类
  • 它有一个非内联的虚析构函数
  • 所有成员函数(除了包含被继承函数的析构函数)都声明为纯虚函数,并任其处于未定义的状态。

协议类是一个抽象类,它没有用用户专用的构造函数,没有数据只有公共成员。除了该协议所继承的其他协议的那些头文件之外,组件本身并不包含任何其他的头文件。所有的成员函数都声明为纯虚函数。

一个协议类几乎是一个完美的绝缘器

一个协议类可以用来消除编译时依赖和链接时依赖

完全绝缘的具体类

一个具体类不仅仅是一个接口–它定义了一个有用的对象,该对象可以实例化程序堆栈上的一个自动变量。协议类与纯面向对象设计是一致的;但是,工程绝不是纯的。有时候我们会为一个协议类构造对象实例,这样做有时有好处。

只保留一个不透明指针,会使一个具体的类能够将其客户程序与其实现绝缘。

一个具体的类如果满足以下条件,就是完全绝缘的:

  • 只包含一个数据成员,它表面上是不透明指针,指向一个定义具体类的实现的non-const struct

  • 不包含任何其他种类的私有的或者保护的成员

  • 不继承任何其他的类

  • 不声明任何虚拟的或者内联的函数

所有完全绝缘的类的物理结构在外表上都是一样的

一个完全绝缘的类的重要特性是,改变器表示法不会影响客户程序认识一个实例的物理布局,因为它的实现永远只是一个单个的不透明指针。若忽略目的或功能,一个完全绝缘的类的实例看起来与其他完全绝缘的类的每个实例都是一样的。正是这种物理上的一致性使我们不必以任何方式改变头文件就可以任意地重新实现类的接口

允许继承或者虚函数存在将影响对象的布局,因为会引入附加数据或附加的虚函数表指针。注意:即使继承一个空的struct也会影响派生对象的大小。因此,从一个基类中继承的并且在其他方面完全绝缘的类的实例,物理上一定不同于一个没有继承基类的完全绝缘的类的实例。换句话说,从一个基类继承会增加一个完全绝缘的类的大小,其大小会超过单个指针的大小,这在物理上可以将其实例和其他完全绝缘的类的实例区分开来。

所有完全绝缘的实现都可以在不影响任何头文件的情况下进行修改

完全绝缘的另一个重要特性是,类可以完全控制和访问定义内部表示的struct。让内部数据成员直接指向定义在一个独立的组件中的类的实例,可能会损害我们对自己实现进行独立的、绝缘的修改的能力。为了在不影响客户程序的情况下添加一个私有成员,我们将被迫改变一个可独立访问和独立测试的对象的接口。

绝缘的包装器

单个组件包装器
多个组件包装器

没有什么办法可以从一个组件的外部通过编程来确定一个组件是否为一个包装器

一个定义在A包装器组件中的类型,无论什么时候被传递到一个定义在B包装器组件中的类型中,B组件都不能访问基础的被包装的实现对象,只能访问包装器的公共功能。

过程接口

过程接口的体系结构

我们提供的接口一般比开发者开始用于创建实现的接口要抽象的多。通过只从过程接口测试来确保系统可靠性是非常困难的。幸好,复杂性取决于系统较低层次。我们作为过程接口的作者的工作是,确定在低层次上的实现组件中已经定义的类型和功能的一个合适的自己,这些实现组件最终用户完成他们所要的应用层任务。

建立和消除不透明指针

Handles句柄

本次我们将handles当作一个类,它维持一个指向一个对象的指针,该对象可以通过handle类的公共接口编程访问

访问和操作不透明对象

在一个过程接口中,使看客户程序只显式地创建对象,可以减少所有权 关系上的混乱,并且能提高性能

作为一个为大系统编写过程接口的作者,你可能会发现一个直接返回一个动态分配对象并且没有将它放到一个客户提供的handle中的接口。在这样的情况下,我们有必要寻找一个合适类型的handle,并且要求我们的客户程序传递一个指向那个handle的非const指针到我们的接口函数中。然后我们必须自己装入一个带有系统分配对象的handle。这样做可以保持过程接口原则,即客户成员只被授权删除他们显式分配的对象。

继承和不透明对象

因为继承二相关的类型转换也是为必须进行的面向对象设计而编写过程接口的另一个方面。目前的问题涉及我们如何提出一个类型安全的、支持指针转换概念的过程接口,这种转换是继承所隐含的

当一个给定类型的公共继承另一个类型时,C++语言支持从第一种类型的指针转换到第二种类型的指针的隐式转换;有必要在过程接口中使这种转换显式的进行。

过程接口完全不同于其他的绝缘技术,它满足一组完全不同的目标。像析取一个协议类和创建一个包装器这样的技术,既服务于封装也有助于客户程序于子系统的底层组织的绝缘。这些其他技术使我们能够并排重用底层实现组件,而完全不可能破坏内部使用这些组件的子系统的封装。

绝缘或不绝缘

绝缘的开销

绝缘一个类很显然会影响其运行时的性能。影响的程度取决于类本身、使用绝缘的方式以及使用绝缘的技术。

尽管计算机体系结构和编译器各不相同,但是下列的规则可以帮助系统结构设计师决定,在设计的早期,是否绝缘和该如何绝缘。

访问单独访问的相对开销
借助内联函数通过值1
借助内联函数通过指针2
借助非内联、非虚函数10
借助虚拟函数机制20
创建单独分配的相对开销
自动化1.5
动态100+

什么时候不进行绝缘

对于一个组件的实现不进行绝缘的决策,可能使基于该组件不是很广泛使用的认识

除非使已经知道性能不是一个问题,否则避免用极小的访问函数对低层次类的实现进行绝缘可能是明智的

轻量级使一个术语,其含义依赖于它被使用的上下文

  • 不依赖于其他组件
  • 创建和析构都不昂贵
  • 不分配额外的动态内存
  • 能有效地利用内联函数来访问/操纵嵌入数据

绝缘轻量级的、被广泛使用的并且通常通过值返回的对象,会显著地降低整体的运行性能

对于大型的、广泛使用的对象来说,要尽早进行绝缘。如果有必要,以后可以选择性地删除该绝缘

绝缘一个接口可能导致动态分配内存、额外的间接调用、非内联或虚函数调用、动态分配对象的显式管理以及附加的编译单元。对一个接口进行绝缘,在性能、开发工作量或复杂性方面的开销有时会超过绝缘本身的好处。另一方面,一些组件既不是轻量级也不是稳定的,并且在整个系统中经常使用。这些大的高层次的、不稳定的且被广泛使用的对象比较适合绝缘。

如何进行绝缘

主要又两种方法:

  • 为要绝缘的类提取一个协议
  • 其他方法:
    • 使用部分实现技术
    • 将该类转换为一个完全绝缘的具体类
    • 为该类创建一个绝缘的包装器
    • 为该类创建一个过程接口

绝缘的程度

有时候整体绝缘的运行时开销不会比部分绝缘大

有时候获得最后百分之10的绝缘要增加10倍的运行时间为代价

  • 如果一个对象的函数已经在绝缘层之下做了大量的工作,那么对该对象的实现进行绝缘在运行时性能上不会又明显的影响
  • 如果一个子系统的函数已经在封装层之下做的工作使微不足道的,那么对该仔细提供 用包装器进行封装,对运行时性能的印象概念股使可以忽略不计的
  • 为了频繁调用的轻量级函数提供一个封装包装器,会显著的影响整体性能
  • 为频繁调用的轻量级函数提供一个绝缘包装器,会对整体性能产生压倒性影响
  • 如果极小对象经常被通过值返回,那么为该极小对象提供整体绝缘包装器会对整体性能产生灾难性影响

小结

我们介绍了绝缘的概念,它是通常称为封装的逻辑概念在物理上的相似概念。如果一个组件的实现细节被修改之后,不会迫使该组件的客户程序重新编译,那么该组件的实现细节就是被绝缘了。

下面几种结构被认为可能会潜在地导致不希望的编译时耦合:

  • 继承和分层迫使客户程序看到继承或者嵌入对象的定义
  • 内联函数和私有成员把对象的实现细节暴露给了客户程序
  • 保护成员把保护的细节暴露给了公共的客户程序
  • 编译器产生的函数迫使实现的变化影响声明的接口
  • 包含指令认为地制造了编译时耦合
  • 默认参数把默认值暴露给了客户程序
  • 枚举类型会引起不必要的编译时耦合,因为不合适的放置或者不适当的重用

在其他条件都一致的情况下,将一个特定的实现细节与一个客户进行绝缘比不进行绝缘要好–即使其他细节仍然不是绝缘的。部分实现技术可以用来降低编译时耦合的程度,并且不会招致整体绝缘可能隐含的开销:

  • 通过将WasA关系转换为HoldsA关系来消除私有继承
  • 通过将WasA关系转换为HoldsA关系来消除嵌入的数据成员
  • 通过使文件作用域中的私有成员函数成为静态的,并将他们转换到源文件中,来消除私有成员函数
  • 通过建立一个独立的工具组件、或者提取一个协议来消除保护成员函数
  • 通过提取一个协议或者移动静态数据到源文件中来消除文件作用域中的私有数据成员
  • 通过显式地定义编译器产生的函数来消除这些函数
  • 通过删除不必要的包含指令或提前用类声明替换它们来消除包含指令
  • 通过用无效的默认值替换有效的默认值或者使用多函数声明来消除默认参数
  • 通过将枚举类型重新定义到源文件中,从const静态类成员数据替换他们,以及在使用枚举类型的类中重新分布这些枚举类型来消除枚举类型

对于广泛使用的接口来说,避免对基础实现细节的所有编译时依赖使非常值得做的工作。我们讨论了三种用于把客户程序与所有实现细节绝缘的一半绝缘方法:

  • 协议类:建立一个抽象的协议类使一种通用的绝缘技术,可用以分离一个抽象基类的实现和接口。不仅客户程序可以与编译时实现细节的变化绝缘,而且可以消除对一个特定实现的连接时依赖
  • 对具体类进行完全绝缘:一个完全绝缘的具体类拥有一个指向一个全部定义在源文件中的私有结构的不透明指针。该struct包含了以前在原始类的私有部分中的所有实现细节
  • 绝缘包装器组件:包装器一般用于对几个其他的组件甚至整个子系统进行绝缘。与过程接口不一样,一个包装器层要求预先进行相当多的计划工作和非常严密的设计工作。特别的,要小心进行多组包装器的设计以避免对远距离友元关系的需求。

一个过程接口使函数的集合,这些函数都是现有函数的成员,并且将功能集的一个子集暴露给最终用户。过程接口使整体绝缘的另一个方案。过程接口既不是逻辑封装,也不是整体绝缘。一个过程接口考虑了对非常大型系统起了独特的作用

一般来说,如果一个组件在系统中被广泛的使用,那么其接口应该绝缘。但是不是所有的接口都应该绝缘。例如,绝缘可能不是切实可行的,特别是对应于轻量级的、可重用的组件来说。选择不对一个组件进行绝缘的常见原因如下:

  • 暴露:客户程序的梳理可能已知使很少的
  • 访问数据时间:该类可能已经嵌入了数据并且有效地利用了极小的内联函数访问它
  • 创建对象时间:一个极小的类可能还没有分配动态内存
  • 开发的开销:可能没有绝缘的强迫性理由;额外的开发工作可能使不合算的
  • 组件的数量:绝缘可能要求需要另一个组件,从而增加了维护的开销
  • 组件的复杂性:一个绝缘的实现可能比一个没有绝缘的实现更难以理解和维护
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

turbolove

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

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

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

打赏作者

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

抵扣说明:

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

余额充值