1. 程式人生 > 實用技巧 >讀《C++ API設計》

讀《C++ API設計》

讀《C++ API設計》

API簡介

API是軟體組織的邏輯介面,隱藏了實現這個介面所需的內部細節。

+-----------------------------------------------------+
|                                                     |
|            Second Life Viewer                       | 應 用 程 序 代 碼
|                                                     |
+-----------------------------------------------------+

+-----------+ +-------------+ +-------------+
|           | |             | |             |
|  IICommon | | IIMessage   | | IIAudio     |   ...    內 部 API
|           | |             | |             |
+-----------+ +-------------+ +-------------+

+----------+ +-----+ +---------+ +---------+
|OpenGL    | | ARP | | Boost   | |OpenSSL  |           第 三 方 API
+----------+ +-----+ +---------+ +---------+

+-------------+ +--------------------------+
|標 準 C 庫    | |  標 準 模 板 庫          |           語 言 API
+-------------+ +--------------------------+

特徵

本章主要用來回答下面這個問題:優質的API應該具有哪些基本特徵。

getter,setter的優點:

  • 有效性驗證
  • 惰性求值
  • 快取
  • 額外的計算
  • 通知
  • 除錯
  • 同步
  • 更精細的訪問控制
  • 維護不變式關係

將私有功能宣告為.cpp檔案中的靜態函式,而不要將其作為私有方法暴露在公開的標頭檔案中。

疑惑之時,果斷棄之!精簡API中共有的類和函式。

避免將函式宣告為虛擬函式,除非有合理且迫切的需求。使用時,需要謹記一下幾點原則:

  • 如果類包含任一虛擬函式,那麼必須將解構函式宣告為虛擬函式。
  • 一定要編寫文件,說明類的方法是如何相互呼叫的。
  • 絕不在建構函式或解構函式中呼叫虛擬函式,這些呼叫不會指向子類。

基於最小化核心API,以獨立的模組或庫的形式構建便捷API。

避免編寫擁有多個相同型別引數的函式。

將資源的申請與釋放當做物件的構造和析構。

不要將平臺相關的#if或#ifdef語句放在公共的API中,因為這些語句暴露了實現細節,並使API因平臺而異。

優秀的API表現為鬆耦合高內聚。

模式

主要涉及的模式有:

  • Pimpl慣用法:支援在共有介面中完全隱藏內部細節。
  • 單例和工廠方法
  • 代理、介面卡和外觀:在現有的不相容介面或遺留介面上封裝API的各種途徑
  • 觀察者:該行為模式可以用來減少類之間的直接依賴。

Pimpl

Pimpl使用示例:

// with out pimpl
// autotimer.h
#ifdef _WIN32
#include <windows.h>
#else
#include <sys/time.h>
#endif
#include <string>

class AutoTimer
{
public:
    explicit AutoTimer(const std::string &name);
    ~AutoTimer();

private:
    double GetElapsed() const;

    std::string mName;
#ifdef _WIN32
    DWORD mStartTime;
#else
    struct timeeval mStartTime;
#endif
};

// with pimpl
// autotimer.h
#include <string>

class AutoTimer
{
public:
    explicit AutoTimer(const std::string &name);
    ~AutoTimer();
private:
    class Impl;
    Impl *mImpl;
};

// autotimer.cpp
#include "autotimer.h"

#include <iostream>
#if _WIN32
#include <windows.h>
#else
#include <sys/time.h>
#endif

class AutoTimer::Impl
{
public:
    double GetElapsed() const
    {
        ...
    }

    std::string mName;
#ifdef _WIN32
    DWORD mStartTime;
#else
    struct timeval mStartTime;
#endif
};

AutoTimer::AutoTimer(const std::string &name) :
    mImpl(new AutoTimer::Impl())
{
    mImpl->mName = name;
#ifdef _WIN32
    mImpl->mStartTime = GetTickCount();
#else
    gettimeofday(&mImpl->mStartTime,NULL);
#endif
}

AutoTimer::~AutoTimer()
{
    std::cout << mImpl->mName << ": took " << mImpl->GetElapsed() << " secs" << std::endl;
    delete mImpl;
    mImpl = NULL;
}

單例

單例是一種更加優雅地維護全域性狀態的方式,但始終應該考慮清楚是否需要全域性狀態。

依賴注入,實現:

/// 此處傳入的Database是一個單例,這樣,在該類的內部不用反覆呼叫GetInstance,
/// 同時,這樣的操作方式使得介面更加易於測試,因為物件的依賴項可以被
/// 替換為樁物件(stub)或模擬物件(mock),以便執行單元測試
class MyClass
{
public:
    MyClass(Database *db) : mDatabase(db) {}
private:
    Database *mDatabase;
};

初版《設計模式》的作者指出,他們計劃從原列表中移除的一個模式就是單例模式

工廠

讓工廠類維護一個對映,此對映將型別名和建立物件的回撥關聯起來。示例:

#include "renderer.h"
#include <string>
#include <map>

class RendererFactory
{
public:
    typedef IRenderer* (*CreateCallback)(); // 此處的CreateCallback可以是具體類的對應的Create函式
    static void RegisterRenderer(const std::string &type, CreateCallback cb);
    static void UnregisterRenderer(const std::string &type);
    static IRenderer *CreateRenderer(const std::string &type);
 
private:
    typedef std::map<std::string, CreateCallback> CallbackMap;
    static CallbackMap mRenderers;
}

代理

代理提供了一個介面,此介面將函式呼叫轉發到具有相同形式的另一個介面。

class Proxy
{
public:
    Proxy() : mOrig(new Original()) {}
    ~Proxy() { delete mOrig; }

    bool DoSomething(int value) { return mOrig->DoSomething(value); }
private:
    Proxy(const Proxy&);
    const Proxy &operator=(const Proxy&);
    Original *mOrig;
};

使用代理模式的一些案例:

  • 實現原始物件的惰性例項,當需要的時候才建立原始物件
  • 實現對原始物件的訪問控制
  • 支援除錯模式,實現不呼叫原始物件的方法,用來除錯介面
  • 支援資源共享,多個proxy物件,共享相同的原始基礎類。
  • 應對original類將來被修改

介面卡

將一個類的介面轉換為一個相容的但不相同的介面。

優點如下:

  • 強制API始終保持一致性。
  • 包裝API的依賴庫
  • 轉換資料型別
  • 為API暴露一個不同調用約定

外觀

能夠為一組類提供簡化的介面。在封裝外觀模式中,底層類不再可訪問。

常見用途:

  • 隱藏遺留程式碼
  • 建立便捷API
  • 支援簡化功能或者替代功能的API

觀察者

觀察者支援元件解耦且避免了迴圈依賴。

設計

+----------------------+         +------------------------+      +---------------+
|     Analyze          |         |       Design           |      |     Implement |
|                      |         |                        |      |               |
|    Requirement       |         |     Architecture       |      |     Coding    |
|                      +--------->                        +------>               |
|    User Case         |         |    Class Design        |      |     Testing   |
|                      |         |                        |      |               |
|    User's Story      |         |     Method Design      |      |     Document  |
|                      |         |                        |      |               |
|                      |         |                        |      |               |
+----------^-----------+         +------------^-----------+      +--------^------+
        |                                  |                           |
        |                                  |                           |
        |                                  |                           |
        +----------------------------------+---------------------------+

演進式實現一個不錯的選擇是,將醜陋的舊程式碼隱藏在精心設計的新的API之後,然後利用這些整潔的API逐步更新所有客戶端程式碼,並將程式碼自動化測試下。

建立API的架構過程可以分解為4個基本步驟:

  • 分析影響架構的功能性需求;
  • 識別架構的約束並加以說明;
  • 創造系統中的主要物件,並確認它們之間的關係;
  • 架構的交流與文件

架構約束可以細分為:

  • 組織因素:預算,時間表,團隊大小與專業知識,軟體開發過程,決定子系統是自己構建還是購買,管理焦點等;
  • 環境因素:硬體,平臺,軟體約束、客戶端/伺服器約束,協議約束,檔案格式約束,資料庫依賴,開發工具等
  • 執行因素:效能,記憶體利用率,可靠性,可用性,併發性,可定製型,可擴充套件性,指令碼功能,安全性,國際化,網路頻寬

識別主要抽象,openscenegraph api頂層架構:

                                +-----------+
                    視 圖         |           |
                    ^         | 節 點 工 具 包 |
遍 歷 器 +                |         |           |
    +---------->     |         |           |
                    +         |   仿 真     |
                場 景 圖 渲 染 <---+           |
數 據 庫 +---------->               |   地 形     |
^                              |           |
|                              |   動 畫     |
+                              |           |
插 件                             +-----------+

一些比較流行架構模式的一個分類:

  • 結構化模式:分層模式,管道與過濾器模式和黑板模式
  • 互動式系統:MVC,MVP,表示-抽象-控制模式
  • 分散式系統:客戶端/伺服器模式,三層架構,點對點模式以及代理模式。
  • 自適應系統:微核心模式與反射模式

迴圈依賴意味著無法對每個元件進行單獨測試,也不能在不牽扯元件的情況下複用另一個元件。基本上要理解任何一個元件都必須理解全部元件。

在API的附屬文件中要描述其高層架構並闡述其原理。

要集中精力設計定義了API80%功能的20%的類。

Liskov替換原則,在不修改任何行為的情況下用派生類替換基類,這應該總是可行的。

組合優先於繼承。

開閉原則:類的目標應該是為擴充套件而開放,為修改而關閉。它關注的焦點是建立可以長期使用的穩定性介面。

迪米特法則,一個函式可以做的事情只包括:

  • 呼叫同一個類的其它函式
  • 在同一個類的資料成員上呼叫函式
  • 在它接受的任何引數上呼叫函式
  • 在它建立的任何區域性物件上呼叫函式
  • 在全域性物件上呼叫函式

常見的互補的術語:

  • Add/Remove
  • Begin/End
  • Create/Destroy
  • Enable/Disable
  • Insert/Delete
  • Lock/Unlock
  • Next/Previous
  • Open/Close
  • Push/Pop
  • Send/Receive
  • Show/Hide
  • Source/Target

使用一致的、充分文件化的錯誤處理機制,返回錯誤碼,丟擲異常,中止程式。

在出現故障時,讓API快速乾淨地退出,並給出完整精確的診斷細節。

風格

本章會介紹四種風格迥異的API

  • 純C API:func(obj,a,b,c)
  • 面向物件的C++ API: obj.func(a,b,c)
  • 基於模板的API
  • 資料驅動型API:send("func",a,b,c); 這類介面特定是,將引數通過靈活的資料結構打包,連通命名的命令一起傳送給資料程式,而不是呼叫特定的方法或自由函式。

C++用法

如果類分配了資源,則應該遵循“三大件”規則,同時定義解構函式、複製建構函式和賦值操作符。

考慮在只帶有一個引數的建構函式的宣告前使用explicit關鍵字。

避免使用友元。它往往預示著糟糕的設計,這就等於賦予使用者訪問API所有受保護成員和私有成員的許可權。

使用內部連結以便隱藏.cpp檔案內部的、具有檔案作用域的自由函式和變數。也就是說,使用static關鍵字或匿名名稱空間。

應該顯示匯出共有API的符號,以便維持對動態庫中類、函式和變數訪問性的直接控制。對於GNU C++,可以使用__fvisibility_hidden選項。

效能

不要以扭曲API的設計為代價換取高效能。

為優化API,應使用工具收集程式碼在真實執行示例中的效能資料,然後把優化精力集中在實際的瓶頸上。不要猜測效能瓶頸的位置。

  • const引用
  • 前置宣告
  • 冗餘的include警戒語句
// head.h
#ifndef _HEAD_
#define _HEAD_
#endif

#ifndef _HEAD_
#include "head.h"
#endif
  • 應該使用extern宣告全域性作用域的常量,或者在類中以靜態const 方式宣告常量,然後再.cpp中定義常量
  • 初始化列表
  • Vector.h detail/Vector.h
  • 寫時複製
  • 時效分析
    • 內嵌測量,程式碼內嵌計時器
    • 二進位制測量
    • 取樣
    • 監控計數器
  • 基於記憶體的分析:IBM Rational Purify,Valgrind,Parasoft Insure++,Coverity
  • 多執行緒分析:Intel Thread Checker,Helgrind,DRD

版本控制 (TODO: Read Again)

主.次.補丁

只在必要時再分支,儘量延遲建立分支的時機。儘量使用分支程式碼線路而非凍結程式碼線路。儘早且頻繁的合併分支。

文件

複用做起來遠不如說起來那麼簡單,它同時需要良好的設計和優秀的文件。即使我們發現了難得一見的良好設計,如果沒有優秀的文件,這個元件就很難得以複用。

doxygen常用命令:

  • \file [<檔名>]
  • \class <類名>[<標頭檔案>][<標頭檔案名>]
  • \brief 簡要說明
  • \author
  • \date
  • \param
  • \param[in]
  • \param[out]
  • \param[in,out]
  • \return
  • \code \endcode
  • \verbatim <字面文字塊> \endverbatim
  • \exception 異常物件 描述
  • \deprecated 解釋及替代品
  • \attention 需要注意的訊息
  • \warning 警告訊息
  • \version
  • \bug
  • \see
  • \name 組名

測試

為了確保不破壞使用者程式,編寫自動化測試所能採取的措施中最重要的一項。

非功能測試:

  • 效能測試
  • 負載測試
  • 可擴充套件性測試
  • 浸泡測試:嘗試長期持續地執行軟體
  • 安全性測試
  • 併發測試

API測試應組合使用單元測試,和整合測試,也可以適當運用非功能性測試,如效能、併發、安全。

單元測試是一種白盒測試技術,用於獨立驗證函式和類的行為。

如果程式碼依賴於不可靠的資源,比如資料庫、檔案系統或網路,那麼可以使用樁物件或模擬物件建立個更健壯的單元測試。

google mock

使用SelfTest()成員函式測試類的私有成員。

使用斷言記錄和驗證那些絕不應該發生的程式設計錯誤。

#ifdef DEBUG
#include <assert.h>
#else
#define assert(func)
#endif

指令碼化 (TODO: read again)

可擴充套件性

Qt工具包可以通過QPluginLoader來擴充套件。

一般如果要建立外掛系統,有兩個主要特性是必須要設計的。

  • 外掛API:要建立外掛,使用者必須編譯並連線外掛API
  • 外掛管理器:核心API的一個物件,負責管理所有外掛的宣告週期,外掛的載入、註冊、解除安裝等各個階段。該物件也叫做外掛登錄檔。

為API設計外掛時的決策:

  • C還是C++:c可以跨平臺跨編譯器
  • 內部元資料還是外部元資料
  • 外掛管理器是通用還是專用
  • 安全性
  • 靜態庫還是動態庫

C++實現外掛

開源庫DynObj。

外掛API

外掛應該提供兩個最基本的回撥函式,初始化和清理函式。

// defines.h
#ifdef _WIN32
#ifdef BUILDING_CORE
#define CORE_API __declspec(dllexport)
#define PLUGIN_API __declspec(dllimport)
#else
#define CORE_API __declspec(dllimport)
#define PLUGIN_API __declspec(dllexport)
#endif
#else
#define CORE_API
#define PLUGIN_API
#endif

// renderer.h
class IRenderer
{
public:
    virtual ~IRenderer() {}
    virtual bool LoadScene(const char* filename) = 0;
    virtual void SetViewportSize(int w, int h) = 0;
    ...
};

// pluginapi.h
#include "defines.h"
#include "renderer.h"

#define CORE_FUNC extern "C" CORE_API
#define PLUGIN_FUNC extern "C" PLUGIN_API

#define PLUGIN_INIT() PLUGIN_FUNC int PluginInit()
#define PLUGIN_FREE() PLUGIN_FUNC int PluginFree()
typedef IRenderer *(*RendererInitFunc)();
typedef void (*RendererFreeFunc)(IRenderer*);

CORE_FUNC void RegisterRenderer(const char* type, RendererInitFunc init_cb, RendererFreeFunc free_cb);

外掛示例:

// plugin1.cpp
#include "pluginapi.h"
#include <iostream>

class OpenGLRenderer : public IRenderer
{
public:
    ~OpenGLRenderer() {}
    ...
};

PLUGIN_FUNC IRenderer *CreateRenderer() { return new OpenGLRenderer(); }
PLUGIN_FUNC void DestroyRenderer(IRenderer* r) { delete r; }
PLUGIN_INIT()
{
    RegisterRenderer("opengl", CreateRenderer, DestroyRenderer);
    return 0;
}

外掛管理器:

  • 載入所有外掛的元資料
  • 將動態庫載入到記憶體中,提供對庫中符號的訪問能力,並在必要時解除安裝
  • 初始化,清理
// pluginmanager.cpp
#include "defines.h"
#include <string>
#include <vector>

class CORE_API PluginInstance
{
public:
    explicit PluginInstance(const std::string& name);
    ~PluginInstance();
    bool Load();
    bool Unload();
    bool IsLoaded();
    std::string GetFileName();
    std::string GetDisplayName();
private:
    PluginInstance(const PluginInstance&);
    const PluginInstance &operator = (const PluginInstance&);
    class Impl;
    Impl *mImpl;
};

class CORE_API PluginManager
{
public:
    static PluginManager &GetInstance();
    bool LoadAll();
    bool Load(const std::string& name);
    bool UnloadAll();
    bool Unload(const std::string& name);
    std::vector<PluginInstance*> GetAllPlugins();
private:
    PluginManager();
    ~PluginManager();
    std::vector<PluginInstance*> mPlugins;
};

訪問者模式

訪問者模式的核心目標是,允許客戶遍歷一個數據結構中的所有物件,並在每個物件上執行給定的操作。

// 場景圖層次結構的例子

                     +----------------+
                     |                |
      +--------------+   Transform0   +--------+
      |              |                |        |
      |              +------+---------+        |
      |                     |                  |
      |                     |                  |
      |                     |                  |
      |                     |                  |
      |                     |                  |
+-----v----+      +---------v------+   +-------v--------+
|          |      |                |   |                |
|  Light0  |      |  Transform1    |   |  Transform2    |
|          |      |                |   |                |
+----------+      +-+------------+-+   +-----------+----+
                    |            |                 |
                    |            |                 |
                    |            |                 |
                    |            |                 |
                    |            |                 |
             +------v-----+   +--v-------+    +----v--------+
             |            |   |          |    |             |
             |  Shape0    |   |  Shape1  |    |   Shape2    |
             |            |   |          |    |             |
             +------------+   +----------+    +-------------+

// nodevisitor.h
class ShapeNode;
class TransformNode;
class LightNode;

class INodeVisitor
{
public:
    virtual ~INodeVisitor() {}
    virtual void Visit(ShapeNode &node) = 0;
    virtual void Visit(TransformNode &node) = 0;
    virtual void Visit(LightNode &node) = 0;
};

// scenegraph.h
#include <string>
class INodeVisitor;
class BaseNode
{
public:
    explicit BaseNode(const std::string &name);
    virtual ~BaseNode() {}
    virtual void Accept(INodeVisitor &visitor) = 0;
private:
    std::string mName;
};

class ShapeNode : public BaseNode {};
class TransformNode : public BaseNode {};
class LightNode : public BaseNode {};