C++ 大规模程序设计 之 绝缘

来源:互联网 发布:wkwebview 调用js 编辑:程序博客网 时间:2024/06/09 18:41

概述

绝缘与封装的过程类似,目的是要消除不必要的编译时的耦合的过程。

绝缘 是个物理过程,它的逻辑相似物便是 封装

涉及到的技术:

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

何为绝缘

从一个简单的例子看绝缘。
两个Stack类的接口部分完全一致,也都使用private对成员变量进行了封装,符合面向对象类的设计思想。不同之处在于右侧的类中使用了StackLink的指针对成员变量进行了再次的封装。

这里左侧的Stack可以称为封装,但非绝缘
这里右侧的Stack可以称为封装,并绝缘

因为当左侧类中对成员变量进行修改时(例如将 int* 修改为double*),依赖于它的其他组件也必须被重新编译,但右侧的类则不会发生这样的情况。在它看来StackLink 只是一个指针,针对StackLink内容的修改不会影响 Stack内存布局的变化,所以并不需要重新编译。

这里写图片描述

当然并不是说要把所有的一个类中基本类型都换成指针。

绝缘要解决的本质问题是“防止一些修改对客户程序产生影响” 这里的客户程序是指使用你开发的库的程序。
换句话说绝缘更主要是针对接口部分的设计,避免因一处小的修改而造成程序需要大规模的编译。


编译时的耦合(非绝缘的产生)

一, 继承(IsA)和编译时的耦合

一个类派生自另一个类,即使是私有派生,可能也没法让他与客户程序绝缘。

例如图中,如果Base 发生变化,即使只是添加注释,不可避免的所有的 Client程序也会受到影响,并被重新编译。
这里写图片描述

二, 分层(HasA/HoldsA)和编译时的耦合

HasA: 当一个类在其定义中嵌入了另一个用户自定义的类型。
此时,B是嵌入在类A中。

class A{private:    B  m_B;public:    A();}

这时,在编译类A时,它是需要清楚的知道类B的定义的,也就是说类B的内存结构会对类A产生影响。类A的头文件中,要包含类B的头文件。

HoldsA : 当一个类只拥有另一个类的指针。

class A{private:    C  *m_C;public:    A();}

此时,类A只有类C的地址,A并不需要知道类C的具体构造,也就不依赖于类C的物理布局。 此时 A只需要知道类C的声明即可。

三, 内联和编译时的耦合

声明为 inline 的函数,在头文件中定义并实现该函数。
内联会造成如下的一些问题:

1) 使用该组件的任何程序员都可以看见其实现

2) 改变内联函数的实现会迫使定义内联函数的组件和使用该组件的所有程序被重新编译。

3) 将一个函数改为内联函数或将内联函数改为普通函数都会造成使用该组件的所有程序被重新编译。

四, 私有成员和编译时的耦合

私有成员实现了对程序逻辑上的封装,但并不一定在物理上是绝缘的。 上面Stack的例子可知。

五, 保护成员和编译时的耦合

类中的保护成员其实面对的是两种客户,继承类和普通用户类。如果保护成员是非绝缘的,那么在修改后会造成三方面的影响。

1) 包含该类的组件需要重新编译

2) 改类的子类需要重新编译

3) 使用该类子类的客户程序需要重新编译。

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

类中一些基本的成员函数有时是由系统自动生成的,例如:一些拷贝构造函数,运算符函数等。当我们需要自定义这些系统生成的函数时,会造成相关组件的重新编译。

七, 包含命令(#include)和编译时的耦合

包含不必要的头文件也可能造成非绝缘。

八, 默认参数和编译时的耦合

关于默认参数,参考如下程序

class A{public void func(double x = 0.0, int y = 1)}

其中,x, y 都使用了默认值,当对默认值进行修改时,会造成客户程序的重新编译。

九, 枚举类型和编译时的耦合

枚举类型, typedfe,宏,非成员 const变量,这些项一般都会被定义在头文件中。

在小型工程中,这些组件会被定义在同一个头文件中类似于 sysdef.h 或 global.h 等等。但当工程不断加大时,对于这种头文件的修改或添加新内容会造成整个系统的重新编译,编译的代价也会越来越大。


部分绝缘技术

并不是每个组件都应该绝缘的,绝缘并不是一个要么全有,要么全无的命题。针对某些接口或某些细节的绝缘处理会减小客户程序重新编译的概率。

1) 消除私有继承

关于私有继承: 私有继承的访问权限

这里写图片描述

它的目的是子类即可以使用基类的成员函数功能,同时又保护基类的成员不会通过子类被外部访问。这也是使用私有继承的优点。

程序示例:使用 car -> wheel & engine 有逻辑关系可以更好的表现私有继承的作用。但这里只是示例,其实engien和wheel 是不应该定义在同一个组件中,它们应该有自己分别的头文件和实现。

(这里car 是通过私有继承使用了wheel 和 engine的方法。)

main.h

#include <iostream>#ifndef _MAIN_H_#define _MAIN_H_using namespace std;class engine {public :    void start() {cout << "engine->start" << endl;}    void move() {cout << "engine->move" << endl;}    void stop() {cout << "engine->stop" << endl;}};class wheel {public :    void start() {cout << "wheel->start" << endl;}    void move() {cout << "wheel->move" << endl;}    void stop() {cout << "wheel->stop" << endl;}};class car : private engine, private wheel{public :    void start();    void move();    void stop();};#endif

main.cpp

#include "main.h"void car::start() {    engine::start();    wheel::start();}void car::move() {    engine::move();    wheel::move();}void car::stop() {    engine::stop();    wheel::stop();}int main(int argc, char* argv[]) {    int i = 0;    car ca;    ca.start();    ca.move();    ca.stop();    cin >> i;    return 0;}

但关于绝缘,此时继承类会受到基类的影响,导致客户程序也受到基类方法变化的影响。 如果 engine 或 wheel 接口中内容发生了变化,会导致car 及 car的客户程序也需要重新编译,但这种情况其实是完全不必要的。

此时可以考虑使用聚合分层)来解决绝缘的问题。

main.h

#include <iostream>#ifndef _MAIN_H_#define _MAIN_H_using namespace std;class engine {public :    void start() {cout << "engine->start" << endl;}    void move() {cout << "engine->move" << endl;}    void stop() {cout << "engine->stop" << endl;}};class wheel {public :    void start() {cout << "wheel->start" << endl;}    void move() {cout << "wheel->move" << endl;}    void stop() {cout << "wheel->stop" << endl;}};class car{private:    engine* m_pEngine;    wheel*  m_pWheel;public :    car();    void start();    void move();    void stop();};#endif

main.cpp

#include "main.h"car::car(){    m_pEngine = new engine();    m_pWheel = new wheel();}void car::start() {    m_pEngine->start();    m_pWheel->start();}void car::move() {    m_pEngine->move();    m_pWheel->move();}void car::stop() {    m_pEngine->stop();    m_pWheel->stop();}int main(int argc, char* argv[]) {    int i = 0;    car ca;    ca.start();    ca.move();    ca.stop();    cin >> i;    return 0;}

它的本质还是对car在类的组织上进行了修改,使car在内存布局上不会依赖于 engine和wheel 达到绝缘的效果。

2) 消除嵌入数据成员

关于嵌入成员,从两个简单的例子对比开始

这里写图片描述

两个类的区别是一个使用了类A的指针,另一个则直接使用了类A,如果右侧类直接使用了类A,这时类B是需要知道A的内存布局的,如果对A进行修改,会导致B和B的客户类的重新编译。

使用类A的指针是一种绝缘的方式。

3) 消除私有成员函数

私有成员函数,逻辑上封装了一个类的部分实现的细节。但从物理上看如果其内容发生了变化,它也会导致客户程序被重新编译。

有时使用私有函数并不是因为该函数本身需要被私有访问,而是因为private的部分是存放这些助手函数的好地方。

此时,可以考虑将这些私有函数转换为静态自由函数。

含有私有成员函数的程序

main.h

#include <iostream>#ifndef _MAIN_H_#define _MAIN_H_using namespace std;class car{private:    void prepare();public :    car();    void start();    void move();    void stop();};#endif

main.cpp

#include "main.h"car::car(){}void car::prepare(){    cout << "car->prepare()" << endl;}void car::start() {    prepare();    cout << "car->start()" << endl;}void car::move() {    cout << "car->move()" << endl;}void car::stop() {    cout << "car->stop()" << endl;}int main(int argc, char* argv[]) {    int i = 0;    car ca;    ca.start();    ca.move();    ca.stop();    cin >> i;    return 0;}

进行私有成员函数转换后
main.h

#include <iostream>#ifndef _MAIN_H_#define _MAIN_H_using namespace std;class car{public :    car();    void start();    void move();    void stop();};#endif

main.cpp

#include "main.h"car::car(){}static void prepare(){    cout << "car->prepare()" << endl;}void car::start() {    prepare();    cout << "car->start()" << endl;}void car::move() {    cout << "car->move()" << endl;}void car::stop() {    cout << "car->stop()" << endl;}int main(int argc, char* argv[]) {    int i = 0;    car ca;    ca.start();    ca.move();    ca.stop();    cin >> i;    return 0;}

当然,如果希望更有效的组织这些帮助函数,可以将他们转移到一个专门的类中,进行重组,对外提供更多统一的服务。

4) 消除保护成员

保护的作用主要是为了区分两种不同的观众: 派生类作者和一般用户。

保护成员函数的功能对于基类和派生类非常方便,也有很好的封装性,但对于客户程序来说,一旦保护成员被修改,客户程序就会面临被重新编译的问题。此时,保护成员函数可以看做是一种对于公共接口的污染。

这里可以通过让派生类使用独立的功能组件替换基类中的保护成员,从而消除接口类中的保护成员。

独立的功能组件对于基类或派生类中成员变量的访问可能会受限,但通过提供一些辅助公共接口还是可以解决的。

5) 消除私有成员数据

消除私有成员包括两种:静态私有成员和非静态私有成员。

静态私有成员:把静态私有成员的定义从头文件中转移到 .c / .cpp文件中就可以了。

非静态私有成员 : 关于消除非静态私有成员,更多的是从类的设计上实现的,例如: Shape作为一个积累,同时定义一个ShapeImp作为细节的实现,将私有成员转移到Imp类中。

6) 消除编译时产生的函数

编译器产生的函数有时会方便程序的使用,但同时很难做到绝缘。

如果想做到绝缘,可以自己定义这些函数,并使用默认的方法实现这些函数,使它们变成可见的,而不是隐藏在编译器后面。

7) 消除include 指令

消除 #include 指令第一种方式就是发现不必要的引用,并删除它们。避免包含冗余的内容。 第二可以将其放在.c或.cpp文件中,在实现时在来引用。

8) 消除默认参数

使用默认参数的好处,可以使函数在调用时参数个数可变。
例如

class A{public:    void func(int x=0, double y=0.0, int z=0)}void main(){    A testa;    a.func(0);  // 合法    a.func(01);  // 合法    a.func(012);  // 合法        }

使用时简便。

消除默认值的方式也比较直接,简单的定义三个函数,分别包含1, 2, 3个参数。

9) 消除枚举类型

对于枚举值,typedef 和其他有内部连接的结构的合理使用可以减少程序编译时的耦合。

一个简单的例子: (反例)

class test{private:    enum {DEFAULT_TABLE_SIZE=100};   // 情形 1public:    enum {DEFAULT_BUFF_SIZE=200};    // 情形 2    enum Status {A,B,C,D,E,F,};      // 情形 3    Status func();}

情形 1: 可以将其转移到.c 或.cpp 文件中,或使用一个const型变量,使用枚举并不合适。

情形 2: 同样也更适合使用一个私有的const 静态变量,同时提供一个接口可以对该变量进行访问。

情形 3: 本身是要成为接口的一部分,可以考虑将这些状态枚举的定义转移到其实现的部分,分散到各个组件中,虽然减少了重用的可能,但同时也增加了一定的灵活性。

同样的示例 : (修改后)

// test.hclass test{private:    static const int m_default_buff_size; // 情形 2public:    static int getDefaultBuffSize();      // 情形 2    enum Status {A,B,C};                  // 情形 3 减少枚举值,只在test内部有效。    Status func();}// test.cppenum {DEFAULT_TABLE_SIZE=100};  // 情形 1test::m_default_buff_size = 200; //  情形 2int test::getDefaultBuffSize()   //  情形 2{return m_default_buff_size;}test::Status test::func(){/*........*/}

整体的绝缘技术

在一个认真设计,精心规划的系统中,我们可以预先知道哪些接口是要开放的,这些信息可以帮助我们从一开始就把一些接口设计为绝缘的,从而避免后面的麻烦。

1) 协议类

理想情况下接口类不会定义实现细节,它只定义接口,通过接口来访问具体的实现类。

协议类的定义:

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

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

一个由绝缘类实现的系统实例

//file.h   协议类class File{public:    // TYPES    enum From{START, CURRENT, END};    virtual ~File();    virtual void seek(int distance, From location)=0;    virtual int read(char* buffer, int numBytes)=0;    virtual int write(const char* buffer, int numBytes)=0;    virtual int tell(From location)=0;}//filemgr.h  文件管理组件struct FileMgr{    static File* open(const char* filename);};

系统框图: FileImp的实现被隔离在了File和FileMgr的后面。

这里写图片描述

一个实例: 将非绝缘类变为绝缘类。

Elem 类, 非绝缘,并且依赖于 Foo 和 Bar两个外部类

#ifndef ELEM_H#define ELEM_H#ifndef INCLUDE_FOO#include foo.h#endif#ifndef INCLUDE_BAR#include bar.h#endifclass Elem{    Foo m_foo;    Bar m_bar;private:    /*  ... ... */protected:    /*  ... ... */public:    enum Status{GOOD=0, BAD, UGLY};    Elem();    Elem(Foo& foo);    Elem(Bar& bar);    Elem(Foo& foo, Bar &bar);    virtual ~Elem();    Elem& operator=(const Elem& elem);    static double f1() {/*  ... */};   // inlien    static void f2(double d);    Foo f3() const {/* ... */};   // inlien    void f4(const Foo& foo);    virtual const char* f5() const;    virtual void f6(const char* name);    virtual Status f7();}#endif

这是一个非绝缘类,可以通过下面步骤把它转化为一个协议类和实现的部分。

  1. 备份原有文件
  2. 复制 Elem 类,并改名为ElemImp
  3. 只保留Elem类中的公共接口部分,清除其余部分
  4. 将Elem保留的接口全部变为虚函数
  5. 去掉Elem中的#include,并将其转化为提前的声明类。

转化后Elem.h 的样子

#ifndef ELEM_H#define ELEM_Hclass Foo;class Elem{public:    enum Status{GOOD=0, BAD, UGLY};    virtual ~Elem();    Elem& operator=(const Elem& elem) = 0;    virtual Foo f3() const  = 0;      virtual void f4(const Foo& foo) = 0;    virtual const char* f5() const = 0;    virtual void f6(const char* name) = 0;    virtual Status f7() = 0;}#endif

关于 ElemImp类,它会作为Elem的具体实现类
其中,/* virtual */ 为可选的意思

#ifndef ELEMIMP_H#define ELEMIMP_H#ifndef INCLUDE_ELEM_H#include Elem.h#endif#ifndef INCLUDE_FOO#include foo.h#endif#ifndef INCLUDE_BAR#include bar.h#endifclass ElemImp : public Elem{    Foo m_foo;    Bar m_bar;private:    /*  ... ... */protected:    /*  ... ... */public:    enum Status{GOOD=0, BAD, UGLY};    Elem();    Elem(Foo& foo);    Elem(Bar& bar);    Elem(Foo& foo, Bar &bar);    /* virtual */ ~Elem();    /* virtual */ Elem& operator=(const Elem& elem);    static double f1() {/*  ... */};   // inlien    static void f2(double d);    /* virtual */ Foo f3() const {/* ... */};   // inlien    /* virtual */ void f4(const Foo& foo);    /* virtual */ const char* f5() const;    /* virtual */ virtual void f6(const char* name);    /* virtual */ virtual Status f7();}#endif

ElemImp的创建,作为绝缘实现的最后一个步骤。
这里可以通过定义结构体或者使用工厂模式都可以完成。将Elem的创建过程也封装起来。

2) 完全绝缘的具体类

这里主要是针对处理类中的私有的成员函数,使它们成为一个不透明的指针来进行绝缘。

一个实例的演化过程:

在左侧的程序中显然是无法绝缘的,example类需要了解类A,B,C的内存布局,导致了编译时的依赖。

中间的程序作了修改,将私有成员变成了指针。

最右侧的程序则绝缘的很彻底,将A,B,C的指针也封装了起来,在example类中它们彻底的成为了一个不透明的指针,达到了彻底绝缘的效果。

这里写图片描述

3) 绝缘的包装器

包装器可以看做是基于完全绝缘类的进一步应用。它主要的任务是协调各个底层被绝缘组件,而非实现他们的功能。

包装器对于绝缘考虑更为全面,从绝缘的考虑是一方面,从性能的考虑是另一方面。因为对于每个需要绝缘的类都定义一 example_i类都会造成内存的分配和释放,在有些时候还是需要进行权衡的。

包装器更像是一种协调机制,通过它我们可以获得绝缘的优势的最大化,同时保证性能。

绝缘与不绝缘不是一个绝对的选项。

4) 过程接口

过程接口,可以看做是封装了底层实现细节,对现有功能的一个重组。对外只提供接口的一种绝缘方式。
例如:数据库 SQL的例子。所以数据库的实现细节全部被隐藏,对外只是通过SQL语句来进行访问。


绝缘与不绝缘

绝缘技术的使用主要解决的问题是仿真底层类库程序的变化造成不必要的客户程序的重新编译。但它本身也带来了一定的系统开销,所以是否需要绝缘要进行仔细的衡量。

  • 组件是否被广泛的使用

  • 系统对性能的要求和成员函数的大小

    这里写图片描述

  • 对于大型系统,尽早考虑绝缘

0 0
原创粉丝点击