现在的位置: 首页 > 综合 > 正文

C++中的接口设计

2014年02月27日 ⁄ 综合 ⁄ 共 4279字 ⁄ 字号 评论关闭

在用C++写要导出类的库时,我们经常只想暴露接口,而隐藏类的实现细节。也就是说我们提供的头文件里只提供要暴露的公共成员函数的声明,类的其他所有信息都不会在这个头文件里面显示出来。这个时候就要用到接口与实现分离的技术。
        下面用一个最简单的例子来说明。
        类ClxExp是我们要导出的类,其中有一个私有成员变量是ClxTest类的对象,各个文件内容如下:

        lxTest.h文件内容:

class ClxTest 
{
public:
    ClxTest();
    virtual ~ClxTest();
 
    void DoSomething();
};

        lxTest.cpp文件内容:

#include "lxTest.h"

#include <iostream>
using namespace std;

ClxTest::ClxTest()
{
}

ClxTest::~ClxTest()
{
}

void ClxTest::DoSomething()
{
    cout << "Do something in class ClxTest!" << endl;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        lxExp.h文件内容:

#include "lxTest.h"

class ClxExp 
{
public:
    ClxExp();
    virtual ~ClxExp();

    void DoSomething();

private:
    ClxTest m_lxTest;

    void lxTest();
};

        lxExp.cpp文件内容:

#include "lxExp.h"

ClxExp::ClxExp()
{
}

ClxExp::~ClxExp()
{
}

//  其实该方法在这里并没有必要,我这样只是为了说明调用关系
void ClxExp::lxTest()
{
    m_lxTest.DoSomething();
}

void ClxExp::DoSomething()
{
    lxTest();
}

        为了让用户能使用我们的类ClxExp,我们必须提供lxExp.h文件,这样类ClxExp的私有成员也暴露给用户了。而且,仅仅提供lxExp.h文 件是不够的,因为lxExp.h文件include了lxTest.h文件,在这种情况下,我们还要提供lxTest.h文件。那样ClxExp类的实现 细节就全暴露给用户了。另外,当我们对类ClxTest做了修改(如添加或删除一些成员变量或方法)时,我们还要给用户更新lxTest.h文件,而这个 文件是跟接口无关的。如果类ClxExp里面有很多像m_lxTest那样的对象的话,我们就要给用户提供N个像lxTest.h那样的头文件,而且其中
任何一个类有改动,我们都要给用户更新头文件。还有一点就是用户在这种情况下必须进行重新编译!上 面是非常小的一个例子,重新编译的时间可以忽略不计。但是,如果类ClxExp被用户大量使用的话,那么在一个大项目中,重新编译的时候我们就有时间可以 去喝杯咖啡什么的了。当然上面的种种情况不是我们想看到的!你也可以想像一下用户在自己程序不用改动的情况下要不停的更新头文件和编译时,他们心里会骂些 什么。其实对用户来说,他们只关心类ClxExp的接口DoSomething()方法。那我们怎么才能只暴露类ClxExp的DoSomething
()方法而不又产生上面所说的那些问题呢?答案就是--接口与实现的分离。我可以让类ClxExp定义接口,而把实现放在另外一个类里面。下面是具体的方法:

        首先,添加一个实现类ClxImplement来实现ClxExp的所有功能。注意:类ClxImplement有着跟类ClxExp一样的公有成员函数,因为他们的接口要完全一致

        lxImplement.h文件内容:

#include "lxTest.h"

class ClxImplement 
{
public:
    ClxImplement();
    ~ClxImplement();

    void DoSomething();

private:
    ClxTest m_lxTest;

    void lxTest();
};

        lxImplement.cpp文件内容:

#include "lxImplement.h"

ClxImplement::ClxImplement()
{
}

ClxImplement::~ClxImplement()
{
}

void ClxImplement::lxTest()
{
    m_lxTest.DoSomething();
}

void ClxImplement::DoSomething()
{
    lxTest();
}

        然后,修改类ClxExp。

        修改后的lxExp.h文件内容:

//  前置声明
class ClxImplement;

class ClxExp 
{
public:
    ClxExp();
    virtual ~ClxExp();

 void DoSomething();

private:
    //  声明一个类ClxImplement的指针,不需要知道类ClxImplement的定义
    ClxImplement *m_pImpl;
};

        修改后的lxExp.cpp文件内容:

//  在这里包含类ClxImplement的定义头文件
#include "lxImplement.h"

ClxExp::ClxExp()
{
    m_pImpl = new ClxImplement;
}

ClxExp::~ClxExp()
{
    if (m_pImpl)
        delete m_pImpl;
}

void ClxExp::DoSomething()
{
    m_pImpl->DoSomething();
}

        通过上面的方法就实现了类ClxExp的接口与实现的分离。请注意两个文件中的注释。类 ClxExp里面声明的只是接口而已,而真正的实现细节被隐藏到了类ClxImplement里面。为了能在类ClxExp中使用类 ClxImplement而不include头文件lxImplement.h,就必须有前置声明class ClxImplement,而且只能使用指向类ClxImplement对象的指针,否则就不能通过编译。在发布库文件的时候,我们只需给用户提供一个头
文件lxExp.h就行了,不会暴露类ClxExp的任何实现细节。而且我们对类ClxTest的任何改动,都不需要再给用户更新头文件(当然,库文件是 要更新的,但是这种情况下用户也不用重新编译)。这样做还有一个好处就是,可以在分析阶段由系统分析员或者高级程序员来先把类的接口定义好,甚至可以把接口代码写好(例如上面修改后的lxExp.h文件和lxExp.cpp文件),而把类的具体实现交给其他程序员开发。

千言万语,对于程序员来说,不如几行代码直观:
// foo.h
#include "afx.h"

class Foo
{
public:
 void Method(const char* filename);
private:
 CFile m_file;
};
上 面这样的代码应该许多人都写过,现在让我们用上面的标准来衡量一下,显然,foo.h需要包含afx.h,因为其中有CFile的定义,可是对于 foo.h的使用者,它需要了解CFile吗?打开后的文件是用MFC的CFile或是C库函数的FILE*或是WindowsAPI中的HANDLE或 者是标准库中的fstream来表示,对于它的使用者来说,一点都不重要,也不需要知道,虽然Foo中用private关键字来保证了m_file不会被 用户直接访问。这在一定程度上迷惑了初学者,以为用private就是封装了。其实private的重要作用是使类能保持其不变式,虽然m_file是
private成员,可是一旦实现需要改变,例如改用fstream,所有直接或间接包含了foo.h的文件都将面临重新编译。有些时候,重新编译是很可 怕的,例如你的工程非常大,而老板给你的机器配置太低,你就可以体会到了(这种方式用来要挟老板为你升级机器倒是很管用)。
如果重新编译你不在 乎,我这还有更重要的理由,你看看afx.h中有许多个好用的类,它们都充满诱惑,难道你没想过在用Foo时顺便再用afx.h中的东西吗?那个 private看着挺不爽的,想加点什么功能都不方便,添加一个数据还得再加两函数,难道你不想把东西放几个到public中吗?一旦你开始这么做,你等 着,不出两个月,如果代码还在维护,它一定已经千疮百孔、盘根错节,牵一发而动全身。
有几个简单的方法来改进这一点,先介绍使用前向声明,对于上面的例子,可以改写如下:
// foo.h
class CFile;
class Foo
{
public:
 void Method(const char* filename);
private:
 CFile* m_file;
};
OK,现在清爽多了。用户不用知道什么是CFile,而你可以在foo.cpp中再包含afx.h。这种方法还是有局限性,如果以后需要增加数据成员了,用户还是需要重新编译,另外对每一个数据成员都不厌其烦的加上前向声明也挺烦的。下面我再介绍一个更加彻底的版本。
// foo.h
class Imp;
class Foo
{
public:
 void Method(const char* filename);
private:
 Imp* _Imp;
};
这下无敌了。在foo.cpp中你可以随心所欲
// foo.cpp
class Imp
{
public:
 CFile m_file;
 string filename;
 // …
};
除了前向声明,还有一个方法是采用接口继承,这个我想留在COM那一章再讲。

转自 http://blog.donews.com/etherman/archive/2006/07/17/966262.aspx

另外一篇比较好的参考文章 从信息隐藏的一个需求看C++接口与实现的分离 http://blog.csdn.net/tonywearme/article/details/6926649

抱歉!评论已关闭.