物理設計原則
原則 | 基本含義 |
---|---|
自滿足原則 | 標頭檔案本身是可以編譯通過的 |
單一職責原則 | 標頭檔案包含的實體的職責是單一的 |
最小依賴原則 | 絕不包含不必要的標頭檔案 |
最小可見性原則 | 儘量封裝隱藏類的成員 |
自滿足原則
所有標頭檔案都應該自滿足的。看一個具體的示例程式碼,這裡定義了一個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