淺談C++物理設計:設計原則

horance發表於2019-05-09

物理設計原則

原則 基本含義
自滿足原則 標頭檔案本身是可以編譯通過的
單一職責原則 標頭檔案包含的實體的職責是單一的
最小依賴原則 絕不包含不必要的標頭檔案
最小可見性原則 儘量封裝隱藏類的成員

自滿足原則

所有標頭檔案都應該自滿足的。看一個具體的示例程式碼,這裡定義了一個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

相關文章