【導讀】58 趕集集團旗下擁有多個 App,且全部使用同一套賬號體系,通過 Passport 部門提供的接口進行通信。經過多年迭代,各個 App 中關於 Passport 的功能均出現了一些流程和接口上的差異。爲了提高賬號安全,統一服務接口和流程,提高用戶體驗,由此決定開發了一個 Passport SDK,以集成 Passport 的相關功能,並提供給集團內各業務 App 使用。
在項目開始之初,我們在公司內經過調研發現在使用 SDK 時,大家最關心的問題就是 SDK 使用起來是否簡單,即接口是否簡單、調用流程是否簡單、迭代升級是否簡單。基於這幾個關鍵問題,我們把設計目標定爲:將原本 Passport 功能中繁瑣的流程變成 Passport SDK 中簡單的功能調用和結果處理,讓使用 Passport 功能的開發者不再需要關心那些數量龐大而又無關緊要的部分,取而代之的是享有一個非常良好的開發體驗。由此,我們將設計原則定爲:
- 接口要精簡;
- 服務的流程要黑盒;
- 無感知的迭代升級。
確定了設計原則後,下一步就是明確核心需求。Passport SDK 旨在爲 58 同城賬號體系下的用戶提供通用的登錄相關服務頁面和接口。所以我們的 SDK 核心需求是提供服務,即通用服務頁面和通用服務接口,並在用戶調用服務後返回其結果。
設計簡單且有效的接口
首先我們從需求上明確接口有哪些?答案是數據接口與服務接口,具體如下:
- 數據接口是一些零散的數據存取操作,實際上無法做出太多的精簡。
- 服務接口包括各種服務頁面的調起和服務接口的調用:在服務頁面中,App 用戶與服務頁面的交互會觸發對應的業務事件;在服務接口中,會直接觸發對應的業務事件。
它們有一些共同點,比如都是主動發起的服務,都有各自的回調方法,大部分都需要可選或必選參數。
按照正常的設計模式,每個服務頁面和服務接口都可以設計爲單獨的一個接口。但是因爲 Passport 提供了數量衆多的服務,這種設計會造成大量接口的出現,從而增加 SDK 的接入與維護成本。因此在接口的設計上,必須做減法。
Passport SDK 的服務接口採用了集中式接口,我們把所有的服務頁面和服務接口抽象成服務類型。其中,每個服務類型代表一種服務,有自己的參數傳遞規則,有對應的回調方法。
如圖 1 所示,我們使用了接口路由的方法,在接口模塊內置了一個路由表,決定服務類型和對應服務(通用服務頁面和通用服務接口)的映射。
用戶只需在這個服務接口裏傳入服務類型和符合規則的參數即可調用對應服務。服務完成後,會通過服務類型對應的回調方法傳遞結果:
[WBLoginSDK handleLoginSDKServiceWithType:WBLoginSDKServiceTypeLoginAccount delegate:self presentByViewController:selfparams:nil];
簡單的接口設計會降低接入工作的成本,並使用戶獲得極好的接入體驗。
服務的實現
我們採用了分層的框架設計來支撐整個服務的實現,如圖 2 所示。
其中,接口層對外提供接口,對內調度服務:SDK 的用戶調用賬戶登錄服務頁面的接口,調起賬戶登錄服務頁面。
服務層響應接口層的服務調用,發起具體業務事件,處理業務事件回調:賬戶登錄服務頁面調起後,App 用戶輸入賬號密碼,點擊“登錄”按鈕,觸發“賬戶登錄”的業務事件,並在得到賬戶登錄結果後處理頁面跳轉。
業務邏輯層處理服務層發起的業務事件,通過網絡服務層發起對應的網絡請求,數據層進行數據存取:“賬戶登錄”的業務事件會首先跟 Passport 服務端通信,得到通信結果後處理數據,返回結果給賬戶登錄服務頁面。
整體框架層級劃分清晰,可以做到垂直劃分,減少了層次之間的依賴。層與層之間通過接口聯繫,互相影響而又互相獨立,接口不變,就可以替換接口所實現的層次:後期服務頁面的整體更換、網絡服務的重新設計等都需要提前考慮。
層與層之間高內聚、低耦合,各司其職,有利於標準化以及各層邏輯的複用,結構更加明確——通用服務頁面和通用服務接口會觸發一些相同的業務事件。這種情況下,兩者在業務邏輯層使用相同的代碼去處理業務邏輯,在不同的服務層處理結果。
由此,既提高了開發編碼速度,減小了併發開發難度,同時也降低了後期維護成本和維護時間。各層要實現的功能分配給不同的開發組,通過接口來互相通信,最後完成時組合起來就變成一個完整的功能。
服務流程的黑盒化
Passport 的服務流程並不是簡單的一步走,以賬號密碼登錄頁面服務爲例:
如圖 3 所示,用戶首先需要輸入賬號和密碼,點擊登錄按鈕,這時會觸發賬號密碼登錄的業務事件,向 Passport 服務端發起請求,服務端會根據請求的校驗結果和賬號狀態返回對應的結果:正常情形下爲成功;異常情形下會根據狀態調起對應的服務頁面,如圖片驗證碼校驗頁面—圖片驗證碼頁面會觸發圖片驗證碼下載的業務事件,得到展示的圖片,用戶輸入正確的圖片驗證碼後,點擊確認,會再次觸發賬號密碼登錄的業務事件,與 Passport 服務端交互,最終得到成功登錄的結果。
由此可見,Passport 的服務流程是由多個業務事件組合起來的,但是這些流程除了必要的交互外,跟用戶並沒有什麼關係,而用戶關心的只是最後的結果。所以,我們的設計目標是讓用戶對整個服務流程實現“無感知”。
如圖 4 所示,用戶通過 Passport SDK 調起通用服務頁面或通用服務接口。前者通過與 App 的用戶交互觸發業務事件,後者則直接觸發業務事件。業務事件經過與服務端通信後得到事件的處理結果,標示服務結束或者跳轉到其他服務頁面,通過頁面交互觸發下一個業務事件直到服務結束。服務結束後,會通知用戶服務的結果。
Passport 的服務可以看作是一個環形,由用戶發起,在用戶結束,中間有一次或多次的業務事件處理,且無需對用戶暴露,所以 Passport 服務的處理過程實際上是一個黑盒——用戶調起服務後,只有在出現成功或失敗的結果後,他們纔會收到服務完成的回調,其他的事件都在 Passport SDK 內部處理,用戶只需要調起服務,等待響應服務結果即可。
考慮到回調的接收對象只有一個,爲了方便用戶,我們會對關鍵服務(例如登錄和註銷)的結果進行全局廣播,只需要監聽對應的頻段即可響應。
服務流程的節點
如圖 5 所示,在服務的流程中,每個業務事件都是一個節點,而這些節點串聯起來就形成了整個服務。業務事件的處理結果決定有沒有下一個服務頁面以及下一個服務頁面是什麼。所以根據服務端的靈活配置,可以對某個服務的流程進行實時變更,使服務更加貼合實際的需求。
我們還加入了萬能節點,用於超出內置服務頁面的特殊情況。它是一個簡單的 Hybrid 框架,可以承載 Passport 的 Web 服務用於處理緊急事件,並同步信息至 SDK。
迭代升級
框架的分層設計使得業務的開發非常便捷——在各個層次添加要實現的功能,並使其通過接口或協議通信即可;對原有功能的修改也隻影響內部對應的部分。
但是作爲 SDK,不僅要考慮其自身的開發成本,還需要考慮用戶的接入成本。
在接入方面,SDK 每次迭代新增的服務具體體現爲新增的服務類型和對應的回調方法。接口調用方法不變,也沒有增加太多的學習成本,且對原有功能的修改於用戶而言也是無感知的。
技術細節
自說明的接口
下方代碼是頭文件中手機動態碼登錄這個服務類型的註釋,從中可以瞭解到該服務類型的功能、是頁面服務還是接口服務、參數的種類與功能、參數的示例。
/**
* 58手機號動態碼登錄
* 是否調起頁面: 是
* Params: NSString: LoginSDKHideLeftButton(隱藏左上按鈕,可選)
NSString: LoginSDKHideRightButton(隱藏註冊按鈕,可選)
NSString: LoginSDKHideAccountButton(隱藏賬號登錄按鈕,可選)
* Examples: @{ LoginSDKHideRightButton : @"1" }
*/
WBLoginSDKServiceTypeLoginMobile,
通過接口名稱和註釋達到功能的自說明,不看示例代碼就可以開始寫,不亦樂乎?
集成功能的可選
有一些服務頁面上集成了多個功能,比如多種登錄方式、去往其他服務頁面的入口等(如圖 6 所示)。用戶在使用通用服務頁面時,可能只是需要其中的某部分功能而已。
所以我們在服務類型的可選參數中給了用戶選擇的途徑——通過參數可配置對應功能,比如入口的開啓和關閉、展示及隱藏。
接口的健壯性
接口的參數一定要在第一時間做格式校驗:接口作爲統一的入口,會把參數通過路由傳遞給服務層,在入口保證參數的合法性,杜絕後面流程中參數可能出現問題的隱患。
/**
* 登錄後的通知
*/
PASSPORT_EXTERN NSString * const LoginSDKLoginNotification;
/**
* 登錄後獲取到用戶信息的通知
*/
PASSPORT_EXTERN NSString * const LoginSDKFetchedUserInfoNotification;
/**
* 註銷後的通知
*/
PASSPORT_EXTERN NSString * const LoginSDKLogoutNotification;
其中,對外暴露的全局變量使用 const 修飾來保證這些變量的安全性。
日誌
Passport SDK 內部內置了一套日誌系統,但是不一定滿足使用者的需求。所以我們在每一次寫日誌的時候都會有一次回調,將當前的日誌參數傳出,用戶可以自行處理(如圖 7 所示)。
開發調試與實際運行差異
SDK 是以靜態庫的方式產出,在開發和調試時則是以源碼的方式。所以 SDK 在調試跟實際運行時是兩個環境,資源文件的路徑會有所不同。
+ (NSBundle *)loginSDKBundle {
if (FrameworkSwitch) {
return [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"WBLoginSDK" ofType:@"bundle"]];
}else {
return [NSBundle mainBundle];
}
}
我們通過宏編譯來達到在兩個環境下使用正確的路徑,並保持代碼中的宏條件永遠處於調試條件。在編譯時使用聚合編譯,通過 Shell 命令修改宏條件,來進行靜態庫條件的編譯,在編譯完成後將此宏條件改回調試條件,即如圖 8 所示。
公用庫的使用
SDK 須儘量避免使用公共庫,如果使用,公用庫也應該作爲外鏈文件存在,不要編譯進靜態庫中,以避免與使用者的工程衝突(見圖 9)。
總結
SDK 的設計與開發是一個長期且繁瑣的過程。爲了保證目標順利實現,需要針對其做好設計方案,這樣可以有效提升開發效率,減少隱藏的技術風險,並加強代碼質量,從而使得整體的開發工作不斷迭代,日趨完美。
作者:張達理,目前就職於 58 同城,任 iOS 客戶端架構師。專注於跨端 SDK 研發以及性能優化,主導了 58 Passport SDK 的架構設計及研發。
責編:唐小引,技術之路,共同進步。歡迎技術投稿、給文章糾錯,請發送郵件至[email protected]。聲明: 本文爲《程序員》03 期原創文章,未經允許請勿轉載,更多精彩文章請訂閱 2017 年《程序員》。