@
最近在工作中,需要將代碼封裝成庫,供其他方調用。在其中涉及到如何設計接口類,第一次接觸,將總結和經驗記錄下來。
導讀
為什么本文叫做《工程實踐:C++的接口設計》,是因為,我們大部分人入門的時候,都是調用別人封裝好的庫函數,卻沒有嘗試過自己封裝庫給別人用。但是在正常工作中,也就是工程化中,我們會經常封裝庫給其他人應用,這里面會涉及到怎么封裝一個函數,提供一個優秀的接口。
接口:類暴露出來的部分,是類所提供的功能。
接口設計准則
我們在工程化的過程中,設計接口一般遵守以下幾個准則:
- 單一功能原則
一個class就其整體應該只提供單一的服務。如果一個class提供多樣的服務,那么就應該把它拆分,反之,如果一個在概念上單一的功能卻由幾個class負責,這幾個class應該合並。 - 開放/封閉原則
一個設計並實現好的class,應該對擴充的動作開放,而對修改的動作封閉。也就是說,這個class應該是允許擴充的,但不允許修改。如果需要功能上的擴充,一般來說應該通過添加新類實現,而不是修改原類的代碼。添加新類不單可以通過直接繼承,也可以通過組合。 - 最小驚訝原理
在重載函數,或者子類實現父類虛函數時,應該基本維持函數原來所期望的功能。
接口設計注意事項
- 過度封裝
很多人喜歡這樣封裝,把接口局限在僅僅解決一個特定的問題上面,失去了代碼的靈活性。而且,也容易出現面條代碼,讓人不知所雲。一個接口被寫的僅僅用於解決當前問題,當試圖增加其擴展性時,發現為時已晚。
為了防止過度封裝,在設計接口的時候,我們應該考慮以下幾個問題:
1.我們需要解決什么問題
2.問題的核心是什么
3.應該怎樣設計,可以方便客戶程序員擴展
-
起名要見名知意
一個好的的接口方法名,是接口設計中成功的一半。 -
不要讓使用者進行過多工作
如果使用者使用我們的接口時,進行了過多的准備,那么對我們來說就是失敗的。
所以,設計的時候要記得盡可能簡化客戶程序員邏輯,使接口設計能夠看起來簡潔、漂亮,而不至於被接口的復雜性所嚇倒。 -
簡潔
這個比較好理解,舉個例子,你一定見過一個函數使用,需要傳進去五六個參數,但是對我們有用的往往只有那么一倆個。
所以,簡潔設計是接口設計的一個重要原則。可以在接口的內部實現中,使用復雜冗余的參數,而在暴露給客戶程序員的接口中,一定要盡可能簡潔。 -
清晰的文檔表述
這也是我在工作中,最頭疼的問題,頭疼的不是我不會寫,而是在使用其他人提供的庫時,沒有使用文檔,使用起來是痛苦的,所以為了不讓這種痛苦發生在其他人身上,我現在從維護一份良好的文檔開始。
接口設計想達到的效果
隔離用戶操作與底層邏輯
接口的倆種方法
一般來說,有兩種方法設計接口類。
第一種是PIMP方法,即Pointer to Implementation,在接口類成員中包含一個指向實現類的指針,這樣可以最大限度的做到接口和實現分離的原則。
第二種方法叫Object-Interface方法,它的思想是采用C++的動態功能,實現類繼承接口類,功能接口函數定義成虛函數。
先說結論,我們處於自身習慣的原因,選擇了Object-Interface方法。
PIMP方法
所謂PImp是非常常見的隱藏真實數據成員的技巧,核心思路就是用另一個類包裝了所要隱藏的真實成員,在接口類中保存這個類的指針。
//header complex.h
class ComplexImpl;
class Complex{
public:
Complex& operator+(const Complex& com );
Complex& operator-(const Complex& com );
Complex& operator*(const Complex& com );
Complex& operator/(const Complex& com );
private:
ComplexImpl* pimpl_;
};
在接口文件中聲明一個ComplexImpl*,然后在另一個頭文件compleximpl.h中定義這個類
//header compleximpl.h
class ComplexImpl{
public:
ComplexImpl& operator+(const ComplexImpl& com );
ComplexImpl& operator-(const ComplexImpl& com );
ComplexImpl& operator*(const ComplexImpl& com );
ComplexImpl& operator/(const ComplexImpl& com );
private:
double real_;
double imaginary_;
};
可以發現,這個ComplexImpl的接口基本沒有什么變化(其實只是因為這個類功能太簡單,在復雜的類里面,是需要很多private的內部函數去抽象出更多實現細節),然后在complex.cpp中,只要
#include "complex.h"
#include "compleximpl.h"
包含了ComplexImpl的實現,那么所有對於Complex的實現都可以通過ComplexImpl這個中介去操作。詳細做法百度還有一大堆,就不細說了。
Object-Interface 抽象基類法
一般來說,如果一個接口類對應有若干個實現類,可以采用這種方法。
上面我們講了plmp方法,我們隱藏掉倆個數據成員,但同時也多出了一個新的數據成員,也就是接口指針,那么有沒有方法,連這個指針也不要呢?
這時候就是抽象基類發揮作用的時候了。看代碼:
class Complex{
public:
static std::unique_ptr<Complex> Create();
virtual Complex& operator+(const Complex& com ) = 0;//純虛函數,接口成員函數
virtual Complex& operator-(const Complex& com ) = 0;
virtual Complex& operator*(const Complex& com ) = 0;
virtual Complex& operator/(const Complex& com ) = 0;
};
將要暴露出去的接口都設置為純虛函數,通過 工廠方法Create來獲取Complex指針,Create返回的是繼承實現了集體功能的內部類;
//Complex類功能的內部實現類
class ComplexImpl : public Complex{
public:
virtual Complex& operator+(const Complex& com ) override;
virtual Complex& operator-(const Complex& com ) override;
virtual Complex& operator*(const Complex& com ) override;
virtual Complex& operator/(const Complex& com ) override;
private:
double real_;
double imaginary_;
}
至於Create函數也很簡單:
std::unique_ptr<Complex> Complex::Create()
{
return std::make_unique<ComplexImpl>();
}
這樣,我們完完全全將Complex類的實現細節全部封裝隱藏起來了,用戶一點都不知道里面的數據結構是什么;
當然,對於Complex這樣的類來說,用戶是有獲取他的實部虛部這樣的需求的,也很簡單,再加上兩個Get方法就可以達到目的。
Object_interface 抽象基類示例代碼
- 首先,聲明一個接口
// circle.h
// 圓的接口類
class Circle {
public:
virtual ~Circle() {};
// 接口方法:面積
virtual double area() = 0;
};
- 通過繼承的方式實現這個接口
// circle_impl.h
#include "circle.h"
// 圓的具體實現類
class CircleImpl : public Circle {
private:
double radius;
public:
CircleImpl(double radius);
double area() override;
};
// circle_impl.cpp
#include <cmath>
#include "circle_impl.h"
inline double pi() {
return std::atan(1) * 4;
};
CircleImpl::CircleImpl(double _radius) : radius(_radius) {
};
double CircleImpl::area() {
return pi() * radius * radius;
};
- 最后,通過管理類創建接口派生類的實例,或者銷毀接口派生類的實例:
// circle_manager.h
#include "circle.h"
// 圓的創建工廠類
class CircleManager {
public:
static Circle* create(double radius); // 創建circle實例
static void destroy(Circle* circlePtr); // 銷毀circle實例
};
// circle_manager.cpp
#include "circle_manager.h"
#include "circle_impl.h"
Circle* CircleManager::create(double radius) {
Circle* circlePtr = new CircleImpl(radius);
return circlePtr;
};
void CircleManager::destroy(Circle* circlePtr) {
delete circlePtr;
};
現在我們接口已經實現完畢了,我們可以把它封裝成庫,給其他人使用了,這里封裝庫我們就不多言了。
最后,來看一下使用效果:
// main.cpp
#include <iostream>
#include "circle_manager.h"
#include "circle.h"
int main()
{
Circle* circlePtr = CircleManager::create(3);
cout << circlePtr->area() <<endl;
CircleManager::destroy(circlePtr);
system("pause");
return 0;
}
以上代碼只提供給外部circle的接口,circle的實現完全被隱藏了起來,外部將無從知曉,外部使用者只能通過circle管理類生成circle的派生類的實例。外部使用者得到circle派生類的實例后,除了能調用接口暴露的方法area()外,其它什么也做不了,這樣就完全達到了使用接口的最終目標。
參考資料
C++中的接口設計准則
C++ 頭文件接口設計淺談
一款優秀的 SDK 接口設計十大原則
C++:如何正確的使用接口類
總結
本篇文章拋磚引玉,自己也是剛剛接觸,寫完收工,干飯去!