C++ API設計筆記

《C++ API設計》原英文版由Martin Reddy著,中文版出版於2013年,這裏是中文版的筆記。

1. API簡介

1.1 什麼是API:API(Application Programming Interface)提供了對某個問題的抽象,以及客戶與解決該問題的軟件組件之間進行交互的方式。組件本身通常以軟件類庫形式分發,它們可以在多個應用程序中使用。概況地說,API定義了一些可複用的模塊,使得各個模塊化功能塊可以嵌入到最終用戶的應用程序中去。API是一個明確定義的接口,可以爲其它軟件提供特定服務。

在C++中,API一般包括一個或多個頭文件(.h)以及輔助文檔。某個特定API的具體實現通常是可以被鏈接到最終用戶程序中的庫文件,它也可以是靜態庫,又或者是動態庫。

C++ API通常會包含如下的元素:頭文件;類庫;文檔

API是軟件組件的邏輯接口,隱藏了實現這個接口所需的內部細節。

1.2 API設計上有什麼不同:接口是開發者所編寫的最重要的代碼。因爲比起相關的實現代碼出現問題,修復接口出現的問題代價要大得多。

API開發中的一些關鍵因素:

(1).API是爲開發者設計的接口。

(2).多個應用程序可以共享同一個API。

(3).修改API時,必須儘可能保證向後兼容。

(4).出於向後兼容的需求,一定要具有變更控制流程。

(5).API的生存週期一般都比較長。

(6).在編寫API時,良好的文檔必不可少,特別是當不提供實現的源代碼時。

(7).自動化測試同樣也很重要。

API描述了其他工程師構建他們的應用軟件所使用的軟件。因此,API必須擁有良好的設計、文檔、迴歸測試,並且保證發佈之間的穩定性。

1.3 爲什麼要使用API:

(1).更健壯的代碼:隱藏實現、延長壽命、促進模塊化、減少代碼重複、消除硬編碼假設、易於改變實現、易於優化。

(2).代碼複用:就是使用已有的軟件去構建新的軟件。API提供了一種代碼複用的機制。

(3).並行開發。

1.4 何時應當避免使用API:

(1).許可證限制。

(2).功能不匹配。

(3).缺少源代碼:不能訪問API的源代碼就喪失了通過修改源代碼修復錯誤的能力。

(4).缺乏文檔。

1.5 API示例:在現代軟件開發中API無處不在,從OS、語言層面的API,到圖像、聲音、圖形、併發、網絡、XML、數學、Web瀏覽器以及GUI API。

術語SDK(軟件開發工具包)和術語API是密切相關的。本質上講,SDK是安裝在計算機上的特定平臺的包,其目的是使用一個或多個API構建應用。SDK至少要包含編譯程序所需的頭文件(.h)以及提供API實現的庫文件(.dylib、.so、.dll),用以鏈接到應用程序之中。然而,SDK還可能包含其它幫助使用API的資源,如文檔、示例代碼以及支持工具。

1.6 文件格式和網絡協議:

在計算機應用中存在幾個其它形式的常用通信”協議”,其中最常見的一個就是文件格式。它是使用衆所周知的數據組織層次將內存中的數據存儲到磁盤文件上的方法。比如,JPEG文件交換格式(JFIF)是用來交換JPEG編碼圖像的圖像文件格式,通常使用.jpg或.jpeg文件擴展名。

客戶端/服務端應用、點對點應用以及中間件服務,使用建立好的且通常基於網絡套接字的協議發送和接收數據。

每當你創建一個文件格式或者客戶端/服務器協議時,同時也要爲其創建API。這樣,規範的細節以及未來的任何變更都將是集中且隱藏的。

2. 特徵

2.1 問題域建模:編寫API的目的是解決特定的問題或完成具體的任務。因此,API應該首先爲問題提供一個清晰的解決方案,同時能對實際的問題域進行準確的建模。

API應該對它所解決的問題提供邏輯抽象。也就是說,在設計API時,應該闡述在選定問題域內有意義的深層概念,而不是公開低層實現細節。當你把API文檔提供給一位非程序員時,他應當能夠理解接口中的概念並且知道它的工作機制。

API同樣需要對問題域的關鍵對象建模。該過程旨在描述特定問題域中對象的層次結構,因此經常被稱作”面向對象設計”或者”對象建模”。對象建模的目的是確定主要對象的集合,這些對象提供的操作以及對象之間的關係。

2.2 隱藏實現細節:創建API的主要原因是隱藏所有的實現細節,以免將來修改API對已有客戶造成影響。任何內部實現細節(那些很可能變更的部分)必須對該API的客戶隱藏。主要有兩種技巧可以達到此目標:物理隱藏和邏輯隱藏。物理隱藏表示只是不讓用戶獲得私有源代碼。邏輯隱藏則需要使用語言特性限制用戶訪問API的某些元素。

物理隱藏:在C和C++中,聲明和定義是有特定含義的精確術語。聲明只是告訴編譯器一個名字以及它的類型,並不爲其分配任何內存。與之相對,定義提供了類型結構體的細節,如果是變量則爲其分配內存。聲明告訴編譯器某個標識符的名稱及類型。定義提供該標識符的完整細節,即它是一個函數體還是一塊內存區域。物理隱藏表示將內部細節(.cpp)與公有接口(.h)分離,存儲在不同的文件中

邏輯隱藏:封裝提供了限制訪問對象成員的機制:public、protected、private。封裝是將API的公有接口與其底層實現分離的過程。邏輯隱藏指的是使用C++語言中受保護的和私有的訪問控制特性從而限制訪問內部細節。類的數據成員應該始終聲明爲私有的,而不是公有的或受保護的。

永遠不要返回私有數據成員的非const指針或引用,這會破壞封裝性。

強烈建議在API中採用Pimpl慣用法,這樣就可以將所有實現細節完全和公有頭文件分開。Pimpl慣用法:它將所有的私有數據成員隔離到一個.cpp文件中獨立實現的類或結構體中。之後,.h文件僅需要包含指向該類實例的不透明指針(opaque pointer)即可

將私有功能聲明爲.cpp文件中的靜態函數,而不要將其作爲私有方法暴露在公開的頭文件中。(更好的做法是使用Pimpl慣用法。)

隱藏實現類:除了隱藏類的內部方法和變量之外,還應該盡力隱藏那些純粹是實現細節的類。實際上,一些類僅用於實現,因此應該將其從API的公有接口中移除。

2.3最小完備性:

(1).優秀的API設計應該是最小完備的。即它應該儘量簡潔,但不要過分簡潔。

(2).謹記奧卡姆(Occam)剃刀原理:若無必要,勿增實體。

(3).疑惑之時,果斷棄之,精簡API中公有的類和函數。

謹慎添加虛函數:虛函數的調用必須在運行時查詢虛函數表決定,而非虛函數的調用在編譯時就能確定。使用虛函數一般需要維護指向虛函數表的指針,進而增加了對象的大小。不是所有的虛函數都能內聯,因而將虛函數聲明爲內聯是沒有任何意義的。因爲虛函數是運行時確定的,而內聯是在編譯時進行優化。謹記C++中的inline關鍵字僅僅是給編譯器的一個提示。如果類包含任一虛函數,那麼必須將析構函數聲明爲虛函數,這樣子類就可以釋放其可能申請的額外資源。絕不在構造函數或析構函數中調用虛函數,這些調用不會指向子類

避免將函數聲明爲可以重寫的函數(虛的),除非你有合理且迫切的需求。

2.4易用性:優秀的API設計應該使簡單的任務更簡單,使人一目瞭然。例如,好的API可以讓客戶僅通過方法簽名就能知曉使用方法,而不需要另寫文檔:

(1).使用枚舉類型代替布爾類型,提高代碼的可讀性。

(2).避免編寫擁有多個相同類型參數的函數。

(3).使用一致的函數命名和參數順序。

(4).正交的API意味着函數沒有副作用。設計正交API時需要銘記兩個重要因素:減少冗餘:確保只有一種方式表示相同的信息;增加獨立性:確保暴露的概念沒有重疊。

(5).當需要客戶銷燬指針時,使用智能指針返回動態申請的對象。

(6).將資源的申請與釋放當作對象的構造和析構。

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

2.5 鬆耦合:耦合:軟件組件之間相互連接的強度的度量,即系統中每個組件對其它組件的依賴程度。內聚:單個軟件組件內的各種方法相互關聯或聚合強度的度量。

(1).優秀的API表現爲鬆耦合和高內聚。

(2).除非確實需要#include類的完整定義,否則應該爲類使用前置聲明。如果類A僅需要知道類B的名字,即它不需要知道類B的大小或調用類B的任何方法,那麼類A就不需要依賴類B的完整聲明。在這種情況下,可以爲類B使用前置聲明,而非包含整個接口,這樣就降低了這兩個類之間的耦合。

(3).與成員函數相比,使用非成員、非有元的方法能降低耦合度。如果情況允許,那麼優先聲明非成員、非友元的函數,而非成員函數,這麼做在促進封裝的同時還降低了這些函數和類的耦合度。

(4).有時,使用數據冗餘降低類之間的耦合是合理的。儘管如此,即使是有意的冗餘,也是冗餘,應該小心謹慎地使用,同時添加良好的註釋。

(5).管理器類可以通過封裝幾個低層次類降低耦合。管理器類擁有並協調幾個低層次的類。可以用它打破基於一組低層次的類的一個或多個類的依賴關係。

(6).回調:在C和C++中,回調是模塊A中的一個函數指針,該指針被傳遞給模塊B,這樣B就能在合適的時候調用A中的函數。模塊B對模塊A一無所知,並且對模塊A不存在”包含”(include)或者”鏈接”(link)依賴。回調的這種特性使得低層代碼能夠執行與其不能有依賴關係的高層代碼。因此,在大型項目中,回調是一種用於打破循環依賴的常用技術。

3. 模式

設計模式是針對軟件設計問題的通用解決方案。該術語源於《設計模式:可複用面向對象軟件的基礎》一書。書中介紹了下列通用的設計模式,它們可以分成三大類

(1).創建型模式:

抽象工廠模式:創建一組相關的工廠。

建造者模式:將複雜對象的構建與表示分離。

工廠方法模式:將類的實例化推遲到子類中。

原型模式:指定類的原型實例,克隆該實例可以生成新的對象。

單例模式:確保類只有一個實例。

(2).結構型模式:

適配器模式:將類的接口轉換爲另一種接口。

橋接模式:將抽象部分與它的實現部分分離,使它們都可以獨立地變化。

組合模式:將對象組合成樹型結構,表示”部分----整體”的層次結構。

裝飾模式:動態地給一個對象添加一些額外的行爲。

外觀模式:爲子系統中的一組接口提供統一的高層次接口。

享元模式:利用共享技術高效地支持大量細粒度的對象。

代理模式:提供另一個對象的替代物或佔位符,以便控制對該對象的訪問。

(3).行爲模式:

職責鏈模式:使多個接收者對象有機會處理來自發送者對象的請求。

命令模式:將請求或操作封裝成對象,並支持可撤銷的操作。

解釋器模式:指定如何對某種語言的語句進行表示和判斷。

迭代器模式:提供一種順序訪問某種聚合對象元素的途徑。

中介者模式:定義一箇中介對象,用於封裝一組對象的交互。

備忘錄模式:捕獲對象的內部狀態,以便將來可將該對象恢復到保存時的狀態。

觀察者模式:定義對象間一種一對多的依賴關係,當對象的狀態發生改變時,所有依賴於它的對象都將得到通知。

狀態模式:當對象內部狀態改變時,對象看起來好像修改了它所屬的類。

策略模式:定義一組算法並封裝每個算法,使它們在運行時可以相互替換。

模板方法模式:定義某操作中算法的框架,將其中一些步驟推遲到子類中。

訪問者模式:表述對某對象結構的元素所執行的操作。

設計模式更多介紹參考:https://blog.csdn.net/fengbingchun/category_2134223.html

測試代碼cplusplus_api_design.hpp內容如下:

// Pimpl慣用法:"自動定時器",當被銷燬時打印出其生存時間
class AutoTimer {
public:
	explicit AutoTimer(const std::string& name);
	~AutoTimer();

	AutoTimer(const AutoTimer&) = delete;
	AutoTimer& operator=(const AutoTimer&) = delete;

private:
	class Impl; // 私有內嵌類
	std::unique_ptr<Impl> impl_;
};

// 單例模式
class Singleton {
public:
	static Singleton& GetInstance() // 既可以返回單例類的指針也可以返回引用,當返回指針時,客戶可以刪除該對象,因此最好返回引用
	{
		static Singleton instance;
		return instance;
	}

	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;

private:
	Singleton() { fprintf(stdout, "constructor\n"); }
	~Singleton() { fprintf(stdout, "destructor\n"); }
};

// 單一狀態設計模式
class Monostate {
public:
	int GetTheAnswer() const { return answer_; }

private:
	static int answer_; // 也可以將該靜態變量聲明爲.cpp文件作用域的靜態變量,而不是私有的類靜態變量
};

// 工廠模式
class IRenderer {
public:
	virtual ~IRenderer() {}
	virtual void Render() = 0;
};

class RendererFactory {
public:
	IRenderer* CreateRenderer(const std::string& type);
};

// 擴展工廠模式:將派生類與工廠方法解耦並支持在運行時添加新的派生類:工廠類維護一個映射,此映射將類型名與創建對象的回調關聯起來。
// 然後就可以允許新的派生類通過一對新的方法調用來實現註冊和註銷。
// 需要注意的問題是,工廠對象必須保持其狀態信息。因此,最好強制要求任一時刻都只能創建一個工廠對象,這也是爲何多數工廠對象也是單例的原因
class RendererFactory2 {
public:
	typedef IRenderer* (*CreateCallback)();
	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> callback_map_;
	static callback_map_ renderers_;
};

// 代理模式:尤其適用於Original類是第三方類庫
class IOriginal {
public:
	virtual void DoSomething(int value) = 0;
};

class Original : public IOriginal {
public:
	void DoSomething(int value) override { fprintf(stdout, "Original::DoSomething\n"); }
};

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

	void DoSomething(int value) override { return orig_->DoSomething(value); }

	Proxy(const Proxy&) = delete;
	Proxy& operator=(const Proxy&) = delete;

private:
	Original* orig_;
};

// 適配器模式
class Rectangle {
public:
	Rectangle() = default;
	~Rectangle() {}

	void setDimensions(float cx, float cy, float w, float h) { fprintf(stdout, "width: %f, height: %f\n", w, h); }
};

class RectangleAdapter {
public:
	RectangleAdapter() : rect_(new Rectangle()) {}
	~RectangleAdapter() { delete rect_; }

	void Set(float x1, float y1, float x2, float y2)
	{
		float w = x2 - x1;
		float h = y2 - y1;
		float cx = w / 2.f + x1;
		float cy = h / 2.f + y1;
		rect_->setDimensions(cx, cy, w, h);
	}

	RectangleAdapter(const RectangleAdapter&) = delete;
	RectangleAdapter& operator=(const RectangleAdapter&) = delete;

private:
	Rectangle* rect_;
};

// 外觀模式
class Subsystem1 {
public:
	Subsystem1() = default;
	~Subsystem1() {}
	void Operation() { fprintf(stdout, "subsystem1 operation\n"); }
};

class Subsystem2 {
public:
	Subsystem2() = default;
	~Subsystem2() {}
	void Operation() { fprintf(stdout, "subsystem2 operation\n"); }
};

class Facade {
public:
	Facade() : subs1_(new Subsystem1()), subs2_(new Subsystem2()) {}
	~Facade()
	{
		delete subs1_;
		delete subs2_;
	}

	void OperationWrapper()
	{
		subs1_->Operation();
		subs2_->Operation();
	}

	Facade(const Facade&) = delete;
	Facade& operator=(const Facade&) = delete;

private:
	Subsystem1* subs1_;
	Subsystem2* subs2_;
};

// 觀察者模式
class SubjectBase; // 抽象主題
class ObserverBase { // 抽象觀察者
public:
	ObserverBase(std::string name, SubjectBase* sub) : name_(name), sub_(sub) {}
	virtual void Update() = 0;

protected:
	std::string name_;
	SubjectBase* sub_;
};

class StockObserver: public ObserverBase { // 具體觀察者,看股票的
public:
	StockObserver(std::string name, SubjectBase* sub) : ObserverBase(name, sub) {}
	void Update() override;
};

class NBAObserver : public ObserverBase { // 具體觀察者,看NBA的
public:
	NBAObserver(std::string name, SubjectBase* sub) : ObserverBase(name, sub) {}
	void Update() override;
};

class SubjectBase { // 抽象主題
public:
	virtual void Attach(ObserverBase* observer) = 0;
	virtual void Notify() = 0;

public:
	std::string action_;
	std::vector<ObserverBase*> observers_;
};


class SecretarySubject : public SubjectBase { // 具體主題
public:
	void Attach(ObserverBase* ob) { observers_.push_back(ob); }

	void Notify()
	{
		for (auto it = observers_.cbegin(); it != observers_.cend(); ++it) {
			(*it)->Update();
		}
	}
};

int test_api_design_3();
int test_api_design_3_pimpl();
int test_api_design_3_singleton();
int test_api_design_3_monostate();
int test_api_design_3_factory();
int test_api_design_3_factory_expand();
int test_api_design_3_proxy();
int test_api_design_3_adapter();
int test_api_design_3_facade();
int test_api_design_3_observer();

測試代碼cplusplus_api_design.cpp內容如下:

class AutoTimer::Impl {
public:
	double GetElapsed() const
	{
#ifdef _WIN32
		return (GetTickCount() - start_time_) / 1e3;
#else
		struct timeval end_time;
		gettimeofday(&end_time, nullptr);
		double t1 = start_time_.tv_usec / 1e6 + start_time_.tv_sec;
		double t2 = end_time.tv_usec / 1e6 + end_time.tv_sec;
		return t2 - t1;
#endif
	}

	std::string name_;
#ifdef _WIN32
	DWORD start_time_;
#else
	struct timeval start_time_;
#endif
};

AutoTimer::AutoTimer(const std::string& name) : impl_(std::make_unique<Impl>())
{
	impl_->name_ = name;
#ifdef _WIN32
	impl_->start_time_ = GetTickCount();
#else
	gettimeofday(&impl_->start_time_, nullptr);
#endif
}

AutoTimer::~AutoTimer()
{
	fprintf(stdout, "%s : took %f secs\n", impl_->name_.c_str(), impl_->GetElapsed());
}

int test_api_design_3_pimpl()
{
	AutoTimer auto_timer("Take");

	return 0;
}

int test_api_design_3_singleton()
{
	Singleton& singleton1 = Singleton::GetInstance();
	Singleton& singleton2 = Singleton::GetInstance();

	return 0;
}

int Monostate::answer_ = 42;
int test_api_design_3_monostate()
{
	Monostate m1, m2;
	fprintf(stdout, "answer1: %d, answer2: %d\n", m1.GetTheAnswer(), m2.GetTheAnswer());

	return 0;
}

class OpenGLRenderer : public IRenderer {
public:
	OpenGLRenderer() { fprintf(stdout, "constructor OpenGLRenderer\n"); }
	void Render() override { fprintf(stdout, "OpenGLRenderer::Render\n"); }
	~OpenGLRenderer() { fprintf(stdout, "destructor OpenGLRenderer\n"); }
};

class DirectXRenderer : public IRenderer {
public:
	DirectXRenderer() { fprintf(stdout, "constructor DirectXRenderer\n"); }
	void Render() override { fprintf(stdout, "DirectXRenderer::Render\n"); }
	~DirectXRenderer() { fprintf(stdout, "destructor DirectXRenderer\n"); }
};

IRenderer* RendererFactory::CreateRenderer(const std::string& type)
{
	if (type == "opengl")
		return new OpenGLRenderer();
	if (type == "directx")
		return new DirectXRenderer();

	return nullptr;
}

int test_api_design_3_factory()
{
	RendererFactory factory;

	IRenderer* renderer1 = factory.CreateRenderer("opengl");
	IRenderer* renderer2 = factory.CreateRenderer("directx");

	if (renderer1) {
		renderer1->Render();
		delete renderer1;
	}

	if (renderer2) {
		renderer2->Render();
		delete renderer2;
	}

	return 0;
}

RendererFactory2::callback_map_ RendererFactory2::renderers_;

void RendererFactory2::RegisterRenderer(const std::string& type, CreateCallback cb)
{
	renderers_[type] = cb;
}

void RendererFactory2::UnRegisterRenderer(const std::string& type)
{
	renderers_.erase(type);
}

IRenderer* RendererFactory2::CreateRenderer(const std::string& type)
{
	callback_map_::iterator it = renderers_.find(type);
	if (it != renderers_.end()) {
		// 調用回調以構造此派生類的對象
		return (it->second)();
	}

	return nullptr;
}

// API用戶現在可以在系統中註冊(以及註銷)新的渲染器
class UserRenderer : public IRenderer {
public:
	UserRenderer() { fprintf(stdout, "constructor UserRenderer\n"); }
	void Render() override { fprintf(stdout, "UserRenderer::Render\n"); }
	~UserRenderer() { fprintf(stdout, "destructor UserRenderer\n"); }
	static IRenderer* Create() { return new UserRenderer(); }
};

int test_api_design_3_factory_expand()
{
	// 註冊一個新的渲染器
	RendererFactory2::RegisterRenderer("user", UserRenderer::Create);
	// 爲新的渲染器創建一個實例
	IRenderer* r = RendererFactory2::CreateRenderer("user");
	if (r) {
		r->Render();
		delete r;
	}

	return 0;
}

int test_api_design_3_proxy()
{
	Proxy proxy;
	proxy.DoSomething(3);

	return 0;
}

int test_api_design_3_adapter()
{
	RectangleAdapter rect;
	rect.Set(10.f, 5.f, 20.f, 25.f);

	return 0;
}

int test_api_design_3_facade()
{
	Facade facade;
	facade.OperationWrapper();

	return 0;
}

void StockObserver::Update()
{
	fprintf(stdout, "%s: %s, can't play stock\n", name_.c_str(), sub_->action_.c_str());
}

void NBAObserver::Update()
{
	fprintf(stdout, "%s: %s, can't watch NBA\n", name_.c_str(), sub_->action_.c_str());
}

int test_api_design_3_observer()
{
	SubjectBase* subject = new SecretarySubject();

	ObserverBase* observer1 = new NBAObserver("Jack", subject);
	ObserverBase* observer2 = new StockObserver("Tom", subject);

	subject->Attach(observer1);
	subject->Attach(observer2);

	subject->action_ = "boss comes";
	subject->Notify();

	delete subject;
	delete observer1;
	delete observer2;

	return 0;
}


int test_api_design_3()
{
	//return test_api_design_3_pimpl();
	//return test_api_design_3_singleton();
	//return test_api_design_3_monostate();
	//return test_api_design_3_factory();
	//return test_api_design_3_factory_expand();
	//return test_api_design_3_proxy();
	//return test_api_design_3_adapter();
	//return test_api_design_3_facade();
	return test_api_design_3_observer();
}

3.1 Pimpl慣用法:”pointer to implementation”(指向實現的指針)的縮寫。該技巧可以避免在頭文件中暴露私有細節。因此它是促進API接口和實現保持完全分離的重要機制。但是Pimpl並不是嚴格意義上的設計模式(它是受制於C++特定限制的變通方案),這種慣用法可以看作是橋接設計模式的一種特例。

使用Pimpl慣用法將實現細節從公有頭文件中分離出來。Pimpl利用了C++的一個特點,即可以將類的數據成員定義爲指向某個已經聲明過的類型的指針。這裏的類型僅僅作爲名字引入,並沒有被完整地定義,因此我們就可以將該類型的定義隱藏在.cpp文件中。這通常稱爲不透明指針,因爲用戶無法看到它所指向的對象細節。本質上,Pimpl是一種同時在邏輯上和物理上隱藏私有數據成員與函數的辦法。Pimpl慣用法將所有私有成員放置在一個類(或結構體)中,這個類在頭文件中前置聲明,在.cpp文件中定義

使用Pimpl慣用法時,應採用私有內嵌實現類。只有在.cpp文件中其它類或自由函數必須訪問Impl成員時,才應採用公有內嵌類(或公有非內嵌類)

如果沒有爲類顯式定義複製構造函數和賦值操作符,C++編譯器會默認創建。但是這種默認的構造函數只能指向對象的淺複製。這不利於採用Pimpl的類,因爲這意味着如果客戶複製了對象,則這兩個對象將指向同一個Impl實現對象。這樣一來,兩個對象可能在析構函數中嘗試刪除同一個Impl對象,這很可能導致崩潰。處理這個問題有以下兩種可選方案:禁止複製類或顯式定義複製語義。

注意Pimpl類的複製語義,可以使用智能指針管理指向Impl對象的指針的初始化和銷燬。

Pimpl慣用法的主要缺點是,必須爲你創建的每個對象分配並釋放實現對象。這使對象增加了一個指針,同時因爲必須通過指針間接訪問所有成員變量,這種額外的調用層次與新增的new和delete開銷類似,可能引入性能衝擊。如果關注內存分配器的性能,那麼可以考慮使用”快速Pimpl”(Fast Pimpl)慣用法,該方法爲Impl類重載了new和delete操作符,以便使用更加高效的小內存定長分配器。

3.2 單例:單例設計模式用來確保一個類僅存在一個實例。該模式亦提供對此唯一實例的全局訪問點。可以認爲單例是一種更加優雅的全局變量。單例模式要求創建一個類,它包含一個靜態方法,每次調用該方法時返回該類的同一個實例。

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

將構造函數、析構函數、複製構造函數以及賦值操作符聲明爲私有(或受保護的),可以實現單例模式。

不同編譯單元中的非局部靜態對象的初始化順序是未定義的。因此,初始化單例的途徑之一是在類的方法中創建靜態變量。但是該方式不是線程安全的。

使用C++創建線程安全的單例是困難的。可以考慮使用靜態構造函數或API初始化函數對其進行初始化。

依賴注入是一種將對象傳入到某個類中(注入),而不是讓這個類自己創建並存儲該對象的技巧。依賴注入使採用了單例的代碼更易於測試。

單一狀態(Monostate)設計模式允許創建類的多個實例,這些實例使用相同的靜態數據。如果不需要惰性初始化全局數據,或者想讓類的唯一性透明化,可以考慮使用單一狀態代替單例。單例僅允許創建一個實例,因此強制了唯一性的結構,而單一狀態使得所有實例共享同一份數據,因此強制了唯一性的行爲。

單例模式有一些替代方案,包括依賴注入、單一狀態模式以及使用會話上下文。

3.3 工廠模式:它是一個創建型的設計模式。它允許創造對象時不指定要創建的對象的具體類型。本質上,工廠方法是構造函數的一般化。從基本層面來看,工廠方法僅是一個普通的方法調用,該調用返回類的實例。但是,它們經常和繼承一起使用,即派生類能夠重寫工廠方法並返回派生類的實例。常見的做法是使用抽象基類實現工廠模式

在C++中,構造函數沒有運行時動態綁定的概念,不能聲明虛構造函數,沒有返回值,構造函數的名字與它所在的類的名字相同。

使用工廠方法提供更強大的類構造語義並隱藏子類的細節。

3.4 API包裝器模式:潛在副作用是影響性能,這主要因爲額外增加的一級間接尋址以及存儲包裝層次狀態帶來的開銷。結構化設計模式可以處理接口包裝任務。

(1).代理模式:代理設計模式爲另一個類提供了一對一的轉發接口。代理類和原始類有相同的接口。它可以被認爲是一個單一組件包裝器。此模式通常的實現是,代理類中存儲原始類的副本,但更可能是指向原始類的指針,然後代理類中的方法將重定向到原始類對象中的同名方法。代理提供了一個接口,此接口將函數調用轉發到具有同樣形式的另一個接口。

(2).適配器模式:將一個類的接口轉換爲一個兼容的但不相同的接口。與代理模式的相似之處是,適配器設計模式也是一個單一組件包裝器,但適配器類和原始類的接口可以不相同

適配器將一個接口轉換爲一個兼容的但不相同的接口。

適配器可以用”組合”或者”繼承”來實現。這兩種類型分別稱爲對象適配器和類適配器。

(3).外觀模式:能夠爲一組類提供簡化的接口。它實際上定義了一個更高層次的接口,以使得底層子系統更易於使用。外觀模式和適配器模式的區別是,外觀模式簡化了類的結構,而適配器模式仍然保持相同的類結構

外觀模式爲一組類提供了簡化的接口。在封裝外觀模式中,底層類不再可訪問。

3.5 觀察者模式:支持組件解耦且避免了循環依賴

模型--視圖--控制器(Model-View-Controller, MVC)架構:此模式要求業務邏輯(Model)獨立於用戶界面(View),控制器(Controller)接收用戶輸入並協調另外兩者。MVC架構模式促使核心業務邏輯(或者說模型)與用戶界面(或者說視圖)分離。它還隔離了控制器邏輯,控制器邏輯會引起模型的改變並更新視圖。

觀察者模式是”發佈/訂閱”範式(Publish/Subscribe,簡寫做pub/sub)的一個具體實例。這些技術定義了對象之間一對多的依賴,使得發佈者對象能夠將它的狀態變化通知給所有的訂閱對象,而又不直接依賴於訂閱者對象。實現觀察者模式的典型做法是引入兩個概念:主題(Subject)和觀察者(Observer),也稱作發佈者和訂閱者。一個或多個觀察者註冊主題中感興趣的事件,之後主題會在自身狀態發生變化時通知所有註冊的觀察者。

4. 設計

4.1 良好設計的例子:

4.2 收集功能性需求:軟件產業中的需求可以分爲不同的類型,包括如下幾種:(1).業務需求:即軟件如何滿足組織的需求。(2).功能性需求:即軟件應該完成什麼功能。(3).非功能性需求:描述軟件必須達到的質量標準。

功能性需求規定了API如何表現。

功能性需求一般用需求文檔來管理,其中每個需求都有一個唯一的標識符和一個描述信息。好的功能性需求應該是簡單、易讀、清晰、可驗證的,而且沒有開發術語。

4.3 創建用例:用例從用戶的角度描述API的需求。

用例可以是面向目標的簡短描述信息的簡要列表,也可以是更爲正式的模板定義的結構化說明。

用戶故事是敏捷開發過程中一種從用戶那獲得最小需求的方法。用戶故事是一個高層的需求概念,它僅包含了足夠的信息,開發者可以利用這些信息對實現用戶故事所需要付出的努力給出一個合理的評估。

4.4 API設計的元素:產生好的API設計的祕訣是:對問題領域進行合理抽象,然後設計相應的對象與類的層次結構來表達該抽象。抽象是關於一些事務的簡單描述,不需要對編程實現有任何瞭解即可理解。

API設計包括開發頂層架構和詳細的類層次結構。

4.5 架構設計:軟件架構描述了一個完整系統的很粗糙的結構:API中頂層對象的集合以及它們彼此之間的關係。

架構設計被衆多獨特的組織、環境和運行等因素約束。有些約束是可以協商的。總是爲變化而設計。變化是不可避免的。

API的關鍵對象並不容易識別,請嘗試從不同的角度看待問題,並不斷迭代和完善你的模型。

要避免API各個組件間的循環依賴。

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

4.6 類的設計:要集中精力設計定義了API 80%功能的20%的類。

避免深度繼承層次結構。

除非使用接口和mixin類,否則要避免多重繼承。組合優先於繼承。

Liskov替換原則(Liskov Substitution Principle, LSP)指出,在不修改任何行爲的情況下用派生類替換基類,這應該總是可行的。

開放--封閉原則(Open/Closed Principle, OCP):API應該爲不兼容的接口變化而關閉,爲功能擴展而開放。

迪米特法則(Law of Demeter, LoD)指出,你應該只調用自己類或直接相關對象的函數。

4.7 函數設計:避免過長的參數列表。對於有很多可選參數的函數,你可以考慮通過傳遞結構體(struct)或映射(map)來代替。

API中處理錯誤條件的三種主要方式是:返回錯誤碼;拋出異常;中止程序。使用一致的、充分文檔化的錯誤處理機制。

如果你選擇在代碼中使用異常來通知意外情況,從std::exception派生自己的異常,並定義what()方法來描述失效信息。

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

5. 風格

5.1 純C API:

爲實現更嚴格的類型檢查,並確保C++程序可以使用你的API,請嘗試用C++編譯器來編譯C API。相對於C編譯器而言,C++編譯器的類型檢查更爲嚴格

C函數的鏈接方式與C++函數不同。也就是說,同樣的函數,如果由C和C++編譯器分別生成目標文件,該函數的表示是不同的。將C API包裝在一個extern “C”構造塊中,這會告訴C++編譯器,構造塊中的函數應該使用C風格的鏈接方式。C編譯器不能解析該語句,所以最好使用條件編譯,使之僅支持C++編譯器。

在C API的頭文件中使用extern “C”限制,以便於C++程序能正確地編譯和鏈接C API

5.2 面向對象的C++ API:面向對象API允許使用對象而非動作來建模軟件,同時也帶來了繼承和封裝等優點。

5.3 基於模板的API:模板是C++的一個特性,它支持使用泛化的、尚未具體指定的類型編寫函數或類。然後,你可以用具體類型實例化這些泛化的類型,以此實現模板的特化。模塊可以在編譯時執行一些工作,進而改進運行時性能。

5.4 數據驅動型API:數據驅動型程序指的是:通過每次運行時提供不同的輸入數據,它可以指向不同的操作。程序的業務邏輯能夠抽象到可以由人來編輯的數據文件中去。利用這種方法,可以在不重新編譯可執行文件的情況下改變程序的行爲。

數據驅動型API可以很好地映射到Web服務和其他客戶端/服務器形式的API,它們也支持數據驅動型測試技術。

6. C++用法

6.1 命名空間:是若干唯一符號的邏輯分組。它提供了一種避免命名衝突的方法,以防兩個API使用相同的名字定義符號。任何時候都不應該在公用API頭文件的全局作用域內使用using關鍵字。

應當始終使用一致的前綴或C++的namespace關鍵字爲API符號提供命名空間。

6.2 構造函數和賦值:如果類分配了資源,則應該遵循”三大件”(Big Tree)規則,同時定義析構函數、複製構造函數和賦值操作符。

考慮在只帶有一個參數的構造函數的聲明前使用explicit關鍵字,用於阻止構造對象時特定的構造函數被隱式調用。

6.3 const正確性:是指使用C++的const關鍵字將變量或者方法聲明爲不可變的。

儘可能早地將函數和參數聲明爲const。過後修正API中的const正確性會既耗時又麻煩。

當向const函數傳入引用或指針時,也要考慮該參數是否可以聲明爲const。

返回函數結果時,將結果聲明爲const的主要理由是其引用了對象的內部狀態。首選傳值方式而不是const引用方式返回函數的結果。

6.4 模板:在編譯時生成代碼,它們對生成大量形式相似但只類型不同的代碼尤其有用。

如果只需要一些確定的特化集合,那麼儘量選擇顯式模板實例化。這樣做可以隱藏私有細節並降低構建時間。

6.5 操作符重載:應當只在有意義的地方使用操作符重載,即只在那些API用戶看起來很自然、不會違反最小意外原理(rule of the least surprise)的地方使用。這指的是應該保持操作符的自然語義。

除非操作符必須訪問私有成員或受保護的成員,或它是=、[]、->、->*、()、(T)、new、delete其中之一,否則應該儘量將其聲明爲自由函數。

轉換操作符用於定義將對象自動轉換成不同類型的對象。轉換操作符沒有指定返回值類型,這是因爲編譯器可以根據操作符名字推斷出類型,而且轉換操作符沒有參數。給類添加轉換操作符,從而利用自動類型強制轉換。

6.6 函數參數:儘量在可行的地方爲輸入參數使用const引用,而非指針。對於輸出參數,考慮使用指針而不是非const引用,以便顯式地向客戶表面它們可能被修改。

當默認值會暴露實現中的常量時,儘量選擇函數重載,而不是默認參數。儘量避免定義涉及構造臨時對象的默認參數,因爲這些參數會以傳值的方式傳遞給方法,開銷很大。

6.7 避免使用#define定義常量:#define預處理指令本質上是用一個字符串替換源碼中的另一個字符串。

使用靜態const數據成員而非#define表示類常量。

6.8 避免使用友元:在C++中,友元是一個類向另外一個類或函數授予其完全訪問特權的方法。友元類或函數可以訪問類中所有受保護成員和私有成員。

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

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

使用內部鏈接以便隱藏.cpp文件內部的、具有文件作用域的自由函數和變量。也就是說,使用static關鍵字或匿名命名空間。

6.10 編碼規範:爲API指定編碼風格標準,這有助於保證一致性、明確流程,並總結常見的工程陷阱。

7. 性能

不要以扭曲API的設計爲代價換取高性能。關於API的性能有幾個主題:編譯時速度、運行時速度、運行時內存開銷、庫的大小、啓動時間。

如果沒有必要使用dynamic_cast操作符,那麼常見的做法是關閉運行時類型信息(RTTI)。GNU C++編譯器的選項是-fno-rtti。

爲優化API,應使用工具收集代碼在真實運行實例中的性能數據,然後把優化精力集中在實際的瓶頸上。不要猜測性能瓶頸的位置。

7.1 通過const引用傳遞輸入參數:應該通過const引用而非傳值方式傳遞不會改變的對象。這樣可以避免創建和銷燬對象的臨時副本,及副本中所有的成員和繼承對象的內存與性能開銷。這條規則僅適用於對象,對於諸如int、bool、float、double及char等內建類型不適用,因爲它們以及很小了,能夠放進CPU寄存器。此外,STL迭代器和函數對象是採用傳值方式的,也不適用這條規則。對於用戶自定義類型,應該儘量使用引用或const引用。

7.2 最小化#include依賴:編譯一個大工程需要花費的時間很大程度上取決於#include文件的數量和深度。如此而論,縮短構建時間的一種常見技巧是嘗試減少頭文件中#include語句的數量。

前置聲明可以在下列幾種情況下使用:(1).不需要知道類的大小;(2).沒有引用類的任何成員方法;(3).沒有引用類的任何成員變量。

一般來說,只有在自己的類中將某個類的對象作爲數據成員使用時,或者需要繼承某個類時,才應該包含那個類的頭文件。

僅僅前置聲明你自己API中的符號。使用前置聲明,意味着你非常清楚符號在被省略的頭文件中是怎樣聲明的。

不管使用前置聲明還是#iniclude方式,頭文件應該#include或者前置聲明其所有的依賴項。

考慮在頭文件中加入冗餘的#include警戒語句,爲客戶優化編譯時間:

#ifndef XXXX_H
#include “xxxx.h”
#endif

7.3 聲明常量:應使用extern聲明全局作用域的常量,或者在類中以靜態const方式聲明常量,然後在.cpp文件中定義常量值。這樣就減少了包含這些頭文件的模塊的目標文件大小。更可取的方法是將這些常量隱藏在函數調用背後。

constexpr關鍵字可以用來標識已知爲恆定不變的函數或變量,以便編譯器執行更好的優化。

7.4 初始化列表:使用構造函數初始化列表,從而爲每個數據成員減少一次調用構造函數的開銷。這些應在.cpp文件中聲明,以便隱藏實現細節。成員變量在構造函數的函數體調用之前構造。

7.5 內存優化:

(1).根據成員變量類型對它們加以聚集,從而優化對象大小。

(2).考慮使用位域進一步壓縮對象,但是要注意它對性能的影響。

(3).使用大小明確的類型(比如int32_t和uint16_t)指定變量所需的最大位數。

7.6 除非需要,勿用內聯:避免在公有頭文件中使用內聯代碼,除非能證明代碼會導致性能問題,並確認內聯可以解決該問題。

7.7 寫時複製:節省內存最好的辦法之一是到確實需要時再分配。這是寫時複製技巧的本質目標。其方法是允許所有客戶共享一份唯一的資源,直到他們中的一個需要修改這份資源爲止。只有在那個時間點纔會構造副本----這是”寫時複製”名字的來由。使用寫時複製語義,爲對象的多份副本減少內存消耗。

7.8 迭代元素:STL解決這個問題的方法是使用迭代器。迭代器可以對容器中的部分或全部元素進行遍歷。採用迭代器模型遍歷簡單的線性數據結構。對於鏈表或樹型數據結構,如果迭代器性能很重要,那麼應該考慮使用數組引用。

7.9 性能分析:工具:Gprof、OProfile、Open SpeedShop、CodeProphet Profiler、Valgrind、Coverity、MALLOC_CHECK_。

8. 版本控制

8.1 版本號:API的每次發佈都應該附帶一個唯一的標識符,以便其最新版本能夠與之前版本有所區別。標準的做法是使用版本號。

大多數方案通過使用一系列數字反映一個發佈版本的修改程度,這些數字通常用英文句點(.)分隔:主版本號.次版本號.補丁版本號

軟件通常在最終版本發佈之前就提供給用戶使用,這些情況下,通常可以在版本號序列後添加一個符號用來表面軟件開發過程的相關階段。例如1.0.0alpha是指alpha發佈,1.0.0b是指beta發佈,而1.0.0rc是指候選發佈版。

在庫名中包含API的主版本號是良好的編程實踐,尤其是在做了一些不能向後兼容的修改時,例如libFoo.so、libFoo2.so或libFoo3.so。

API的版本信息應該可以在代碼中訪問,以便允許客戶以API版本號爲條件編寫代碼。

8.2 軟件分支策略:每個軟件項目都需要一條”主幹”代碼路線,它是項目源代碼的持久存儲庫。對於每次版本發佈,或者必須與下次發佈區分的開發工作,可由主幹代碼派生出分支。

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

在創建相同API的Basic和Advanced分發版時,在所有生成文件的版本號裏帶上”Basic”或”Advanced”字符串。不要試圖僅使用版本號識別文件是由Basic還是Advanced API生成的。

8.3 API的生命週期:API發佈後,可以改進(evolve)但不應改變(change)。

8.4 兼容性級別:

向後兼容性可以定義爲API提供與上一個版本相同的功能。換句話說,如果一個API不需要用戶作出任何改變就能夠完全取代上一個版本的API,那麼它就是向後兼容的。這暗示了新版本API是舊版本API的超集。可以添加新的功能,但是不能對舊API定義的已有功能做不兼容的修改。

向後兼容性有不同的類型,包括:功能兼容性、源代碼(或API)兼容性、二進制(或ABI)兼容性。

向後兼容性意味着使用第N版本API的客戶代碼能夠不加修改地升級到第N+1版本

功能兼容性意味着第N+1版本API的行爲和第N版本一致。

源代碼兼容性意味着用戶使用第N版本API編寫的代碼可以使用第N+1版本進行編譯,而不用修改源代碼。

二進制兼容性意味着使用第N版本API編寫的應用程序可以僅通過替換或重新鏈接API的新動態鏈接庫,就升級到第N+1版本。

有助於實現二進制兼容性的技巧:

(1).不要給已有方法增加參數,可以定義該方法新的重載版本。

(2).Pimpl模式可以用來幫助保持接口的二進制兼容性,因爲它把那些將來很可能發生變化的實現細節移進了.cpp文件,使之不會影響公有的.h文件。

(3).採用純C風格的API可以更容易地獲得二進制兼容性。爲了利用C和C++各自的優勢,可以選擇使用面向對象的C++風格開發API,然後用純C風格封裝C++ API。

(4).如果確實需要做二進制不兼容的修改,那麼可以考慮爲新庫換個不同的名字,這樣就不會破壞已有的應用程序。

向前兼容性:使用未來版本API編寫的客戶代碼如果無須修改就能夠編譯使用較老版本的API,則API是向前兼容的。因此,向前兼容性意味着用戶可以降級到之前的發佈版本,代碼無須修改,仍然能夠正常工作。

向前兼容性意味着使用第N版本API的客戶代碼可以不加修改地降級使用第N-1版本

8.5 怎樣維護向後兼容性:

在API初始版本發佈後,不要爲抽象基類添加新的純虛成員函數。

大多數編譯器提供了將類、方法或變量標記爲”已棄用”的方法,只要程序訪問了帶有這種標記的符號,就會輸出編譯時警告。對於Visual Studio C++,可在方法聲明前添加__declspec(deprecated)前綴,而對於GNU C++編譯器,可使用__attribute__((deprecated))。

8.6 API審查:在發佈API新版本前,引入API審查過程,以便檢查所有修改。

9. 文檔

編寫API文檔的一種最簡單的方式是使用工具自動地從頭文件中提取註釋來構建。在這類工具中最爲流行且特性完善的一款是Doxygen。

9.1 編寫文檔的理由:良好的文檔描述瞭如何使用API,並會解釋API對不同輸入所產生的行爲。在實現每個組件時編寫API文檔。在API完成後對文檔進行修訂。

契約編程(contract programming)核心觀點是:軟件組件爲它實現的服務提供一種契約或責任。通過使用組件,客戶同意契約中的條款。契約編程意味着爲函數的前置條件、後置條件,以及類的不變式編寫文檔。

應當爲API的每個公有元素編寫文檔,包括每個類、函數、枚舉、常量及typedef。如果客戶可以訪問某個元素,那就應該告訴客戶它是什麼和怎樣使用。

9.2 文檔的類型:使用自動生成文檔的工具,從頭文件註釋中提取API文檔。

除了自動生成的API文檔以外,還應該由人工編寫提供有關API更高層次信息的文檔。這通常是一份概述,內容包括API能做什麼、用戶爲什麼應當關注它等。

首次發佈之後,每個版本都應該包含發佈說明。它告訴用戶自上次發佈以來有哪些改動。發佈說明通常是一份簡潔的文檔。

在發佈API時,始終應該說明API所使用的授權方式。這讓客戶知道你授予他們哪些權利以及他們具有什麼義務。應當明確指定API的授權條款。

9.3 文檔可用性:索引頁、一致的視圖和體驗、代碼示例、圖表、搜索、麪包屑導航、術語。

9.4 使用Doxygen:Doxygen是一種基於源代碼中編寫的註釋自動生成多種格式API文檔的實用工具。它支持多種語言,包括C、C++、Objective-C、Java、Python等。它能夠生成多種格式的輸出,包括HTML、LaTex、PDF、XML以及其它格式。Doxygen是開源的(以GNU通用公共許可證授權發佈),提供若干種平臺的二進制發佈包,包括Windows、Linux和Mac。

Doxygen可自由配置。Doxygen支持多種不同風格的註釋。

Doxygen使用介紹可參考:https://blog.csdn.net/fengbingchun/article/details/104734578

10. 測試

爲了確保不破壞用戶程序,編寫自動化測試是所能採取的措施中最重要的一項。

10.1 編寫測試的理由:自動化測試可以幫助你確定是否正在構建正確的東西(也稱爲確認),以及是否構建正確(也稱爲驗證)。

10.2 API測試的類型:常見的軟件測試活動分爲:白盒測試:測試是在理解源代碼的基礎上進行的。黑盒測試:測試是基於產品說明進行的,而不關心任何實現細節。灰盒測試:白盒測試和黑盒測試的組合,其中黑盒測試是在獲知軟件實現細節的基礎上進行的。

API測試應該組合使用單元測試和集成測試。也可以適當運用非功能性技術測試,比如性能測試、併發測試和安全性測試。

單元測試是一種白盒測試技術,用於獨立驗證函數和類的行爲。如果代碼依賴於不可靠的資源,比如數據庫、文件系統或網絡,那麼可以使用樁對象或模擬對象創建更健壯的單元測試。

集成測試是一種黑盒技術,用於驗證幾個組件的交互過程。

對關鍵用例進行性能測試,有助於避免無意引入的速度問題或內存損失。如果API的性能很重要,考慮爲關鍵用例編寫性能測試,以避免引入性能損失。

10.3 編寫良好的測試:

良好測試的特徵:快速、穩定、可移植、高編碼標準、錯誤可重現。

測試API的關鍵技術:條件測試、等價類、邊界測試、參數測試、返回值斷言、getter/setter對、操作順序、迴歸測試、負面測試、緩衝區溢出、內存所有權、空輸入。

10.4 編寫可測試的代碼:不要等到最後再測試API。

測試驅動開發(Test-Driven Development, TDD)或者測試優先編程,指在編寫實現功能的代碼之前,先編寫自動化測試驗證代碼功能。測試驅動開發指首先編寫單元測試,然後編寫代碼使測試通過。這可以讓你專注於API的主要用途。

樁對象和模擬對象都返回封裝好的響應,但是模擬對象還會驗證調用行爲。

斷言是一種用於驗證代碼所做假設的辦法。你可以將假設信息編碼到斷言函數或宏中。表達式值爲真時,一切正常,不會有任何事情發生。而表達式值爲假時,則代碼所做的假設無效,程序會中止並拋出相應的錯誤信息。使用斷言記錄和驗證那些絕不應該發生的程序設計錯誤。

通過對斷言的系統性使用,強制保證接口契約。對接口而不是實現執行契約檢查。

國際化(i18n)是使軟件產品支持不同語言和區域差異的過程。相關術語”本地化”(i10n)是指基於基本的國際化支持,把應用程序的文本翻譯成特定的語言,併爲特定區域定義區域設置,如日期格式或貨幣符號

10.5 自動化測試工具:分爲4類:自動化測試框架、代碼覆蓋率工具、缺陷跟蹤系統、持續構建系統。

自動化測試框架:CppUnit、Boost Test、Google Test、TUT(Template Unit Test)。

代碼覆蓋率:這些工具能夠幫助你精確地發現代碼中哪些語句被測試過,也就是說,這些工具可以幫你快速定位沒有被測試覆蓋到的代碼。Bullseye Converage、Rational PureConverage、Intel Code-Converage Tool、Gconv。

缺陷跟蹤系統是一種能夠在數據庫中持續跟蹤軟件項目缺陷(通常也包括建議)的應用程序。缺陷跟蹤系統不是故障通知單(trouble ticket)系統或問題跟蹤系統,這兩個系統是用來接收用戶反饋的客戶支持系統,提出的很多問題都不是軟件本身的問題。客戶支持系統發現的有效軟件問題會進入缺陷跟蹤系統,並指派給一名工程師去解決。缺陷跟蹤系統也不是任務或項目管理工具,也就是說,它不是跟蹤任務和計劃工作的系統。不過,有些缺陷跟蹤系統廠商也提供了互補產品,使用底層基礎設施來支持項目管理工具。這些工具包括:Bugzilla、JIRA。

持續構建系統是一套自動化過程:一旦向版本控制系統提交了新修改,就會重新構建軟件。包括:Tindexrbox、Parabuild、TeamCity、Electric Cloud。

11. 腳本化

11.1 添加腳本綁定:腳本綁定提供了一種從腳本語言訪問C++ API的方式。這通常涉及爲C++的類和函數創建包裝代碼,然後利用腳本語言自身的模塊加載特性將其導入到腳本語言之中,比如Python可以使用import關鍵字導入包裝代碼,Ruby可以使用require。

11.2 腳本綁定技術:

Boost Python:也寫作boost::python或Boost.Python,是一個C++庫,它支持C++ API與Python的互操作。它是Boost庫中的一部分。利用Boost Python,你可以在C++代碼中以編程的方式創建綁定,然後將該綁定與Python和Boost Python庫鏈接起來。這會產生一個能夠直接導入到Python中的動態庫。

WING:是一個開源的實用工具,可以用於在多種不同的高級語言中創建對C和C++接口的綁定。

SIP:是一個工具,它支持爲Python創建C和C++綁定。

COM(Component Object Model, 組件對象模型):是一個二進制接口標準,它支持對象通過進程間通信機制進行彼此之間的交互。

11.3 使用Boost Python添加Python綁定:務必確認編譯腳本綁定時使用的Python版本與構建Boost Python庫時使用的是一致的。

11.4 使用SWIG添加Ruby綁定:

12. 可擴展性

所謂可擴展性,指的是對於客戶的特定需求,在無需改進API的情況下,客戶自行修改接口的行爲。

12.1 通過插件擴展:在最常見的場景下,同樣是動態庫,插件並不是在構建時鏈接的,而是在運行時發現並加載的。因此,用戶可以利用你定義好的插件API來編寫自己的插件。插件庫是一個動態庫,它可以獨立於核心API編譯,在運行時根據需要顯示加載。不過插件也可以使用靜態庫,比如在嵌入式系統中,所有插件都是在編譯時靜態鏈接到應用程序中的。對插件而言,確保以下兩點是有幫助的:一是插件要能在運行時找到,二是插件應該使用與主程序相同的構建環境。

一般而言,如果要創建插件系統,有兩個主要特性是必須要設計的:

(1).插件API:要創建插件,用戶必須編譯並鏈接插件API。這裏將它與核心API區分開來,後者是個規模更大的代碼庫,插件系統就是添加在覈心API之中的。

(2).插件管理器:這是核心API代碼中的一個對象(一般設計爲單例),負責管理所有插件的聲命週期,即插件的加載、註冊和卸載等各個階段。該對象也叫做插件註冊表。插件API是你提供給用戶的用於創建插件的接口。

你總是可以引入自己的插件文件擴展名。例如,Adobe Illustrator使用的插件擴展名是.aip,而Microsoft Excel插件的擴展名是.xll。

C++插件示例參考:https://blog.csdn.net/fengbingchun/article/details/105104659

12.2 通過繼承擴展:繼承是用於擴展類的主要面向對象機制。用戶可以基於API中現有的類類定義新類並修改其功能。

在定義派生類時,你只應該考慮提供了虛析構函數的類。

訪問者模式:其核心目標是允許客戶遍歷一個數據結構中的所有對象,並在每個對象上執行給定操作。本質上,該模式是對向現有類中添加虛方法這一方式的模仿。客戶能夠有效地將它們自己的方法插入到你的類層次結構中。

測試代碼cplusplus_api_design.hpp內容如下:

class Element;

// 訪問者模式: 表示一個作用於某對象結構中的各元素的操作. 它使你可以在不改變各元素的類的前提下定義作用於這些元素的新操作
// 訪問者模式使得易於增加新的操作: 訪問者使得增加依賴於複雜對象結構的構件的操作變得容易. 僅需增加一個新的訪問者即可在一個對象結構上定義一個新的操作
class Visitor {
public:
	virtual ~Visitor() = default;
	virtual void VisitConcreteElementA(Element* element) = 0;
	virtual void VisitConcreteElementB(Element* element) = 0;

protected:
	Visitor() = default;
};

class ConcreteVisitorA : public Visitor {
public:
	ConcreteVisitorA() = default;
	~ConcreteVisitorA() = default;

	void VisitConcreteElementA(Element* element) override
	{
		fprintf(stdout, "I will visit ConcreteElementA ...\n");
	}

	void VisitConcreteElementB(Element* element) override
	{
		fprintf(stdout, "I will visit ConcreteElementB ...\n");
	}
};

class ConcreteVisitorB : public Visitor
{
public:
	ConcreteVisitorB() = default;
	~ConcreteVisitorB() = default;

	void VisitConcreteElementA(Element* element) override
	{
		fprintf(stdout, "I will visit ConcreteElementA ...\n");
	}

	void VisitConcreteElementB(Element* element) override
	{
		fprintf(stdout, "I will visit ConcreteElementB ...\n");
	}
};

class Element {
public:
	virtual ~Element() = default;
	virtual void Accept(Visitor* visit) = 0;

protected:
	Element() = default;
};

class ConcreteElementA : public Element {
public:
	ConcreteElementA() = default;
	~ConcreteElementA() = default;

	void Accept(Visitor* visit) override
	{
		fprintf(stdout, "visiting ConcreteElementA ...\n");
		visit->VisitConcreteElementA(this);
	}
};

class ConcreteElementB : public Element {
public:
	ConcreteElementB() = default;
	~ConcreteElementB() = default;

	void Accept(Visitor* visit) override
	{
		fprintf(stdout, "visiting ConcreteElementB ...\n");
		visit->VisitConcreteElementB(this);
	}
};

int test_api_design_12_vistor();
int test_api_design_12();

測試代碼cplusplus_api_design.cpp內容如下:

int test_api_design_12_vistor()
{
	Visitor* visit = new ConcreteVisitorA();
	Element* element = new ConcreteElementA();
	element->Accept(visit);

	delete visit;
	delete element;

	return 0;
}

int test_api_design_12()
{
	return test_api_design_12_vistor();
}

12.3 通過模板擴展:當使用模板編程時,擴充接口的默認方式是利用具體類型來特化模板。

附錄A:

A.1 靜態庫與動態庫

靜態庫:靜態庫中包含目標代碼,這些目標代碼會被鏈接到最終用戶的應用程序,併成爲其中的一部分。靜態庫也稱爲存檔文件,因爲它本質上就是一些編譯後目標文件組成的包。一般來說,在Unix或Mac OS X機器上,靜態庫以.a爲文件擴展名,而在Windows上,其文件擴展名爲.lib。注意,靜態庫中只有真正用到的目標文件纔會被複制到應用程序中

動態庫:動態庫在編譯時鏈接,以便解析未定義引用;然後隨客戶的面向最終用戶的應用程序一起發佈,這樣應用程序就能在運行時加載庫的代碼了。這通常要求利用最終用戶機器上動態鏈接器在運行時判定並加載所有依賴的動態庫,執行必要的符號重定位,然後將控制傳給應用程序。比如說,Linux的動態鏈接器是ld.so,而Mac的動態鏈接器是dyld。一般而言,動態鏈接器支持很多環境變量,可以通過這些環境變量修改或調試其行爲。

動態庫有時稱爲共享庫,因爲它們可以被多個程序共享。在不同的平臺上,共享庫的文件擴展名也有所不同,比如說Unix上是.so文件,Windows上是.dll文件,而Mac OSX上則是.dylib文件。

爲了給客戶更大的靈活性,在發佈時,你應該首選動態庫形式。如果庫相當小,且非常穩定,你也可以提供一個靜態庫版本。

A.2 Windows上的庫:在Windows上,靜態庫用.lib文件表示,而動態庫用.dll文件表示。此外,每個.dll文件必須有一個相應的導入庫,或.lib文件。導入庫用來將引用解析爲DLL中導出的符號。

DLL入口點:DLL可以提供一個可選的入口點函數,線程或進程加載DLL時可以用它初始化數據結構,卸載DLL的時候也可以用它清理內存。這是由DllMain()函數管理的,你可以定義並將其導出DLL。如果入口函數的返回值爲FALSE,則是一個致命錯誤,應用程序會啓動失敗。

在Windows上加載插件:在Windows平臺上,LoadLibrary()或LoadLibraryEx()函數用以將動態庫加載到進程中,GetProcAddress()函數用以獲取DLL中的導出符號的地址。注意,以這種方式加載動態庫時,就不需要.lib導入庫文件了。

A.3 Linux上的庫:

在Linux上創建靜態庫:在Linux上,靜態庫就是一個簡單的存檔文件,其中包含的是目標文件。利用Linux的ar命令,可以將一些目標文件編譯到一個靜態庫中。

在Linux上創建動態庫:利用GNU C++編譯器,只要使用-shared鏈接器選項即可生成.so文件,而非可執行文件。你應該讓編譯器生成位置無關的代碼(PIC),不過這在有些平臺上並非默認行爲,因此需要使用命令行選項-fpic或-fPIC來指導編譯器。這是必要的,因爲當不同的可執行程序用到同一個動態庫時,庫中的代碼可能會被加載到不同的內存位置。因此,爲共享庫生成PIC代碼很重要,這樣用戶代碼就不需要依賴於符號的絕對內存地址了。

如果在同一個目錄中同時存在靜態庫和動態庫,它們的基本文件名又是相同的,如libmyapi.a和libmyapi.so,這時編譯器會選擇動態庫(除非使用-static選項,要求只使用靜態庫)。

注意:在動態庫中,所有代碼本質上都被扁平化地組織到一個目標文件中了。這與靜態庫不同,因爲靜態庫表示爲一組目標文件的集合,其中的目標文件會根據需要複製到可執行程序中(也就是說,對於靜態存檔文件中的目標文件,如果沒有引用到,就不會複製到可執行程序映像中)。其結果就是,加載動態庫時會將.so文件中定義的所有代碼都加載進來

在Linux上加載插件:在Linux平臺上,你可以調用dlopen()函數將.so文件加載到當前進程中,然後用dlsym()函數訪問庫中的符號。

A.4 Mac OS X上的庫:

在Mac OS X上創建靜態庫:Linux上創建靜態庫的方法也可以用於Mac OS X。不過,在將靜態庫鏈接到應用程序時,有些行爲是不同的。蘋果公司並不鼓勵使用-static編譯器選項以靜態鏈接所有依賴庫的方式來生成可執行程序。基本上,Mac上的-static選項是爲構建內核而保留的。Mac鏈接器默認情況下會掃描庫查找路徑中的所有位置來查找動態庫。如果查找失敗,鏈接器隨後會再次掃描這些位置來尋找靜態庫。

在Mac OSX上創建動態庫:與Linux上用到的指令非常相似。不過有一項重要的區別:在Mac上創建動態庫時,要使用g++的-dynamiclib選項代替-shared。

GitHubhttps://github.com/fengbingchun/Messy_Test

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章