- 設(shè)計(jì)原則
- 代碼規(guī)范
- 最佳實(shí)踐
- 附錄: 實(shí)用宏
設(shè)計(jì)原則
原則 | 基本含義 |
---|---|
自滿足原則 | 頭文件本身是可以編譯通過的 |
單一職責(zé)原則 | 頭文件包含的實(shí)體的職責(zé)是單一的 |
最小依賴原則 | 絕不包含不必要的頭文件 |
最小可見性原則 | 盡量封裝隱藏類的成員 |
自滿足原則
所有頭文件都應(yīng)該自滿足的盟劫。看一個(gè)具體的示例代碼砖茸,這里定義了一個(gè)TestCase.h
頭文件侣诵。TestCase
對(duì)父類TestLeaf, TestFixture
都存在編譯時(shí)依賴,但沒有包含基類的頭文件边苹。
反例:
// 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
的頭文件滩报,因?yàn)?code>TestCase覆寫了其父類的getName
成員函數(shù),父類為了保證自滿足原則播急,自然已經(jīng)包含了std::string
的頭文件脓钾。
同樣的原因,也沒有必要在此前置聲明TestResult
桩警,因?yàn)楦割惪啥ㄒ呀?jīng)聲明過了可训。
單一職責(zé)
這是SRP(Single Reponsibility Priciple)
在頭文件設(shè)計(jì)時(shí)的一個(gè)具體運(yùn)用。頭文件如果包含了其它不相關(guān)的元素捶枢,則包含該頭文件的所有實(shí)現(xiàn)文件都將被這些不相關(guān)的元素所污染握截,重編譯將成為一件高概率的事件。
如示例代碼烂叔,將OutputStream, InputStream
同時(shí)定義在一個(gè)頭文件中谨胞,將違背該原則。本來只需只讀接口蒜鸡,無意中被只寫接口所污染胯努。
反例:
// 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
正例: 先創(chuàng)建一個(gè)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
再創(chuàng)建一個(gè)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
最小依賴
一個(gè)頭文件只應(yīng)該包含必要的實(shí)體牢裳,尤其在頭文件中僅僅對(duì)實(shí)體的聲明產(chǎn)生依賴,那么前置聲明是一種有效的降低編譯時(shí)依賴的技術(shù)叶沛。
反例:
// 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
如示例代碼蒲讯,定義了一個(gè)xUnit
框架中的Test
頂級(jí)接口,其對(duì)TestResult
的依賴僅僅是一個(gè)聲明依賴灰署,并沒有必要包含TestResult.h
判帮,前置聲明是解開這類編譯依賴的鑰匙。
值得注意的是溉箕,對(duì)標(biāo)準(zhǔn)庫(kù)std::string
的依賴脊另,即使它僅作為返回值,但因?yàn)樗鼘?shí)際上是一個(gè)typedef
约巷,所以必須老實(shí)地包含其對(duì)應(yīng)的頭文件偎痛。事實(shí)上,如果產(chǎn)生了對(duì)標(biāo)準(zhǔn)庫(kù)名稱的依賴独郎,基本上都需要包含對(duì)應(yīng)的頭文件踩麦。
另外,對(duì)DEFINE_ROLE
宏定義的依賴則需要包含相應(yīng)的頭文件氓癌,以便實(shí)現(xiàn)該頭文件的自滿足谓谦。
但是,TestResult
僅作為成員函數(shù)的參數(shù)出現(xiàn)在頭文件中贪婉,所以對(duì)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
在選擇包含頭文件還是前置聲明時(shí),很多程序員感到迷茫疲迂。其實(shí)規(guī)則很簡(jiǎn)單才顿,在如下場(chǎng)景前置聲明即可,無需包含頭文件:
- 指針
- 引用
- 返回值
- 函數(shù)參數(shù)
相反地尤蒿,如果編譯器需要知道實(shí)體的真正內(nèi)容時(shí)郑气,則必須包含頭文件,此依賴也常常稱為強(qiáng)編譯時(shí)依賴腰池。強(qiáng)編譯時(shí)依賴主要包括如下幾種場(chǎng)景:
-
typedef
定義的實(shí)體 - 繼承
- 宏
inline
template
- 引用類內(nèi)部成員時(shí)
-
sizeof
運(yùn)算
最小可見性
在頭文件中定義一個(gè)類時(shí)尾组,清晰、準(zhǔn)確的public, protected, private
是傳遞設(shè)計(jì)意圖的指示燈示弓。其中private
做為一種實(shí)現(xiàn)細(xì)節(jié)被隱藏起來讳侨,為適應(yīng)未來不明確的變化提供便利的措施。
不要將所有的實(shí)體都public
奏属,這無疑是一種自殺式做法跨跨。應(yīng)該以一種相反的習(xí)慣性思維,盡最大可能性將所有實(shí)體private
拍皮,直到你被迫不得不這么做為止歹叮,依次放開可見性的權(quán)限。
如下例代碼所示铆帽,按照public-private, function-data
依次排列類的成員咆耿,并對(duì)具有相同特征的成員歸類,將大大改善類的整體布局爹橱,給讀者留下清晰的設(shè)計(jì)意圖萨螺。
反例:
//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
代碼規(guī)范
頭文件保護(hù)宏
每一個(gè)頭文件都應(yīng)該具有獨(dú)一無二的保護(hù)宏,并保持命名規(guī)則的一致性愧驱,其中命名規(guī)則包括兩種風(fēng)格:
INCL_<PROJECT>_<MODULE>_<FILE>_H
- 全局唯一的隨機(jī)序列碼
第一種命名規(guī)則問題在于:當(dāng)文件名重命名或移動(dòng)目錄時(shí)慰技,需要同步修改頭文件保護(hù)宏;推薦使用IDE
隨機(jī)自動(dòng)地生成頭文件保護(hù)宏组砚,其更加快捷吻商、簡(jiǎn)單、安全糟红、有效艾帐。
反例:
// thread/Runnable.h
// 因名稱太短,存在名字沖突的可能性
#ifndef RUNNABLE_H
#define RUNNABLE_H
#include "base/Role.h"
DEFINE_ROLE(Runnable)
{
ABSTRACT(void run());
};
#endif
正例:
// cppunit/AutoRegisterSuite.h
#ifndef INCL_CPPUNIT_AUTO_REGISTER_SUITE_H
#define INCL_CPPUNIT_AUTO_REGISTER_SUITE_H
#include "base/Role.h"
struct TestSuite;
DEFINE_ROLE(AtuoRegisterSuite)
{
ABSTRACT(void add(TestSuite&));
};
#endif
正例:
// cppunit/AutoRegisterSuite.h
// IDE自動(dòng)生成
#ifndef INCL_ADCM_LLL_3465_DCPOE_ACLDDDE_479_YTEY_H
#define INCL_ADCM_LLL_3465_DCPOE_ACLDDDE_479_YTEY_H
#include "base/Role.h"
struct TestSuite;
DEFINE_ROLE(AtuoRegisterSuite)
{
ABSTRACT(void add(TestSuite&));
};
#endif
下劃線與駝峰
路徑名一律使用小寫盆偿、下劃線或中劃線風(fēng)格的名稱柒爸;文件名應(yīng)該與程序主要實(shí)體名稱相同,可以使用駝峰命名事扭,也可以使用小寫捎稚、下劃線或中劃線分割的名字;實(shí)現(xiàn)文件的名字必須和頭文件保持一致求橄;包含頭文件時(shí)今野,必須保持路徑名、文件名大小寫敏感罐农。
反例:
// 路徑名htmlParser使用了駝峰命名風(fēng)格
#include "htmlParser/core/Attribute.h"
正例:
// 正確的頭文件包含
#include "html-parser/core/Attribute.h"
#include "yaml_parser.h"
最后腥泥,值得注意的是,團(tuán)隊(duì)內(nèi)必須保持一致的命名風(fēng)格啃匿。
大小寫敏感
包含頭文件時(shí)蛔外,必須保持路徑名、文件名大小寫敏感溯乒。因?yàn)樵赲ascii{Windows}夹厌,其大小寫不敏感,編譯時(shí)檢查失效裆悄,代碼失去了可移植性矛纹,所以在包含頭文件時(shí)必須保持文件名的大小寫敏感。
假如存在兩個(gè)物理文件名分別為SynchronizedObject.h, yaml_parser.h
的兩個(gè)文件光稼。
反例:
// 路徑名或南、文件名大小寫與真實(shí)物理路徑孩等、物理文件名稱不符
#include "CppUnit/Core/SynchronizedObject.h"
#include "YAML_Parser.h"
正例:
#include "cppunit/core/SynchronizedObject.h"
#include "yaml_parser.h"
最后,值得注意的是采够,團(tuán)隊(duì)內(nèi)必須保持一致的命名風(fēng)格肄方。
分隔符
包含頭文件時(shí),路徑分隔符一律使用Unix
風(fēng)格蹬癌,拒絕使用Windows
風(fēng)格权她;即采用/
而不是使用\
分割路徑。
反例:
// 使用了Windows風(fēng)格的路徑分割符
#include "cppunit\core\SynchronizedObject.h"
正例:
// 使用了Unix風(fēng)格的路徑分割符
#include "cppunit/core/SynchronizedObject.h"
extern "C"
使用extern "C"
時(shí)逝薪,不要包括include
語句隅要。
反例:
//oss/oss_memery.h
#ifndef HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#define HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#ifdef __cplusplus
extern "C" {
#endif
// 錯(cuò)誤地將include放在了extern "C"中
#include "oss_common.h"
void* oss_alloc(size_t);
void oss_free(void*);
#ifdef __cplusplus
}
#endif
#endif
正例:
//oss/oss_memery.h
#ifndef HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#define HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#include "oss_common.h"
#ifdef __cplusplus
extern "C" {
#endif
void* oss_alloc(size_t);
void oss_free(void*);
#ifdef __cplusplus
}
#endif
#endif
兼容性
當(dāng)以C
提供實(shí)現(xiàn)時(shí),頭文件中必須使用extern "C"
聲明董济,以便支持C++
的擴(kuò)展步清。
反例:
// oss/oss_memery.h
#ifndef HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#define HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#include "oss_common.h"
void* oss_alloc(size_t);
void oss_free(void*);
#endif
正例:
// oss/oss_memery.h
#ifndef HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#define HF0916DFB_1CD1_4811_B82B_9B8EB1A007D8
#include "oss_common.h"
#ifdef __cplusplus
extern "C" {
#endif
void* oss_alloc(size_t);
void oss_free(void*);
#ifdef __cplusplus
}
#endif
#endif
上帝頭文件
拒絕創(chuàng)建巨型頭文件,將所有實(shí)體聲明都放到頭文件中虏肾,而僅僅將外部依賴的實(shí)體聲明放到頭文件中尼啡。
信息隱藏
實(shí)現(xiàn)文件也是一種信息隱藏的慣用技術(shù),如果一些程序的實(shí)體不對(duì)外所依賴询微,則放在自己的實(shí)現(xiàn)文件中崖瞭,一則可降低依賴關(guān)系,二則實(shí)現(xiàn)更好的信息隱藏撑毛。
對(duì)于上帝頭文件书聚,其很多聲明和定義本來是不應(yīng)該放到頭文件,而應(yīng)該放會(huì)實(shí)現(xiàn)文件以便實(shí)現(xiàn)更好地信息隱藏藻雌。
編譯時(shí)依賴
巨型頭文件必然造成了巨大的編譯時(shí)依賴雌续,不僅僅帶來巨大的編譯時(shí)開銷,更重要的是這樣的設(shè)計(jì)將太多的實(shí)現(xiàn)細(xì)節(jié)暴露給用戶胯杭,導(dǎo)致后續(xù)版本兼容性的問題驯杜,阻礙了頭文件進(jìn)一步演進(jìn)、修改做个、擴(kuò)展的可能性鸽心,從而失去了軟件的可擴(kuò)展性。
include
順序依賴
不要認(rèn)為提供一個(gè)大而全的頭文件會(huì)給你的用戶帶來方便居暖,用戶因此而更加困擾顽频。對(duì)于一個(gè)巨大的頭文件,其依賴關(guān)系很難一眼看清楚太闺,其自滿足性很難得到保證糯景,用戶在包含此頭文件時(shí),還要關(guān)心頭文件之間的依賴關(guān)系,甚至關(guān)心include語句的順序蟀淮,但這樣的代碼實(shí)現(xiàn)是及其脆弱的最住。
最佳實(shí)踐
自滿足驗(yàn)證
為了驗(yàn)證頭文件設(shè)計(jì)的自滿足原則,實(shí)現(xiàn)文件的第一條語句必然是包含其對(duì)應(yīng)的頭文件怠惶。
反例:
// cppunit/TestCase.cpp
#include "cppunit/core/TestResult.h"
#include "cppunit/core/Functor.h"
// 錯(cuò)誤:沒有放在第一行涨缚,無法校驗(yàn)其自滿足性
#include "cppunit/core/TestCase.h"
namespace
{
struct TestCaseMethodFunctor : Functor
{
typedef void (TestCase::*Method)();
TestCaseMethodFunctor(TestCase &target, Method method)
: target(target), method(method)
{}
bool operator()() const
{
target.*method();
return true;
}
private:
TestCase ?
Method method;
};
}
void TestCase::run(TestResult &result)
{
result.startTest(*this);
if (result.protect(TestCaseMethodFunctor(*this, &TestCase::setUp)))
{
result.protect(TestCaseMethodFunctor(*this, &TestCase::runTest));
}
result.protect(TestCaseMethodFunctor(*this, &TestCase::tearDown));
result.endTest(*this);
}
...
正例:
// cppunit/TestCase.cpp
#include "cppunit/core/TestCase.h"
#include "cppunit/core/TestResult.h"
#include "cppunit/core/Functor.h"
namespace
{
struct TestCaseMethodFunctor : Functor
{
typedef void (TestCase::*Method)();
TestCaseMethodFunctor(TestCase &target, Method method)
: target(target), method(method)
{}
bool operator()() const
{
target.*method();
return true;
}
private:
TestCase ?
Method method;
};
}
void TestCase::run(TestResult &result)
{
result.startTest(*this);
if (result.protect(TestCaseMethodFunctor(*this, &TestCase::setUp))
{
result.protect(TestCaseMethodFunctor(*this, &TestCase::runTest));
}
result.protect(TestCaseMethodFunctor(*this, &TestCase::tearDown));
result.endTest(*this);
}
...
override
和private
所有override
的函數(shù)(除override
的virtual
析構(gòu)函數(shù)之外)都應(yīng)該是private
的,以保證按接口編程的良好設(shè)計(jì)原則甚疟。
反例:
// html-parser/filter/AndFilter.h
#ifndef EOIPWORPIO_06123124_NMVBNSDHJF_497392
#define EOIPWORPIO_06123124_NMVBNSDHJF_497392
#include "html-parser/filter/NodeFilter.h"
#include <list>
struct AndFilter : NodeFilter
{
void add(NodeFilter*);
// 設(shè)計(jì)缺陷:本應(yīng)該private
OVERRIDE(bool accept(const Node&) const);
private:
std::list<NodeFilter*> filters;
};
#endif
正例:
// html-parser/filter/AndFilter.h
#ifndef EOIPWORPIO_06123124_NMVBNSDHJF_497392
#define EOIPWORPIO_06123124_NMVBNSDHJF_497392
#include "html-parser/filter/NodeFilter.h"
#include <list>
struct AndFilter : NodeFilter
{
void add(NodeFilter*);
private:
OVERRIDE(bool accept(const Node&) const);
private:
std::list<NodeFilter*> filters;
};
#endif
inline
避免頭文件中inline
頭文件中避免定義inline
函數(shù)仗岖,除非性能報(bào)告指出此函數(shù)是性能的關(guān)鍵瓶頸逃延。
C++
語言將聲明和實(shí)現(xiàn)進(jìn)行分離览妖,程序員為此不得不在頭文件和實(shí)現(xiàn)文件中重復(fù)地對(duì)函數(shù)進(jìn)行聲明。這是C/C++
天生給我們的設(shè)計(jì)帶來的重復(fù)揽祥。這是一件痛苦的事情讽膏,驅(qū)使部分程序員直接將函數(shù)實(shí)現(xiàn)為inline
。
但inline
函數(shù)的代碼作為一種不穩(wěn)定的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)拄丰,被放置在頭文件里府树,其變更所導(dǎo)致的大面積的重新編譯是個(gè)大概率事件,為改善微乎其微的函數(shù)調(diào)用性能與其相比將得不償失料按。
除非有相關(guān)profiling
性能測(cè)試報(bào)告奄侠,表明這部分關(guān)鍵的熱點(diǎn)代碼需要被放回頭文件中。
但需要注意在特殊的情況载矿,可以將實(shí)現(xiàn)inline
在頭文件中垄潮,因?yàn)闉樗鼈儎?chuàng)建實(shí)現(xiàn)文件過于累贅和麻煩。
-
virtual
析構(gòu)函數(shù) - 空的
virtual
函數(shù)實(shí)現(xiàn) -
C++11
的default
函數(shù)
鼓勵(lì)實(shí)現(xiàn)文件中inline
對(duì)于在編譯單元內(nèi)部定義的類而言闷盔,因?yàn)樗目蛻魯?shù)量是確定的弯洗,就是它本身。另外逢勾,由于它本來就定義在源代碼文件中牡整,因此并沒有增加任何“物理耦合”。所以溺拱,對(duì)于這樣的類逃贝,我們大可以將其所有函數(shù)都實(shí)現(xiàn)為inline
的,就像寫Java
代碼那樣迫摔,Once & Only Once
秋泳。
以單態(tài)類的一種實(shí)現(xiàn)技術(shù)為例,講解編譯時(shí)依賴的解耦與匿名命名空間的使用攒菠。(首先迫皱,應(yīng)該抵制單態(tài)設(shè)計(jì)的誘惑,單態(tài)其本質(zhì)是面向?qū)ο蠹夹g(shù)中全局變量的替代品。濫用單態(tài)模式卓起,猶如濫用全局變量和敬,是一種典型的設(shè)計(jì)壞味道。只有確定在系統(tǒng)中唯一存在的概念戏阅,才能使用單態(tài)模式)昼弟。
實(shí)現(xiàn)單態(tài),需要對(duì)系統(tǒng)中唯一存在的概念進(jìn)行封裝奕筐;但這個(gè)概念往往具有巨大的數(shù)據(jù)結(jié)構(gòu)舱痘,如果將其聲明在頭文件中,無疑造成很大的編譯時(shí)依賴离赫。
反例:
// ne/NetworkElementRepository.h
#ifndef UIJVASDF_8945873_YUQWTYRDF_85643
#define UIJVASDF_8945873_YUQWTYRDF_85643
#include "base/Status.h"
#include "base/BaseTypes.h"
#include "transport/ne/NetworkElement.h"
#include <vector>
struct NetworkElementRepository
{
static NetworkElement& getInstance();
Status add(const U16 id);
Status release(const U16 id);
Status modify(const U16 id);
private:
typedef std::vector<NetworkElement> NetworkElements;
NetworkElements elements;
};
#endif
受文章篇幅的所限芭逝,NetworkElement.h
未列出所有代碼實(shí)現(xiàn),但我們知道NetworkElement
擁有巨大的數(shù)據(jù)結(jié)構(gòu)渊胸,上述設(shè)計(jì)導(dǎo)致所有包含NetworkElementRepository
的頭文件都被NetworkElement
所間接污染旬盯。
此時(shí),其中可以將依賴置入到實(shí)現(xiàn)文件中翎猛,解除揭開其嚴(yán)重的編譯時(shí)依賴胖翰。更重要的是,它更好地遵守了按接口編程的原則切厘,改善了軟件的擴(kuò)展性萨咳。
正例:
// ne/NetworkElementRepository.h
#ifndef UIJVASDF_8945873_YUQWTYRDF_85643
#define UIJVASDF_8945873_YUQWTYRDF_85643
#include "base/Status.h"
#include "base/BaseTypes.h"
#include "base/Role.h"
DEFINE_ROLE(NetworkElementRepository)
{
static NetworkElementRepository& getInstance();
ABSTRACT(Status add(const U16 id));
ABSTRACT(Status release(const U16 id));
ABSTRACT(Status modify(const U16 id));
};
#endif
其實(shí)現(xiàn)文件包含NetworkElement.h
,將對(duì)其的依賴控制在本編譯單元內(nèi)部疫稿。
// ne/NetworkElementRepository.cpp}]
#include "transport/ne/NetworkElementRepository.h"
#include "transport/ne/NetworkElement.h"
#include <vector>
namespace
{
struct NetworkElementRepositoryImpl : NetworkElementRepository
{
OVERRIDE(Status add(const U16 id))
{
// inline implements
}
OVERRIDE(Status release(const U16 id))
{
// inline implements
}
OVERRIDE(Status modify(const U16 id))
{
// inline implements
}
private:
typedef std::vector<NetworkElement> NetworkElements;
NetworkElements elements;
};
}
NetworkElementRepository& NetworkElementRepository::getInstance()
{
static NetworkElementRepositoryImpl inst;
return inst;
}
此處培他,對(duì)NetworkElementRepositoryImpl
類的依賴是非常明確的,僅本編譯單元內(nèi)而克,所有可以直接進(jìn)行inline
靶壮,從而簡(jiǎn)化了很多實(shí)現(xiàn)。
匿名namespace
匿名namespace
的存在常常被人遺忘员萍,但它的確是一個(gè)利器腾降。匿名namespace
的存在,使得所有受限于編譯單元內(nèi)的實(shí)體擁有了明確的處所碎绎。
自此之后螃壤,所有C
風(fēng)格并局限于編譯單元內(nèi)的static
函數(shù)和變量;以及類似Java
中常見的private static
的提取函數(shù)將常常被匿名namespace
替代筋帖。
請(qǐng)記住匿名命名空間也是一種重要的信息隱藏技術(shù)奸晴。在實(shí)現(xiàn)文件中提倡使用匿名namespace
, 以避免潛在的命名沖突。
如上例日麸,NetworkElementRepository.cpp
通過匿名namespace
寄啼,極大地減低了其頭文件的編譯時(shí)依賴逮光。
struct
VS. class
除了名字不同之外,class
和struct
唯一的差別是:默認(rèn)可見性墩划。這體現(xiàn)在定義和繼承時(shí)涕刚。struct
在定義一個(gè)成員,或者繼承時(shí)乙帮,如果不指明杜漠,則默認(rèn)為public
,而class
則默認(rèn)為private
察净。
但這些都不是重點(diǎn)驾茴,重點(diǎn)在于定義接口和繼承時(shí),冗余public
修飾符總讓人不舒服氢卡。簡(jiǎn)單設(shè)計(jì)四原則告訴告訴我們锈至,所有冗余的代碼都應(yīng)該被剔除。
但很多人會(huì)認(rèn)為struct
是C
遺留問題异吻,應(yīng)該避免使用裹赴。但這不是問題喜庞,我們不應(yīng)該否認(rèn)在寫C++
程序時(shí)诀浪,依然在使用著很多C
語言遺留的特性。關(guān)鍵在于延都,我們使用的是C
語言中能給設(shè)計(jì)帶來好處的特性雷猪,何樂而不為呢?
正例:
// hamcrest/SelfDescribing.h
#ifndef OIWER_NMVCHJKSD_TYT_48457_GSDFUIE
#define OIWER_NMVCHJKSD_TYT_48457_GSDFUIE
struct Description;
struct SelfDescribing
{
virtual void describeTo(Description& description) const = 0;
virtual ~SelfDescribing() {}
};
#endif
反例:
// hamcrest/SelfDescribing.h
#ifndef OIWER_NMVCHJKSD_TYT_48457_GSDFUIE
#define OIWER_NMVCHJKSD_TYT_48457_GSDFUIE
class Description;
class SelfDescribing
{
public:
virtual void describeTo(Description& description) const = 0;
virtual ~SelfDescribing() {}
};
#endif
更重要的是晰房,我們確信“抽象”和“信息隱藏”對(duì)于軟件的重要性求摇,這促使我將public
接口總置于類的最前面成為我們的首選,class
的特性正好與我們的期望背道而馳(class
的特性正好適合于將數(shù)據(jù)結(jié)構(gòu)捧為神物的程序員殊者,它們常常將數(shù)據(jù)結(jié)構(gòu)置于類聲明的最前面与境。)
不管你信仰那一個(gè)流派,切忌不能混合使用class
和struct
猖吴。在大量使用前導(dǎo)聲明的情況下摔刁,一旦一個(gè)使用struct
的類改為class
,所有的前置聲明都需要修改海蔽。
萬惡的struct tag
定義C
風(fēng)格的結(jié)構(gòu)體時(shí)共屈,struct tag
徹底抑制了結(jié)構(gòu)體前置聲明的可能性,從而阻礙了編譯優(yōu)化的空間党窜。
反例:
// radio/domain/Cell.h
#ifndef AQTYER_023874_NMHSFHKE_7432378293
#define AQTYER_023874_NMHSFHKE_7432378293
typedef struct tag_Cell
{
WORD16 wCellId;
WORD32 dwDlArfcn;
} T_Cell;
#endif
// radio/domain/Cell.h
#ifndef AQTYER_023874_NMHSFHKE_7432378293
#define AQTYER_023874_NMHSFHKE_7432378293
typedef struct
{
WORD16 wCellId;
WORD32 dwDlArfcn;
} T_Cell;
#endif
為了兼容C
并為結(jié)構(gòu)體前置聲明提供便利拗引,如下解法是最合適的。
正例:
// radio/domain/Cell.h
#ifndef AQTYER_023874_NMHSFHKE_7432378293
#define AQTYER_023874_NMHSFHKE_7432378293
typedef struct T_Cell
{
WORD16 wCellId;
WORD32 dwDlArfcn;
} T_Cell;
#endif
需要注意的是幌衣,在C
語言中矾削,如果沒有使用typedef
,則定義一個(gè)結(jié)構(gòu)體的指針,必須顯式地加上struct
關(guān)鍵字:struct T_Cell *pcell
哼凯,而C++
沒有這方面的要求垦细。
PIMPL
如果性能不是關(guān)鍵問題,考慮使用PIMPL
降低編譯時(shí)依賴挡逼。
反例:
// mockcpp/ApiHook.h
#ifndef OIWTQNVHD_10945_HDFIUE_23975_HFGA
#define OIWTQNVHD_10945_HDFIUE_23975_HFGA
#include "mockcpp/JmpOnlyApiHook.h"
struct ApiHook
{
ApiHook(const void* api, const void* stub)
: stubHook(api, stub)
{}
private:
JmpOnlyApiHook stubHook;
};
#endif
正例:
// mockcpp/ApiHook.h
#ifndef OIWTQNVHD_10945_HDFIUE_23975_HFGA
#define OIWTQNVHD_10945_HDFIUE_23975_HFGA
struct ApiHookImpl;
struct ApiHook
{
ApiHook(const void* api, const void* stub);
~ApiHook();
private:
ApiHookImpl* This;
};
#endif
// mockcpp/ApiHook.cpp
#include "mockcpp/ApiHook.h"
#include "mockcpp/JmpOnlyApiHook.h"
struct ApiHookImpl
{
ApiHookImpl(const void* api, const void* stub)
: stubHook(api, stub)
{
}
JmpOnlyApiHook stubHook;
};
ApiHook::ApiHook( const void* api, const void* stub)
: This(new ApiHookImpl(api, stub))
{
}
ApiHook::~ApiHook()
{
delete This;
}
通過ApiHookImpl* This
的橋接括改,在頭文件中解除了對(duì)JmpOnlyApiHook
的依賴,將其依賴控制在本編譯單元內(nèi)部家坎。
template
編譯時(shí)依賴
當(dāng)選擇模板時(shí)嘱能,不得不將其實(shí)現(xiàn)定義在頭文件中。當(dāng)編譯時(shí)依賴開銷非常大時(shí)虱疏,編譯模板將成為一種負(fù)擔(dān)惹骂。設(shè)法降低編譯時(shí)依賴,不僅僅為了縮短編譯時(shí)間做瞪,更重要的是為了得到一個(gè)低耦合的實(shí)現(xiàn)对粪。
反例:
// oss/OssSender.h
#ifndef HGGAOO_4611330_NMSDFHW_86794303_HJHASI
#define HGGAOO_4611330_NMSDFHW_86794303_HJHASI
#include "pub_typedef.h"
#include "pub_oss.h"
#include "oss_comm.h"
#include "pub_commdef.h"
#include "base/Assertions.h"
#include "base/Status.h"
struct OssSender
{
OssSender(const PID& pid, const U8 commType)
: pid(pid), commType(commType)
{
}
template <typename MSG>
Status send(const U16 eventId, const MSG& msg)
{
DCM_ASSERT_TRUE(OSS_SendAsynMsg(eventId, &msg, sizeof(msg), commType,(PID*)&pid) == OSS_SUCCESS);
return DCM_SUCCESS;
}
private:
PID pid;
U8 commType;
};
#endif
為了實(shí)現(xiàn)模板函數(shù)send
,將OSS
的一些實(shí)現(xiàn)細(xì)節(jié)暴露到了頭文件中装蓬,包含OssSender.h
的所有文件將無意識(shí)地產(chǎn)生了對(duì)OSS
頭文件的依賴著拭。
提取一個(gè)私有的send
函數(shù),并將對(duì)OSS
的依賴移入到OssSender.cpp
中牍帚,對(duì)PID
依賴通過前置聲明解除儡遮,最終實(shí)現(xiàn)如代碼所示。
正例:
// oss/OssSender.h
#ifndef HGGAOO_4611330_NMSDFHW_86794303_HJHASI
#define HGGAOO_4611330_NMSDFHW_86794303_HJHASI
#include "base/Status.h"
#include "base/BaseTypes.h"
struct PID;
struct OssSender
{
OssSender(const PID& pid, const U16 commType)
: pid(pid), commType(commType)
{
}
template <typename MSG>
Status send(const U16 eventId, const MSG& msg)
{
return send(eventId, (const void*)&msg, sizeof(MSG));
}
private:
Status send(const U16 eventId, const void* msg, size_t size);
private:
const PID& pid;
U8 commType;
};
#endif
識(shí)別哪些與泛型相關(guān)暗赶,哪些與泛型無關(guān)的知識(shí)鄙币,并解開此類編譯時(shí)依賴是C++
程序員的必備之技。
顯式模板實(shí)例化
模板的編譯時(shí)依賴存在兩個(gè)基本模型:包含模型蹂随,export
模型十嘿。export
模型受編譯技術(shù)實(shí)現(xiàn)的挑戰(zhàn),最終被C++11
標(biāo)準(zhǔn)放棄岳锁。
此時(shí)绩衷,似乎我們只能選擇包含模型。其實(shí)浸锨,存在一種特殊的場(chǎng)景唇聘,適時(shí)選擇顯式模板實(shí)例化(Explicit Template Instantiated)
,降低模板的編譯時(shí)依賴柱搜。是能做到降低模板編譯時(shí)依賴的迟郎。
反例:
// quantity/Quantity.h
#ifndef HGGQMVJK_892302_NGFSLEU_796YJ_GF5284
#define HGGQMVJK_892302_NGFSLEU_796YJ_GF5284
#include <quantity/Amount.h>
template <typename Unit>
struct Quantity
{
Quantity(const Amount amount, const Unit& unit)
: amountInBaseUnit(unit.toAmountInBaseUnit(amount))
{}
bool operator==(const Quantity& rhs) const
{
return amountInBaseUnit == rhs.amountInBaseUnit;
}
bool operator!=(const Quantity& rhs) const
{
return !(*this == rhs);
}
private:
const Amount amountInBaseUnit;
};
#endif
// quantity/Length.h
#ifndef TYIW7364_JG6389457_BVGD7562_VNW12_JFH
#define TYIW7364_JG6389457_BVGD7562_VNW12_JFH
#include "quantity/Quantity.h"
#include "quantity/LengthUnit.h"
typedef Quantity<LengthUnit> Length;
#endif
// quantity/Volume.h
#ifndef HG764MD_NKGJKDSJLD_RY64930_NVHF977E
#define HG764MD_NKGJKDSJLD_RY64930_NVHF977E
#include "quantity/Quantity.h"
#include "quantity/VolumeUnit.h"
typedef Quantity<VolumeUnit> Volume;
#endif
如上的設(shè)計(jì),泛型類Quantity
的實(shí)現(xiàn)都放在了頭文件聪蘸,不穩(wěn)定的實(shí)現(xiàn)細(xì)節(jié)宪肖,例如計(jì)算amountInBaseUnit
的算法變化等因素表制,將導(dǎo)致包含Length
或Volume
的所有源文件都需要重新編譯。
更重要的是控乾,因?yàn)?code>LengthUnit, VolumeUnit頭文件的包含么介,如果因需求變化需要增加支持的單位,將間接導(dǎo)致了包含Length
或Volume
的所有源文件也需要重新編譯蜕衡。
如何控制和隔離Quantity, LengthUnit, VolumeUnit
變化的蔓延壤短,而避免大部分的客戶代碼重新編譯,從而與客戶徹底解偶呢慨仿?可以通過顯式模板實(shí)例化將模板實(shí)現(xiàn)從頭文件中剝離出去久脯,從而避免了不必要的依賴。
正例:
// quantity/Quantity.h
#ifndef HGGQMVJK_892302_NGFSLEU_796YJ_GF5284
#define HGGQMVJK_892302_NGFSLEU_796YJ_GF5284
#include <quantity/Amount.h>
template <typename Unit>
struct Quantity
{
Quantity(const Amount amount, const Unit& unit);
bool operator==(const Quantity& rhs) const;
bool operator!=(const Quantity& rhs) const;
private:
const Amount amountInBaseUnit;
};
#endif
// quantity/Quantity.tcc
#ifndef FKJHJT68302_NVGKS97474_YET122_HEIW8565
#define FKJHJT68302_NVGKS97474_YET122_HEIW8565
#include <quantity/Quantity.h>
template <typename Unit>
Quantity<Unit>::Quantity(const Amount amount, const Unit& unit)
: amountInBaseUnit(unit.toAmountInBaseUnit(amount))
{}
template <typename Unit>
bool Quantity<Unit>::operator==(const Quantity& rhs) const
{
return amountInBaseUnit == rhs.amountInBaseUnit;
}
template <typename Unit>
bool Quantity<Unit>::operator!=(const Quantity& rhs) const
{
return !(*this == rhs);
}
#endif
// quantity/Length.h
#ifndef TYIW7364_JG6389457_BVGD7562_VNW12_JFH
#define TYIW7364_JG6389457_BVGD7562_VNW12_JFH
#include "quantity/Quantity.h"
struct LengthUnit;
struct Length : Quantity<LengthUnit> {};
#endif
// quantity/Length.cpp
#include "quantity/Quantity.tcc"
#include "quantity/LengthUnit.h"
template struct Quantity<LengthUnit>;
// quantity/Volume.h
#ifndef HG764MD_NKGJKDSJLD_RY64930_NVHF977E
#define HG764MD_NKGJKDSJLD_RY64930_NVHF977E
#include "quantity/Quantity.h"
struct VolumeUnit;
struct Volume : Quantity<VolumeUnit> {};
#endif
// quantity/Volume.cpp
#include "quantity/Quantity.tcc"
#include "quantity/VolumeUnit.h"
template struct Quantity<VolumeUnit>;
Length.h
僅僅對(duì)Quantity.h
產(chǎn)生依賴; 特殊地镰吆,Length.cpp
沒有產(chǎn)生對(duì)Length.h
的依賴帘撰,相反對(duì)Quantity.tcc
產(chǎn)生了依賴。
另外万皿,Length.h
對(duì)LengthUnit
的依賴關(guān)系也簡(jiǎn)化為聲明依賴摧找,而對(duì)其真正的編譯時(shí)依賴,也控制在模板實(shí)例化的時(shí)刻牢硅,即在Length.cpp
內(nèi)部蹬耘。
LenghtUnit, VolumeUnit
的變化,及其Quantity.tcc
實(shí)現(xiàn)細(xì)節(jié)的變化唤衫,被完全地控制在Length.cpp, Volume.cpp
內(nèi)部婆赠。
子類化優(yōu)于typedef/using
如果使用typedef
绵脯,如果存在對(duì)Length
的依賴佳励,即使是名字的聲明依賴,除了包含頭文件之外蛆挫,別無選擇赃承。
另外,如果Quantity
存在virtual
函數(shù)時(shí)悴侵,Length
還有進(jìn)一步擴(kuò)展Quantity
的可能性瞧剖,從而使設(shè)計(jì)提供了更大的靈活性。
反例:
// quantity/Length.h
#ifndef TYIW7364_JG6389457_BVGD7562_VNW12_JFH
#define TYIW7364_JG6389457_BVGD7562_VNW12_JFH
#include "quantity/Quantity.h"
struct LengthUnit;
typedef Quantity<LengthUnit> Length;
#endif
正例:
// quantity/Length.h
#ifndef TYIW7364_JG6389457_BVGD7562_VNW12_JFH
#define TYIW7364_JG6389457_BVGD7562_VNW12_JFH
#include "quantity/Quantity.h"
struct LengthUnit;
struct Length : Quantity<LengthUnit> {};
#endif
附錄:實(shí)用宏
為了提高代碼的表現(xiàn)力可免,規(guī)范中使用了一部分實(shí)用的宏定義抓于。
// base/Default.h
#ifndef GKOQWPRT_1038935_NCVBNMZHJS_8909603
#define GKOQWPRT_1038935_NCVBNMZHJS_8909603
namespace details
{
template <typename T>
struct DefaultValue
{
static T value()
{
return T();
}
};
template <typename T>
struct DefaultValue<T*>
{
static T* value()
{
return 0;
}
};
template <typename T>
struct DefaultValue<const T*>
{
static T* value()
{
return 0;
}
};
template <>
struct DefaultValue<void>
{
static void value()
{
}
};
}
#define DEFAULT(type, method) \
virtual type method { return ::details::DefaultValue<type>::value(); }
#endif
DEFAULT
對(duì)于定義空實(shí)現(xiàn)的virtual
函數(shù)非常方便。需要注意的是浇借,所有計(jì)算都是發(fā)生在編譯時(shí)的捉撮。
// base/Keywords.h
#ifndef H16274882_9153_4DB2_A2E2_F23D4CCB9381
#define H16274882_9153_4DB2_A2E2_F23D4CCB9381
#include "base/Config.h"
#include "base/Default.h"
#define ABSTRACT(...) virtual __VA_ARGS__ = 0
#if __SUPPORT_VIRTUAL_OVERRIDE
# define OVERRIDE(...) virtual __VA_ARGS__ override
#else
# define OVERRIDE(...) virtual __VA_ARGS__
#endif
#define EXTENDS(...) , ##__VA_ARGS__
#define IMPLEMENTS(...) EXTENDS(__VA_ARGS__)
#endif
Config.h
提供了編譯器支持C++11
特性的配置信息。ABSTRACT, OVERRIDE, EXTENDS, IMPLEMENTS
等關(guān)鍵字妇垢,使得Java
程序員也能看懂C++
的代碼巾遭,也極大地改善了C++
的表現(xiàn)力肉康。
// base/Role.h
#ifndef HF95EF112_D6C6_4DB0_8C1A_BE5A6CF8E3F1
#define HF95EF112_D6C6_4DB0_8C1A_BE5A6CF8E3F1
#include <base/Keywords.h>
namespace details
{
template <typename T>
struct Role
{
virtual ~Role() {}
};
}
#define DEFINE_ROLE(type) struct type : ::details::Role<type>
#endif
通過DEFINE_ROLE
的宏定義來實(shí)現(xiàn)對(duì)接口的定義,從而可以消除子類對(duì)虛擬析構(gòu)函數(shù)的重復(fù)實(shí)現(xiàn)灼舍。