全部博文(47)
分类: C/C++
2009-03-31 11:31:21
下面用一个最简单的例子来说明。
类ClxEXP是我们要导出的类,其中有一个私有成员变量是ClxTest类的对象,各个文件内容如下:
lxTest.h文件内容:
class ClxTest
{
public:
ClxTest();
virtual ~ClxTest();
void DoSomething();
};
lxTest.cpp文件内容:
#include "lxTest.h"
#include
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();
virtual ~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()
{
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文件),而把类的具体实现交给其他程序员开发。
java和c#都不支持类的多继承,相对c++来说,又都增加了接口的概念,一个类可以实现多个接口;和c++一样,java和c#保留了抽象类的概念。在编码中很多情况下接口和抽象类可以互换,并且都能正确编译和运行,GOF23种设计模式在不同的书籍上也出现了接口和抽象类混用的情况。从语言层面讲,把接口看成是仅包含抽象属性和抽象方法的抽象类,勉强可以说得过去。所以很多程序员分不清什么时候该用接口,什么时候该用抽象类,只好任选其一。
首先,java和c#不支持类的多继承是很有道理的(类的单一职责原则),当你在设计时遇到必须用多继承的情景时,请检查你的类结构设计是否合理,或者把某些父类改成接口。
其次,接口中声明的成员方法不能有任何实现,这样如果两个类实现这个接口,有可能两个类需要编写同样的实现。而用抽象类则可以把实现写在抽象的父类中。
以上两点仅仅是语言层面上的区别,是显而易见的。如果做到良好的面向对象设计,把类和类关系定的合理,可能根本遇不到上述两种情况。我们来看一个场景:
固定翼飞机、军用固定翼飞机、民用固定翼飞机(民航客机)、孙悟空、海鸥
军用固定翼飞机、民用固定翼飞机,都属于固定翼飞机,有强烈的 is a 关系,所以应该从固定翼飞机继承;如果一架飞机是固定翼飞机,那么它不可能仅仅是固定翼飞机,因为它不是民用就是军用,所以固定翼飞机是抽象的,不会有它本身的实例,有的只是它子类的实例。所以固定翼飞机是 abstract class 。
上述五类事物都会飞,但显然,飞机、鸟、会飞的人完全不是一回事,不应该让它们继承于一个抽象的AbstractFly类,所以固定翼飞机、孙悟空、海鸥都可以实现IFly接口,却不能从同一个抽象类继承。孙悟空实现了IFly接口,同时还可以和猪八戒等实现IChangeSelf(会变)接口。
一个类可以实现多个接口,接口是没有实际意义的,所以即使一个类实现了再多接口,它还可以不违反类的单一职责原则。反之,继承于多个抽象类,大多会违反类的单一职责原则,要不然就是那几个抽象类设计的不合理。
另外,根据依赖倒置原则,当高层模块和底层模块不得不耦合时,最好由高层模块定义一个“规范”,让低层模块来实现,而不是高层模块直接调用低层模块的类和成员方法。“规范”在很多时候用接口和抽象类都可以编译和运行,但通过前文的例子,我们很容易知道这时应该用接口,因为高层模块仅仅是定义了一个规范,没有实际意义,而且高层模块并不了解低层模块的类结构,不该强行为低层模块设置父类,影响低层模块实现的灵活性。