《API Design for C++》讀書筆記(一)
背景: 常見的模組化開發、程式碼複用、元件化、動態連結庫(DLL)、 軟體框架、分散式計算以及面向服務的架構(SOA),都隱含了對髙超的API設計技能的需求。
目標: 健壯而優雅、穩定而耐用、抽象而隱藏,最主要的: 變更管理----應對變化、新需求、功能要求及錯誤修復。
第一章:API簡介
1.1 什麼是API:
API的一個重要的基本定義是:API是一個明確定義的軟體元件的邏輯介面, 可以為其他軟體提供特定服務,可大可小,可互相依賴,隱藏了內部實現細節。API是為其所提供的服務或者行為定義的一個契約。
C++API通常會包含如下的元素
(1)標頭檔案:一組.h標頭檔案
(2)一個或多個靜態庫或動態庫檔案
(3)文件
Win32 API是純C API,而非C++API。可以在C++程式中直接使用C API, C++API中的傑出代表是STL( Standard Template Library,標準模板庫)。STL包含了一組容器類、對容器中元素進行遍歷的迭代器以及作用於容器的各種演算法。例如,該演算法集合中包括很多高階操作,比如std::search()、std::reverse()、std::sort()和std::set_intersection()。因此,STL 提供的是操作元素集合任務的邏輯介面,且沒有暴露每個演算法內部的實現細節。
1.2 API設計上有什麼不同:
與實現相比:
1. API是為開發者設計的介面,會被多處重用;
2. 修改API時,必須儘可能保證向後相容。
API描述了其他工程師構建他們的應用軟體所使用的軟體。因此,API必須擁有良好的設計、文件、迴歸測試,並且保證釋出之間的穩定性。
1.3 為什麼使用API
隱藏實現、延長系統壽命、促進模組化、減少程式碼重複(程式碼複用)、易於改變實現、易於優化、減少並行開發時的相互依賴.
1.6 檔案格式和網路協議
在計算機應用中存在幾個其他形式的常用通訊“協議”,其中最常見的一個就是檔案格式
表1-2 JFIF檔案格式頭部規範 |
||
屬性 |
位元組數 |
描述 |
APP0 marker |
2 |
總是OxFFE0 |
Length |
2 |
除APPO marker外的片段長度 |
Identifier |
5 |
總是0x4A46494600(“JFIF\0") |
Version |
2 |
0x0102 |
Density units |
1 |
單位畫素密度屬性,0表示無單位 |
X density |
2 |
整型水平畫素密度 |
Y density |
2 |
整型垂直畫素密度 |
Thumbnail width (w) |
1 |
內嵌縮圖的水平大小 |
Thumbnail height (h) |
1 |
內嵌縮圖的垂直大小 |
Thumbnail data |
3 x w x h |
無壓縮24位RGB柵格資料 |
有了資料檔案的格式,例如表1-2給出的JFIF/JPEG格式,任何程式都能讀寫這種格式的影象檔案。 這就使得不同的使用者之間可以交換影象資料,從而也衍生了影象査看器和能操作這些影象的工具。
同樣,網路協議也是一樣。
這些例子在概念上都和api相似,在api中為了交換資訊也需要定義標準的介面或規範。此外,任何規範的變更必定要考慮到對現有客戶端的影響。雖然它們具有相似性,但檔案格式和線路協議卻不是真正的API,因為它們不是程式的程式設計介面,因此不能被連結到應用程式中。一個較好的經驗法則是,每當你擁有一個檔案格式或客戶端/伺服器協議時,就應該製作附屬的API以管理規範變更。
每當你建立一個檔案格式或者客戶端/伺服器協議時,同時也要為其建立API。這樣,規範的細節以及未來的任何變更都將是集中且隱蔽的。
1.7 關於本書
應該考慮的特徵==》 有助於API設計的設計模式/慣用法==》設計實踐(功能收集+用例建模推動)==》風格(純C型,面向物件型,基於模板型,資料驅動型)
==》C++用法 ==》效能==》版本控制==》文件==》測試==》指令碼化(可選)==》可擴充套件性
第2章 特徵:
優秀API特徵: 資訊隱藏、一致性、鬆耦合。
C++中使用Pimpl慣用法是一種隱藏內部實現細節的方法。
2.1 問題域建模
2.1.1 提供良好的抽象
對於非技術人員,API提供的一組操作應該合乎邏輯且共屬同一單元。每個類都應該有一個主旨, 且這個主旨應該能通過類名和類包含的方法名一看就能看出來。
絕大多數API都能以多種方式建模,每種建模方式都能提供良好的抽象和易用的介面。關鍵是API應該具有一致且合理的支撐體系。
UML類圖:UML規範定義了一組面向物件軟體系統的視覺化建模符號。
本書將經常使用UML類圖描述類的設計。在這些圖表中,類用方框表示,該方框被分割成以下三個部分:
(1) 上層區域是類名;
(2) 中層區域列出類的屬性;
(3) 下層區域列出類的方法。
方框中、下層區域中的每條記錄都可以新增字首符,以此標識對應屬性或方法的訪問級別(有:
+表示公有的(public)類成員
-表示私有的(private)類成員
#表示受保護的(protected )類成員
類之間的關係可以用多種樣式的連線線和箭頭表示。下面列出了 UML類圖中可能出現的常見關係。
關聯:兩個類之間簡單的依賴關係,二者互不從屬於對方,這種關係用實線表示。關聯可以帶有方向,UML用開放箭頭表示方向,比如“>”。
聚合:“has-a”關係,或者整體與部分的關係,兩個類互不從屬於對方。用帶有空心菱形的直線表示。
組合:“contains-a”關係,部分和整體具有統一的生存期。用帶有實心菱形的直線表示。
泛化:類之間的父子關係,用帶空心三角箭頭的直線表示。
物件間的各種關係可以在某個物件邊用註釋定義,這樣就能分辨出它們的關係是一對一、一對多還是多對多。一些常見的關係有:
0..1表示零個或一個例項 1表示只有一個例項
0..*表示零個或多個例項
1..*表示一個或多個例項
2.1.2 關鍵物件的建模:
----描述特定問題域中物件的層次結構,確定主要物件的集合,釐清這些物件提供的操作以及物件之間的關係。
2.2 隱藏實現細節:
主要有兩種技巧可以達到此目標:物理隱藏和邏輯隱藏,物理隱藏表示只是不讓使用者獲得私有原始碼。邏輯隱藏則需要使用語言特性限制使用者訪問API的某些元素。
物理隱藏表示將內部細節(.cpp)與公有介面(.h)分離,儲存在不同的檔案中。
邏輯隱藏:封裝,提供了限制訪問物件成員的機制。public、protected以及private。封裝是將API的公有介面與其底層實現分離的過程。邏輯隱藏指的是使用C++語言中受保護的和私有的訪問控制特性從而限制訪問內部細節。
Java中的包私有表示該成員只能被同一個包中的類訪問,這是Java中的預設訪問級別。若要讓同一個JAR檔案中的其他類訪問該類的內部成員,而又不必將該類的內部成員暴露給客戶,那麼使用包私有是很好的做法。包私有在需要驗證私有方法的單元測試中十分有用。
應該使用get或set方法間接地訪問成員變數,可以在API不是執行緒安全的情況下,在get、set裡增加互斥鎖,免得直接訪問時每次都加。
類定義中成員變數或物件應隱藏,即宣告為私有。
2.2.4 隱藏實現方法
隱藏實現方法: 類只應該定義做什麼而不是如何做。
很遺憾,由於C++語言的限制,所有公有的、受保護的和私有的類成員都必須出現在類的宣告中。 理想情況下,類的頭部應該可以在其他地方宣告所有的私有成員。 儘管如此,仍然有一些方法可以讓私有成員在標頭檔案中不可見(Headington,1995)。一種常用的技巧稱為Pimpl慣用法,它將所有的私有資料成員隔離到一個.cpp檔案中獨立實現的類或結構體內。之後,.h檔案僅需要包含指向該類例項的不透明指標(opaque pointer)即可。在第3章中我將詳細探討這個極有價值的技巧。
強烈建議在API中採用Pimpl慣用法,這樣就可以將所有實現細節完全和公有標頭檔案分開。如果你不想這麼做,至少也要將標頭檔案內不需要的私有方法移到.cpp檔案中,並將它們轉換為靜態函式(Lakos, 1996)。但只有當私有方法僅訪問類的公有成員或者根本不訪問任何類成員時才能這麼做,(例如接收檔名字串,然後返回該檔名的副檔名的程式)。很多工程師認為如果類使用了私有方法,那麼就必須將其包含在類的聲明當中,但這麼就暴露了多餘的實現細節。
2.2.5 隱藏實現類
並非所有的類都需要公開,有些僅僅是用於實現,因此應該將其從API的公有介面中移除。
2.3 最小完備性
API儘量簡單,單一。
一個很好的建議是:當不確定是否需要某個介面時,就不要提供此介面。
2.3.2 謹慎新增虛擬函式
重寫函式可能破壞類的內部完整性,客戶可能採用不正確的或易於出錯的方式擴充套件API;也不需要將虛擬函式設為內聯,因為內聯是編譯時優化,虛擬函式是執行時確定的。
實際上,Herb Sutter指出,應該將虛擬函式宣告為私有的。
如果類包含任一虛擬函式,那麼必須將析構函式宣告為虛函式。絕不在建構函式或解構函式中呼叫虛擬函式,這些呼叫不會指向子類。
2.3.3 便捷化API
在減少API函式數目與使API易於各種客戶使用之間存在天然的矛盾。
原語性----一方面,有人認為API應該僅提供一個方法,僅執行一項任務。這確保了 API是最小化的、集中的、 一致的且易於理解的,還減少了實現的複雜性,並具備更穩定、更易於除錯和維護等優點。
另一方面,也有人認為API應該讓簡單的事情更簡單。這需要更高層次的封裝。 兩者並不矛盾,但最好不要出現在一塊兒,即混在同一個類中。API應該通過易用介面來呈現基本功能,同時將高階功能隱藏在另一個獨立的層次中。如基於OpenGL API的GLU和GLUT。
2.4 易用性
簡單、易理解、文件。使用自描述的列舉型別代替bool型別。不依賴引數順序(多定義幾個類如:Date類,Year、Month、Day類),命名一致、引數順序一致、記憶體模型語義、異常的使用和錯誤處理等。
不混用Begin/End和Start/Finish, src和end的順序一致, 儘量不使用縮略語。減少冗餘,增加獨立性。
2.4.5 健壯的資源分配:
弱指標,弱指標包含一個指向物件的指標,通常是共享指標,但是並不增加其指向的物件的引用計數。如果一個共享指標和一個弱指標引用了相同物件,那麼在共享指標被銷燬時,弱指標的值 會立即變為NULL。通過此方式,弱指標可以檢測其所指向的物件是否已經過期:即其指向物件的引用計數是否為零。這樣就避免了懸掛指標(指向已經釋放的記憶體)問題。
RAII ( Resource Acquisition Is Initialization)。
2.4.6 平臺獨立
class MobilePhone
{
public:
bool StartCall (const std::string &number);
bool EndCall();
#if defined TARGET_OS_IPHONE
bool GetGPSLocation(double* lat, double *lon);
#endif
}
這個拙劣的設計在不同的平臺上建立不同的API, 而如果在API的後續版本中增加了對另一個裝置類的支援(如Windows Mobile ),就不得不更新公有標頭檔案中的#if語句,使其包含_WIN32_WCE。然後,你的API客戶就必須在他們的程式碼中査找所有已經嵌入的TARGET_OS_IPHONE定義,並且擴充套件它使其也包含_WIN32_WCE,這都是因為你在無意中暴露了 API的實現細節。
正確的方法是隱藏某些功能只適用於特定平臺這一事實,並提供一個方法判定當前平臺是否支援 所需的功能。例如:
class MobilePhone
{
public:
bool StartCall(const std::string &number);
bool EndCall();
bool HasGPS() const;
bool GetGPSLocation(double* lat, double*lon);
}
具體實現上:
bool MobilePhone::HasGPS() const
{
#if defined TARGET_OS_IPHONE
return true:
#else
return false;
#endif
}
2.5 鬆耦合
耦合:軟體元件之間相互連線的強度的度量,即系統中每個元件對其他元件的依賴程度。
內聚:單個軟體元件內的各種方法相互關聯或聚合強度的度量。
優秀的軟體設計應該是低耦合(或鬆耦合)且高內聚的,即最小化不同元件之間功能的關聯件和連通性。達到這一目標之後,每個元件的使用、理解以及維護就能實現相互獨立了。
除非確實需要include類的完整定義,否則應該為類使用前置宣告。
如果情況允許, 先宣告非成員、非友元的函式,而非成員函式。與成員函式相比,使用非成員、非友元的方法能降低耦合度。
有時,冗餘是必要的。
2.5.4 管理器類:
管理器類可以通過封裝幾個低層次類降低耦合。
2.5.5 回撥、觀察者和通知
1. 回撥:
在C和C++中,回撥是模組A中的一個函式指標,該指標被傳遞給模組B,這樣B就能在合適的時間呼叫A中的函式。模組B對模組A—無所知,並且對模組A不存在“包含”(include)或者“連結” (link)依賴。回撥的這種特性使得低層程式碼能夠執行與其不能有依賴關係的高層程式碼。因此,在大型專案中,回撥是一種用於打破迴圈依賴的常用技術。
有時為回撥函式提供一個“閉包”也是有用的。閉包是模組A傳遞給模組B的一條資料,該資料包含在A提供給B的回撥函式中。這是模組A傳遞一些重要的回撥狀態資訊給模組B的一種途徑。
以下的標頭檔案展示瞭如何在C++中定義簡單的回撥API:
#include <string>
class ModuleB
{
public:
typedef void (*CallbackType)(const std::string &name, void *data);
void SetCal1back (Cal1backType cb, void *data);
.........
private:
Ca11backType mCa11back;
void *mClosure;
};
使用:
if (mCa11back)
(*mCallback)("Hello World", mClosure);
在面向物件的C++程式中使用回撥有一個難題,即使用非靜態(例項)方法作為回撥有些複雜。因為,此情況下物件的“this”指標也需要傳遞.本書附帶的原始碼中給出如何實現非靜態回撥函式的示例,該示例為每個成員回撥方法建立了一個靜態的封裝方法,並且使用額外的回撥引數傳遞this指標。
Boost庫中的boost::bind和C++11裡的std::function和std::bind更優雅.
boost::bind()的實現使用了functors(帶有狀態的函式). C++中, functors可以實現為一個類, 該類使用私有變數儲存狀態,幷包含一個過載的operator()方法執行函式.
相關推薦
《API Design for C++》讀書筆記(一)
背景: 常見的模組化開發、程式碼複用、元件化、動態連結庫(DLL)、 軟體框架、分散式計算以及面向服務的架構(SOA),都隱含了對髙超的API設計技能的需求。 目標: 健壯而優雅、穩定而耐用、抽象而隱藏,最主要的: 變更管理----應對變化、新需求、功能要求
《API Design for C++》讀書筆記(二):API特徵
目錄 本章所講的內容都是在回答下面這個問題:優質的API應該具有哪些基本特徵? 1、問題域建模 編寫API的目的是解決特定的問題或是完成具體的任務。因此,API應該首先為問題提供一個清晰的解決方案,同時能對實際的問題域進行準確的建模。
《Effective C++》讀書筆記一
1、C++中std是什麼意思? 摘自:https://blog.csdn.net/calvin_zhou/article/details/78440145 在程式中像vector ,cout,這類東西都是在std內,有時會忽略std::,你得自己認清 2、
Primer C++第五版 讀書筆記(一)
Primer C++第五版 讀書筆記(一) (如有侵權請通知本人,將第一時間刪文) 1.1-2.2 章節 關於C++變數初始化: 初始化不是賦值,初始化的含義是建立變數時賦予其一個初始值,而賦值的含義是把物件的當前值擦除,以一個新值來替代. 定義一個名為a的int變數並初始化為0,有以下4種方法
Head First C 讀書筆記(一)
一段程式碼: #include <stdio.h> int main(){ char cards[] = "JQK";//editable, copy is in stack //
【記】《.net之美》之讀書筆記(一) C#語言基礎
### 前言 工作之中,我們習慣了碰到任務就直接去實現其業務邏輯,但是C#真正的一些基礎知識,在我們久而久之不去了解鞏固的情況下,就會忽視掉。我深知自己正一步步走向只知用法卻不知原理的深淵,所以工作之餘,一直想找一些能深入講解C#基礎知識和底層原理的書籍,有幸在網上看到了《.net之美》一書,(--張子陽作
《你必須知道的.NET》讀書筆記一:小OO有大智慧
實現 職責 可靠性 基本 code cfile 生存 最好 min() 此篇已收錄至《你必須知道的.Net》讀書筆記目錄貼,點擊訪問該目錄可以獲取更多內容。 一、對象 (1)出生:系統首先會在內存中分配一定的存儲空間,然後初始化其附加成員,調用構造函數執行初始化,這
effective C++ 讀書筆記 條款14 以對象管理資源
effect virt 什麽 con pin 構造 ostream pos sha 如果我們使用一個投資行為的程序庫: #include "stdafx.h" #include <iostream> #include <memory> using
《Inside C#》筆記(一) .NET平臺
學習資料 framework 操作性 nbsp 數據庫操作 tom 不同 med out C# 基於.NET運行時,所以有必要首先對.NET以及C#與.NET平臺的關系有一定的了解。 一 .NET平臺 .NET背後的基本思想是將原本獨立工作的設備、網絡服務整合在一個統一的
effective C++ 讀書筆記 條款11
col tor 變量 pre amp 副本 swap 基本 目標 條款11: 在operator= 中處理“自我賦值” 在實現operator=時考慮自我賦值是必要的就像 x=y 。我們不知道變量x與y代表的值是否為同一個值(把x和y說成是一個指針更恰當一點)。例如
《JavaScript 高級程序設計》讀書筆記一 簡介
ron 設計 str 歷史 定義 程序 筆記一 scrip strong 一 歷史 二 實現 a. javascript三個部分: ECMAScript:由ECMA-262定義,提供核心語言功能; DOM:提供HTML的應用程序編程接口/提
C#日期筆記一
筆記 c# void image cnblogs oda pro program class 好記性不如爛筆頭。 關於日期的知識由此開篇~~ 1 class Program 2 { 3 static void Main(strin
C#數值筆記一
logs tel col ima mat string 分享 als decimal 好記性不如爛筆頭~~ 關於數值的筆記由此開始~~ 1 class Program 2 { 3 static void Main(string[]
連通性問題--Algorithms IN C讀書筆記
基礎 英文版 efi sni 數組元素 否則 每次 -m 添加 近期在看《Algorithms IN C》這本書。剛開始看,讀的是英文版的。感覺作者的敘述有點不太easy理解。就找了一本中文版的來看,發現還是看英文版的比較好。先看了第一章的大部分,後面的總
CLR via C#讀書筆記 CLR寄宿和AppDomain
利用 create 控制 jmp 代理 情況 note 系統目錄 exce 寄宿 寄宿是指讓其他應用程序(非托管代碼)使用CLR的能力,比如自己用C++開發的窗體能創建CLR實例。 托管代碼也能調用非托管代碼 [DllImport("kernel32.d
《大型網站技術架構》讀書筆記一:大型網站架構演化
硬件 解決方案 更新 獨立 流量 操作 大型網站技術架構 負責 思維導圖 一、大型網站系統特點 (1)高並發、大流量:PV量巨大 (2)高可用:7*24小時不間斷服務 (3)海量數據:文件數目分分鐘xxTB (4)用戶分布廣泛,網絡情況復雜:網絡運營
CLR via C# 讀書筆記-27.計算限制的異步操作(上篇)
top oid 輔助線 var 思考 read 運行 簡單例子 class 前言 學習這件事情是一個習慣,不能停。。。另外這篇已經看過兩個月過去,但覺得有些事情不總結跟沒做沒啥區別,遂記下此文 1.CLR線程池基礎 2.ThreadPool的簡單使用練習 3.執行上下文 4
ansible for devops讀書筆記第一章
class 讀書筆記 nbsp yum div username -a free gpo yum -y install ansible ansible --version mkdir /etc/ansible touch /etc/ansible/host
《算法導論》讀書筆記(一)
列排序 not else archive stdlib.h 二分查找 printf ima fine 本章是本書的開篇,介紹了什麽是算法,為什麽要學習算法,算法在計算機中的地位及作用。 算法(algorithm)簡單來說就是定義良好的計算機過程,它取一個或一組值
【C++】筆記一:Microsoft Visual Studio 2010軟件的安裝與建立第一個cpp文件
軟件的安裝 aid 密碼 span win32控制臺 left 編寫 visual 五步 筆記一:Microsoft Visual Studio 2010軟件的安裝與建立第一個cpp文件 我學習C++使用軟件為Microsoft Visual Studio 2010。 首先