浅谈C++物理设计:设计原则

536 查看

物理设计原则

原则 基本含义
自满足原则 头文件本身是可以编译通过的
单一职责原则 头文件包含的实体的职责是单一的
最小依赖原则 绝不包含不必要的头文件
最小可见性原则 尽量封装隐藏类的成员

自满足原则

所有头文件都应该自满足的。看一个具体的示例代码,这里定义了一个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

2018 - 知识虫 - 我的知识库 渝ICP备16002641号-2

渝公网安备 50010702501581号