物理设计原则
原则 | 基本含义 |
---|---|
自满足原则 | 头文件本身是可以编译通过的 |
单一职责原则 | 头文件包含的实体的职责是单一的 |
最小依赖原则 | 绝不包含不必要的头文件 |
最小可见性原则 | 尽量封装隐藏类的成员 |
自满足原则
所有头文件都应该自满足的。看一个具体的示例代码,这里定义了一个TestCase.h
头文件。TestCase
对父类TestLeaf, TestFixture
都存在编译时依赖,但没有包含基类的头文件。
反例:
// cppunit/TestCase.h
#ifndef EOPTIAWE_23908576823_MSLKJDFE_0567925
#define EOPTIAWE_23908576823_MSLKJDFE_0567925
struct TestCase : TestLeaf, TestFixture
{
TestCase(const std::string &name="");
private:
OVERRIDE(void run(TestResult *result));
OVERRIDE(std::string getName() const);
private:
ABSTRACT(void runTest());
private:
const std::string name;
};
#endif
为了满足自满足原则,其自身必须包含其所有父类的头文件。
正例:
// cppunit/TestCase.h
#ifndef EOPTIAWE_23908576823_MSLKJDFE_0567925
#define EOPTIAWE_23908576823_MSLKJDFE_0567925
#include "cppunit/core/TestLeaf.h"
#include "cppunit/core/TestFixture.h"
struct TestCase : TestLeaf, TestFixture
{
TestCase(const std::string &name="");
private:
OVERRIDE(void run(TestResult &result));
OVERRIDE(std::string getName() const);
private:
ABSTRACT(void runTest());
private:
const std::string name;
};
#endif
即使TestCase
直接持有name
的成员变量,但没有必要包含std::string
的头文件,因为TestCase
覆写了其父类的getName
成员函数,父类为了保证自满足原则,自然已经包含了std::string
的头文件。
同样的原因,也没有必要在此前置声明TestResult
,因为父类可定已经声明过了。
单一职责
这是`SRP(Single Reponsibility
Priciple)`在头文件设计时的一个具体运用。头文件如果包含了其它不相关的元素,则包含该头文件的所有实现文件都将被这些不相关的元素所污染,重编译将成为一件高概率的事件。
如示例代码,将OutputStream, InputStream
同时定义在一个头文件中,将违背该原则。本来只需只读接口,无意中被只写接口所污染。
反例:
// io/Stream.h
#ifndef LDGOUIETA_437689Q20_ASIOHKFGP_980341
#define LDGOUIETA_437689Q20_ASIOHKFGP_980341
#include "base/Role.h"
DEFINE_ROLE(OutputStream)
{
ABSTRACT(void write());
};
DEFINE_ROLE(InputStream)
{
ABSTRACT(void read());
};
#endif
正例: 先创建一个OutputStream.h
文件:
// io/OutputStream.h
#ifndef LDGOUIETA_437689Q20_ASIOHKFGP_010234
#define LDGOUIETA_437689Q20_ASIOHKFGP_010234
#include "base/Role.h"
DEFINE_ROLE(OutputStream)
{
ABSTRACT(void write());
};
#endif
再创建一个InputStream.h
文件:
// io/InputStream.h
#ifndef LDGOUIETA_437689Q20_ASIOHKFGP_783621
#define LDGOUIETA_437689Q20_ASIOHKFGP_783621
#include "base/Role.h"
DEFINE_ROLE(InputStream)
{
ABSTRACT(void read());
};
#endif
最小依赖
一个头文件只应该包含必要的实体,尤其在头文件中仅仅对实体的声明产生依赖,那么前置声明是一种有效的降低编译时依赖的技术。
反例:
// cppunit/Test.h
#ifndef PERTP_8792346_QQPKSKJ_09472_HAKHKAIE
#define PERTP_8792346_QQPKSKJ_09472_HAKHKAIE
#include <base/Role.h>
#include <cppunit/core/TestResult.h>
#include <string>
DEFINE_ROLE(Test)
{
ABSTRACT(void run(TestResult& result));
ABSTRACT(int countTestCases() const);
ABSTRACT(int getChildTestCount() const);
ABSTRACT(std::string getName() const);
};
#endif
如示例代码,定义了一个xUnit
框架中的Test
顶级接口,其对TestResult
的依赖仅仅是一个声明依赖,并没有必要包含TestResult.h
,前置声明是解开这类编译依赖的钥匙。
值得注意的是,对标准库std::string
的依赖,即使它仅作为返回值,但因为它实际上是一个typedef
,所以必须老实地包含其对应的头文件。事实上,如果产生了对标准库名称的依赖,基本上都需要包含对应的头文件。
另外,对DEFINE_ROLE
宏定义的依赖则需要包含相应的头文件,以便实现该头文件的自满足。
但是,TestResult
仅作为成员函数的参数出现在头文件中,所以对TestResult
的依赖只需前置声明即可。
正例:
// cppunit/Test.h
#ifndef PERTP_8792346_QQPKSKJ_09472_HAKHKAIE
#define PERTP_8792346_QQPKSKJ_09472_HAKHKAIE
#include <base/Role.h>
#include <string>
struct TestResult;
DEFINE_ROLE(Test)
{
ABSTRACT(void run(TestResult& result));
ABSTRACT(int countTestCases() const);
ABSTRACT(int getChildTestCount() const);
ABSTRACT(std::string getName() const);
};
#endif
在选择包含头文件还是前置声明时,很多程序员感到迷茫。其实规则很简单,在如下场景前置声明即可,无需包含头文件:
指针
引用
返回值
函数参数
相反地,如果编译器需要知道实体的真正内容时,则必须包含头文件,此依赖也常常称为强编译时依赖。强编译时依赖主要包括如下几种场景:
typedef
定义的实体继承
宏
inline
template
引用类内部成员时
sizeof
运算
最小可见性
在头文件中定义一个类时,清晰、准确的public, protected, private
是传递设计意图的指示灯。其中private
做为一种实现细节被隐藏起来,为适应未来不明确的变化提供便利的措施。
不要将所有的实体都public
,这无疑是一种自杀式做法。应该以一种相反的习惯性思维,尽最大可能性将所有实体private
,直到你被迫不得不这么做为止,依次放开可见性的权限。
如下例代码所示,按照public-private, function-data
依次排列类的成员,并对具有相同特征的成员归类,将大大改善类的整体布局,给读者留下清晰的设计意图。
反例:
//trans-dsl/sched/SimpleAsyncAction.h
#ifndef IUOTIOUQW_NMAKLKLG_984592_KJSDKLJFLK
#define IUOTIOUQW_NMAKLKLG_984592_KJSDKLJFLK
#include "trans-dsl/action/Action.h"
#include "trans-dsl/utils/EventHandlerRegistry.h"
struct SimpleAsyncAction : Action
{
template<typename T>
Status waitOn(const EventId eventId, T* thisPointer,
Status (T::*handler)(const TransactionInfo&, const Event&),
bool forever = false)
{
return registry.addHandler(eventId, thisPointer, handler, forever);
}
Status waitUntouchEvent(const EventId eventId);
OVERRIDE(Status handleEvent(const TransactionInfo&, const Event&));
OVERRIDE(void kill(const TransactionInfo&, const Status));
DEFAULT(void, doKill(const TransactionInfo&, const Status));
EventHandlerRegistry registry;
};
#endif
正例:
// trans-dsl/sched/SimpleAsyncAction.h
#ifndef IUOTIOUQW_NMAKLKLG_984592_KJSDKLJFLK
#define IUOTIOUQW_NMAKLKLG_984592_KJSDKLJFLK
#include "trans-dsl/action/Action.h"
#include "trans-dsl/utils/EventHandlerRegistry.h"
struct SimpleAsyncAction : Action
{
template<typename T>
Status waitOn(const EventId eventId, T* thisPointer,
Status (T::*handler)(const TransactionInfo&, const Event&),
bool forever = false)
{
return registry.addHandler(eventId, thisPointer, handler, forever);
}
Status waitUntouchEvent(const EventId eventId);
private:
OVERRIDE(Status handleEvent(const TransactionInfo&, const Event&));
OVERRIDE(void kill(const TransactionInfo&, const Status));
private:
DEFAULT(void, doKill(const TransactionInfo&, const Status));
private:
EventHandlerRegistry registry;
};
#endif
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。