一款優秀的 SDK 介面設計十大原則。
這些年我參與和主導過多款音影片 SDK 的設計和開發,也服務過大大小小几十家 toB 客戶,其中,有一條深深的感悟:
一個 PaaS 技術中介軟體產品,無論它的服務端 & 核心設計和實現的多麼牛逼多麼漂亮,最終交付給客戶開發者的 SDK 才是最最關鍵的要素和門面,它設計得好,即使背後有不足也能有一定程度上的彌補;它設計的爛,就幾乎廢棄掉了底層所有的努力,還會平添無數的無效加班和問題排障的投入。
本文關注一款優秀的 SDK 應該如何設計介面規格,以實現如下幾個目標:
- 簡潔明瞭,邊界清晰,介面正交(不存在 2 個介面相互衝突),使用者不容易踩坑
- 每一個 API 的行為確定,呼叫錯誤或者執行時異常的反饋及時準確
- 面向高階客戶:配置豐富,回撥豐富,業務擴充套件性和靈活性好
這裡致敬 《Effective C++》的行文模式,以條款的形式來描述和示例我的個人思考和總結(以最近深度參與的 RTC SDK 介面設計為例子)。
條款 1 :引數配置提供獨立的 profile 類,不要每個引數都提供一個 set 方法
// good case
// 記得給出合理的預設值
class AudioProfile
{
int samplerate{44100};
int channels{1};
};
// 記得給出合理的預設值
class VideoProfile
{
int maxEncodeWidth{1280};
int maxEncodeHeight{720};
int maxEncodeFps{15};
};
// 可以很好地進行擴充套件,比如 SystemProfile,ScreenProfile...
class EngineProfile
{
AudioProfile audio;
VideoProfile video;
};
class RtcEngine
{
public:
static RtcEngine* CreateRtcEngine(const EngineProfile& profile) = 0;
};
// bad case
// 1. 核心介面類 RtcEngine 的函式數量爆炸
// 2. 無法約束業務方呼叫 API 的時間(可能在加入房間後或者某個不合適的時間去配置引數)
// 3. 如果某個配置期望支援動態更新怎麼辦 ?通常配置是不建議頻繁動態更新的(會影響 SDK 內部行為),
// 如有必須,請顯式在 engine 提供 updateXXXX or switchXXX 介面
class RtcEngine
{
public:
static RtcEngine* CreateRtcEngine() = 0;
virtual void setAudioSampelerate(int samplerate) = 0;
virtual void setAudioChannels(int channels) = 0;
virtual void setVideoMaxEncodeResolution(int width, int height) = 0;
virtual void setVideoMaxEncodeFps(int fps) = 0;
};
條款 2 :非執行時的狀態 & 資訊的查詢和配置介面提供靜態方法
// good case
class RtcEngine
{
public:
static int GetSdkVersion();
static void SetLogLevel(int loglevel);
};
條款 3 :關鍵的非同步方法附帶上閉包回撥告知結果
// good case
typedef std::function<void(int code, string message)> Callback;
class RtcEngine
{
public:
// 客戶可及時在 callback 中處理事件,比如:改變 UI 狀態|提示錯誤|再次重試
virtual void Publish(Callback const& callback = nullptr) = 0;
virtual void Subscribe(Callback const& callback = nullptr) = 0;
};
// bad case
class RtcEngine
{
public:
class Listener
{
// 需要根據 code 來詳細判斷錯誤事件,且不一定能對得上哪一次 API 呼叫產生的錯誤
// 錯誤種類繁多,且跳出原來的邏輯,很多業務方會忽略在這裡處理一些關鍵錯誤
virtual void OnError(int code, string message) = 0;
};
void SetListener(Listener * listener)
{
_listener = listener;
}
virtual void Publish() = 0;
virtual void Subscribe() = 0;
private:
Listener * _listener;
};
條款 4 :所有介面儘量保證 “正交” 關係(不存在 2 個介面相互衝突)
// bad case
// EnalbeAudio 與其他 API 介面並不 “正交”,組合起來容易用錯
// MuteLocalAudioStream(true) & MuteAllRemoteAudioStreams(true) 依賴了使用者先呼叫 EnalbeLocalAudio(true)
class RtcEngine
{
public:
// EnalbeLocalAudio + MuteLocalAudioStream + MuteRemoteAudioStream
virtual void EnalbeAudio(bool enable) = 0;
// 開啟本地的音訊裝置(麥克風 & 揚聲器)
virtual void EnalbeLocalAudio(bool enable) = 0;
// 釋出/取消釋出本地音訊流
virtual void MuteLocalAudioStream(bool mute) = 0;
// 訂閱/取消訂閱遠端音訊流
virtual void MuteAllRemoteAudioStreams(bool mute) = 0;
};
條款 5 :考慮擴充套件性,可抽象的物件儘量用結構體代替 原子型別
// good case
class RtcUser
{
string userId;
string metadata;
};
class RtcEngineEventListenr
{
public:
// 未來可以很容易擴充套件 User 的資訊和屬性
virtual void OnUserJoined(const RtcUser& user) = 0;
};
// bad case
class RtcEngineEventListenr
{
public:
// 一旦介面提供出去後,未來關於 User 物件的一些擴充套件資訊和屬性無法新增
virtual void OnUserJoined(string userId, string metadata) = 0;
};
條款 6 :不可恢復的退出事件使用明確的 OnExit 且給出原因
客戶在面對 SDK 提供的 OnError 回撥事件的時候,由於錯誤種類特別多,他們往往不知道該如何應對和處理,建議有明確的文件告知處理方案。另外,當 SDK 內部發生了必須銷燬物件退出頁面的事件時,建議給出獨立的 callback 函式讓客戶專門處理。
enum ExitReason {
EXIT_REASON_FATAL_ERROR, // 未知的關鍵異常
EXIT_REASON_RECONNECT_FAILED, // 斷線後自動重連達到次數&時間上限
EXIT_REASON_ROOM_CLOSED, // 房間被關閉了
EXIT_REASON_KICK_OUT, // 被踢出房間了
};
class RtcEngineEventListenr
{
public:
// 一些警告訊息,不礙事,接著用
virtual void OnWarning(int code, const string &message) = 0;
// 發生了必須銷燬 SDK 物件的事件,請關閉頁面
virtual void OnExit(ExitReason reason, const string &message) = 0;
};
條款 7 :PaaS 產品的 SDK 不要包含業務邏輯和資訊
// bad case
enum ClientRole {
CLIENT_ROLE_BROADCASTER, // 主播,可以推流也可以拉流
CLIENT_ROLE_AUDIENCE // 觀眾,不能推流僅可以拉流
};
class RtcEngine
{
public:
// 需要明確的文件介紹不同的 role 所對應的角色,以及 role 切換產生的行為
// 該 API 與其他的 API 不是 “正交” 的,比如:Publish
virtual void SetClientRole(ClientRole& role) = 0;
};
// good case
// 建議在 examples 或者最佳實踐中,封裝多個 SDK 的原子介面,以達成上述 API 所起到的作用
class RoleManager
{
public:
// 透過這種方式,客戶可以顯式地感知到這個 API 背後的一系列的行為動作
void SetClientRole(ClientRole& role)
{
// _engine->xxxxx1();
// _engine->xxxxx2();
// _engine->xxxxx3();
}
private:
RtcEngine * _engine;
};
條款 8 :請提供所有必要的狀態查詢和事件回撥,別讓使用方 cache 狀態
// good case
class RtcUser
{
string userId;
string metadata;
bool audio{false}; // 是否開啟並且釋出了音訊流
bool video{false}; // 是否開啟並且釋出了影片流
bool screen{false}; // 是否開啟並且釋出了螢幕流
};
class RtcEngine
{
public:
// 由 SDK 內部來保持使用者狀態(最準確實時),並提供明確的查詢 API
// 而不是讓客戶在自己的程式碼中 cache 狀態(很容易出現兩邊狀態不一致的問題)
virtual list<RtcUser> GetUsers() = 0;
virtual RtcUser GetUsers(const string& userId) = 0;
};
條款 9 :儘可能為引數配置提供列舉能力,並且返回 bool 告知配置結果
class VideoProfile
{
public:
// 提供能力的列舉和配置結果,從而防止客戶以為的配置跟實際的情況不一致
bool IsHwEncodeSupported();
bool SetHwEncodeEnabled(bool enabled);
// 提供能力的列舉和配置結果,從而防止客戶以為的配置跟實際的情況不一致
int GetSupportedMaxEncodeWidth();
int GetSupportedMaxEncodeHeight();
bool SetMaxEncodeResolution(int width, int height);
};
條款 10 :介面檔案的位置和命名風格保持一定的規則和關係
// good case
// 某個程式碼 repo 的目錄結構(當然,僅 Android 的包客戶可感知,C++ 的庫外部無法感知目錄結構)
// 建議所有的對外的 interface 標頭檔案都在根目錄下,而實現檔案隱藏在內部資料夾中
// 合理的標頭檔案位置關係,能夠幫助開發者自己 & 客戶準確地感知哪些是介面檔案,哪些是內部檔案
// 所有的對外的標頭檔案,不允許 include 內部的檔案,否則存在標頭檔案汙染問題
// 所有的介面 Class 命名都以統一的風格開頭,比如 RtcXXXX,回撥都叫 XXXCallback 等等
src
- base
- audio
- video
- utils
- metrics
- rtc_types.h
- rtc_engine.h
- rtc_engine_event_listener.h
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69992957/viewspace-2751564/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 優秀設計的十大準則(上)
- 優秀設計的十大準則(下)
- 設計模式的七大原則(2) --介面隔離原則設計模式
- 設計模式“6”大原則!設計模式
- 設計模式六大原則(四)----介面隔離原則設計模式
- 優秀元件設計的關鍵:自私原則"元件
- 面象物件設計6大原則之四:介面隔離原則物件
- 優秀程式設計師都在注意的十個點程式設計師
- 遊戲設計的三大原則遊戲設計
- 設計模式的六大原則設計模式
- 設計模式的七大原則設計模式
- 設計模式 -- 設計模式七大原則設計模式
- 設計類六大原則
- 設計模式之7大原則設計模式
- 設計模式-六大原則設計模式
- 軟體設計7大原則
- 設計模式七大原則設計模式
- 設計模式——六大原則設計模式
- 設計模式六大原則設計模式
- Java的設計模式和6大原則Java設計模式
- 物件導向設計的6大原則物件
- 設計模式的七大原則(5) --開閉原則設計模式
- 設計模式之六大原則設計模式
- Python設計模式六大原則!Python設計模式
- 架構設計的五大原則-SOLID架構Solid
- OA系統設計的六大原則
- 設計模式的七大原則詳解設計模式
- 設計模式的七大原則(6) --迪米特法則設計模式
- 設計模式的七大原則(4) --里氏替換原則設計模式
- 設計模式六大原則(六)----開閉原則設計模式
- 設計模式六大原則詳解設計模式
- 設計模式的分類和六大原則設計模式
- [開發故事]成為優秀程式設計師的十個有效方法程式設計師
- 優秀的程式設計師都有的十條特徵,你中了幾條?程式設計師特徵
- 設計模式的七大原則(1) --單一職責原則設計模式
- 設計模式六大原則(五)----迪米特法則設計模式
- 物件導向設計的六大原則(SOLID原則)-——里氏替換原則物件Solid
- 好程式設計師Java教程分享Java設計模式的6大原則程式設計師Java設計模式