淘寶iOS掃一掃架構升級 - 設計模式的應用
本文在“掃一掃功能的不斷迭代,基於設計模式的基本原則,逐步採用設計模式思想進行程式碼和架構優化”的背景下,對設計模式在掃一掃中新的應用進行了總結。
背景
掃一掃是淘寶鏡頭頁中的一個重要組成,功能執行久遠,其歷史程式碼中較少採用面向物件程式設計思想,而較多采用面向過程的程式設計。
隨著掃一掃功能的不斷迭代,我們基於設計模式的基本原則,逐步採用設計模式思想進行程式碼和架構優化。本文就是在這個背景下,對設計模式在掃一掃中新的應用進行了總結。
掃一掃原架構
掃一掃的原架構如圖所示。其中邏輯&展現層的功能邏輯很多,並沒有良好的設計和拆分,舉幾個例子:
- 所有碼的處理邏輯都寫在同一個方法體裡,一個方法就接近 2000 多行。
- 龐大的碼處理邏輯寫在 viewController 中,與 UI 邏輯耦合。
按照現有的程式碼設計,若要對某種碼邏輯進行修改,都必須將所有邏輯全量編譯。如果繼續沿用此程式碼,掃一掃的可維護性會越來越低。
因此我們需要對程式碼和架構進行優化,在這裡優化遵循的思路是:
- 瞭解業務能力
- 瞭解原有程式碼邏輯,不確定的地方通過埋點等方式線上驗證
- 對原有程式碼功能進行重寫/重構
- 編寫單元測試,提供測試用例
- 測試&上線
掃碼能力綜述
掃一掃的解碼能力決定了掃一掃能夠處理的碼型別,這裡稱為一級分類。基於一級分類,掃一掃會根據碼的內容和型別,再進行二級分類。之後的邏輯,就是針對不同的二級型別,做相應的處理,如下圖為技術鏈路流程。
設計模式
責任鏈模式
上述技術鏈路流程中,碼處理流程對應的就是原有的 viewController 裡面的巨無霸邏輯。通過梳理我們看到,碼處理其實是一條鏈式的處理,且有前後依賴關係。優化方案有兩個,方案一是拆解成多個方法順序呼叫;方案二是參考蘋果的 NSOperation 獨立計算單元的思路,拆解成多個碼處理單元。方案一本質還是沒解決開閉原則(對擴充套件開放,對修改封閉)問的題。方案二是一個比較好的實踐方式。那麼怎麼設計一個簡單的結構來實現此邏輯呢?
碼處理鏈路的特點是,鏈式處理,可控制處理的順序,每個碼處理單元都是單一職責,因此這裡引出改造第一步:責任鏈模式。
責任鏈模式是一種行為設計模式, 它將請求沿著處理者鏈進行傳送。收到請求後, 每個處理者均可對請求進行處理, 或將其傳遞給鏈上的下個處理者。
本文設計的責任鏈模式,包含三部分:
- 建立資料的 Creator
- 管理處理單元的 Manager
- 處理單元 Pipeline
三者結構如圖所示
建立資料的 Creator
包含的功能和特點:
- 因為資料是基於業務的,所以它只被宣告為一個 Protocol ,由上層實現。
- Creator 對資料做物件化,物件生成後
self.generateDataBlock(obj, Id)
即開始執行
API 程式碼示例如下
/// 資料產生協議 <CreatorProtocol>
@protocol TBPipelineDataCreatorDelegate <NSObject>
@property (nonatomic, copy) void(^generateDataBlock)(id data, NSInteger dataId);
@end
上層業務程式碼示例如下
````
@implementation TBDataCreator
@synthesize generateDataBlock;
- (void)receiveEventWithScanResult:(TBScanResult )scanResult eventDelegate:(id
NSInteger dataId = 100;
//開始執行遞迴
self.generateDataBlock(data, dataId);
} @end ````
管理處理單元的 Manager
包含的功能和特點:
- 管理建立資料的 Creator
- 管理處理單元的 Pipeline
- 採用支援鏈式的點語法,方便書寫
API 程式碼示例如下
```
@interface TBPipelineManager : NSObject
/// 新增建立資料 Creator
- (TBPipelineManager (^)(id實現程式碼示例如下
@implementation TBPipelineManager
- (TBPipelineManager *(^)(id
@weakify
return ^(id
-
(TBPipelineManager *(^)(id
pipeline))addPipeline { @weakify return ^(id pipeline) { @strongify if (pipeline) { [self.pipelineArr addObject:pipeline]; //每一次add的同時,我們做鏈式標記(通過runtime給每個處理加Next) if (self.pCurPipeline) { NSObject *cur = (NSObject *)self.pCurPipeline; cur.tb_nextPipeline = pipeline; } self.pCurPipeline = pipeline; } return self;
}; }
-
(void)setThrowDataBlock:(void (^)(id _Nonnull))throwDataBlock { _throwDataBlock = throwDataBlock;
@weakify //Creator的陣列,依次對 Block 回撥進行賦值,當業務方呼叫此 Block 時,就是開始處理資料的時候
[self.dataGenArr enumerateObjectsUsingBlock:^(id_Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { obj.generateDataBlock = ^(id data, NSInteger dataId) { @strongify data.dataId = dataId; //開始遞迴處理資料 [self handleData:data]; }; }]; } -
(void)handleData:(id)data { [self recurPipeline:self.pipelineArr.firstObject data:data]; }
-
(void)recurPipeline:(id
)pipeline data:(id)data { if (!pipeline) { return; } //遞迴讓pipeline處理資料 @weakify [pipeline receiveData:data throwDataBlock:^(id _Nonnull throwData) { @strongify NSObject cur = (NSObject )pipeline; if (cur.tb_nextPipeline) { [self recurPipeline:cur.tb_nextPipeline data:throwData]; } else { !self.throwDataBlock?:self.throwDataBlock(throwData); } }]; } @end ````
處理單元 Pipeline
包含的功能和特點:
-
因為資料是基於業務的,所以它只被宣告為一個 Protocol ,由上層實現。
API 程式碼示例如下
@protocol TBPipelineDelegate <NSObject>
//如果有錯誤,直接丟擲
- (void)receiveData:(id)data throwDataBlock:(void(^)(id data))block;
@end
上層業務程式碼示例如下
```
//以A型別碼碼處理單元為例
@implementation TBGen3Pipeline
- (void)receiveData:(id
TBScanResult result = data.scanResult;
NSString scanType = result.resultType;
NSString *scanData = result.data;
if ([scanType isEqualToString:TBScanResultTypeA]) {
//跳轉邏輯
...
//可以處理,終止遞迴
BlockInPipeline();
} else {
//不滿足處理條件,繼續遞迴:由下一個 Pipeline 繼續處理
PassNextPipeline(data);
}
} @end ````
業務層呼叫
有了上述的框架和上層實現,生成一個碼處理管理就很容易且能達到解耦的目的,程式碼示例如下
``` - (void)setupPipeline { //建立 manager 和 creator self.manager = TBPipelineManager.new; self.dataCreator = TBDataCreator.new;
//建立 pipeline
TBCodeTypeAPipelie *codeTypeAPipeline = TBCodeTypeAPipelie.new;
TBCodeTypeBPipelie *codeTypeBPipeline = TBCodeTypeBPipelie.new;
//...
TBCodeTypeFPipelie *codeTypeFPipeline = TBCodeTypeFPipelie.new;
//往 manager 中鏈式新增 creator 和 pipeline
@weakify
self.manager
.addDataCreator(self.dataCreator)
.addPipeline(codeTypeAPipeline)
.addPipeline(codeTypeBPipeline)
.addPipeline(codeTypeFPipeline)
.throwDataBlock = ^(id data) {
@strongify
if ([self.proxyImpl respondsToSelector:@selector(scanResultDidFailedProcess:)]) { [self.proxyImpl scanResultDidFailedProcess:data];
}
};
} ````
狀態模式
回頭來看下碼展示的邏輯,這是我們使用者體驗優化的一項重要內容。碼展示的意思是對於當前幀/圖片,識別到碼位置,我們進行錨點的高亮並跳轉。這裡包含三種情況: 1. 未識別到碼的時候,無錨點展示 1. 識別到單碼的時候,展示錨點並在指定時間後跳轉 1. 識別到多碼額時候,展示錨點並等待使用者點選
可以看到,這裡涉及到簡單的展示狀態切換,這裡就引出改造的第二步:狀態模式
狀態模式是一種行為設計模式, 能在一個物件的內部狀態變化時改變其行為, 使其看上去就像改變了自身所屬的類一樣。
本文設計的狀態模式,包含兩部分:
- 狀態的資訊 StateInfo
- 狀態的基類 BaseState
兩者結構如圖所示
狀態的資訊 StateInfo
包含的功能和特點:
- 當前上下文僅有一種狀態資訊流轉
- 業務方可以儲存多個狀態鍵值對,狀態根據需要執行相應的程式碼邏輯。
狀態資訊的宣告和實現程式碼示例如下
````
@interface TBBaseStateInfo : NSObject {
@private
TBBaseState
@implementation TBBaseStateInfo
- (void)performAction {
//當前狀態開始執行
[_currentState perfromAction:self];
}
- (void)setState:(TBBaseState 上層業務程式碼示例如下
typedef NS_ENUM(NSInteger,TBStateType) {
TBStateTypeNormal, //空狀態
TBStateTypeSingleCode, //單碼展示態
TBStateTypeMultiCode, //多碼展示態
};
@interface TBStateInfo : TBBaseStateInfo
//以 key-value 的方式儲存業務 type 和對應的狀態 state
- (void)setState:(TBBaseState
@implementation TBStateInfo
-
(void)setState:(TBBaseState
*)state forType:(TBStateType)type {
[self.stateDict tb_setObject:state forKey:@(type)]; } -
(void)setType:(TBStateType)type { id oldState = [self getState]; //找到當前能響應的狀態 id newState = [self.stateDict objectForKey:@(type)]; //如果狀態未發生變更則忽略 if (oldState == newState) return; if ([newState respondsToSelector:@selector(perfromAction:)]) { [self setState:newState]; //轉態基於當前的狀態資訊開始執行 [newState perfromAction:self]; } } @end ````
狀態的基類 BaseState
包含的功能和特點:
- 定義了狀態的基類
- 聲明瞭狀態的基類需要遵循的 Protocol
Protocol 如下,基類為空實現,子類繼承後,實現對 StateInfo 的處理。
```
@protocol TBBaseStateDelegate 上層(以單碼 State 為例)程式碼示例如下
@interface TBSingleCodeState : TBBaseState
@end
@implementation TBSingleCodeState
//實現 Protocol - (void)perfromAction:(TBStateInfo *)stateAction { //業務邏輯處理 Start ... //業務邏輯處理 End }
@end ````
業務層呼叫
以下程式碼生成一系列狀態,在合適時候進行狀態的切換。 ```` //狀態初始化 - (void)setupState { TBSingleCodeState singleCodeState =TBSingleCodeState.new; //單碼狀態 TBNormalState normalState =TBNormalState.new; //正常狀態 TBMultiCodeState *multiCodeState = [self getMultiCodeState]; //多碼狀態
[self.stateInfo setState:normalState forType:TBStateTypeNormal];
[self.stateInfo setState:singleCodeState forType:TBStateTypeSingleCode];
[self.stateInfo setState:multiCodeState forType:TBStateTypeMultiCode];
}
//切換常規狀態 - (void)processorA { //... [self.stateInfo setType:TBStateTypeNormal]; //... }
//切換多碼狀態 - (void)processorB { //... [self.stateInfo setType:TBStateTypeMultiCode]; //... }
//切換單碼狀態 - (void)processorC { //... [self.stateInfo setType:TBStateTypeSingleCode]; //... } ```` 最好根據狀態機圖編寫狀態切換程式碼,以保證每種狀態都有對應的流轉。
| 次態→ 初態↓ | 狀態A | 狀態B | 狀態C | | ------- | --- | --- | --- | | 狀態A | 條件A | ... | ... | | 狀態B | ... | ... | ... | | 狀態C | ... | ... | ...
代理模式
在開發過程中,我們會在越來越多的地方使用到上圖能力,比如「淘寶拍照」的相簿中、「掃一掃」的相簿中,用到解碼、碼展示、碼處理的能力。
因此,我們需要把這些能力封裝並做成外掛化,以便在任何地方都能夠使用。這裡就引出了我們改造的第三步:代理模式。
代理模式是一種結構型設計模式,能夠提供物件的替代品或其佔位符。代理控制著對於原物件的訪問, 並允許在將請求提交給物件前後進行一些處理。 本文設計的狀態模式,包含兩部分:
- 代理單例 GlobalProxy
- 代理的管理 ProxyHandler
兩者結構如圖所示
代理單例 GlobalProxy
單例的目的主要是減少代理重複初始化,可以在合適的時機初始化以及清空儲存的內容。單例模式對於 iOSer 再熟悉不過了,這裡不再贅述。
代理的管理 Handler
維護一個物件,提供了對代理增刪改查的能力,實現對代理的操作。這裡實現 Key - Value 的 Key 為 Protocol ,Value 為具體的代理。
程式碼示例如下
````
-
(void)registerProxy:(id)proxy withProtocol:(Protocol *)protocol { if (![proxy conformsToProtocol:protocol]) { NSLog(@"#TBGlobalProxy, error"); return; } if (proxy) { [[TBGlobalProxy sharedInstance].proxyDict setObject:proxy forKey:NSStringFromProtocol(protocol)]; } }
-
(id)proxyForProtocol:(Protocol *)protocol { if (!protocol) { return nil; } id proxy = [[TBGlobalProxy sharedInstance].proxyDict objectForKey:NSStringFromProtocol(protocol)]; return proxy; }
-
(NSDictionary *)proxyConfigs { return [TBGlobalProxy sharedInstance].proxyDict; }
-
(void)removeAll { [TBGlobalProxy sharedInstance].proxyDict = [[NSMutableDictionary alloc] init]; } ````
業務層的呼叫
所以不管是什麼業務方,只要是需要用到對應能力的地方,只需要從單例中讀取 Proxy,實現該 Proxy 對應的 Protocol,如一些回撥、獲取當前上下文等內容,就能夠獲取該 Proxy 的能力。
```
//讀取 Proxy 的示例
- (id
//寫入 Proxy 的示例(解耦呼叫) - (void)registerGlobalProxy { //碼處理能力 [TBGlobalProxy registerProxy:[[NSClassFromString(@"TBScanProxy") alloc] init] withProtocol:@protocol(TBScanProtocol)]; //解碼能力 [TBGlobalProxy registerProxy:[[NSClassFromString(@"TBDecodeProxy") alloc] init] withProtocol:@protocol(TBDecodeProtocol)];} ````
掃一掃新架構
基於上述的改造優化,我們將原掃一掃架構進行了優化:將邏輯&展現層進行程式碼分拆,分為屬現層、邏輯層、介面層。已達到層次分明、職責清晰、解耦的目的。
總結
上述沉澱的三個設計模式作為掃拍業務的 Foundation 的 Public 能力,應用在鏡頭頁的業務邏輯中。
通過此次重構,提高了掃碼能力的複用性,結構和邏輯的清晰帶來的是維護成本的降低,不用再大海撈針從程式碼“巨無霸”中尋找問題,降低了開發人日。
- 第14個天貓雙11,技術創新帶來消費新體
- 如何避免寫重複程式碼:善用抽象和組合
- 淘寶PC改版!我們跟一位背後付出6年的男人聊了聊……
- 在阿里做前端程式設計師,我是這樣規劃的
- 一種可灰度的介面遷移方案
- 如何快速理解複雜業務,系統思考問題?
- 淘寶iOS掃一掃架構升級 - 設計模式的應用
- HTTP3 RFC標準正式釋出,QUIC會成為傳輸技術的新一代顛覆者嗎?
- 2022大淘寶技術工程師推薦書單
- 國際頂會OSDI首度收錄淘寶系統論文,端雲協同智慧獲大會主旨演講推薦
- 如何持續突破效能表現? | DX研發模式
- 列表容器&事件鏈如何幫業務提升發版迭代效率? | DX研發模式
- 2022淘寶天貓618背後——與你息息相關的技術祕密
- 淘寶Native研發模式的演進與思考 | DX研發模式
- CVPR2022 | 開源:基於間距自適應查詢表的實時影象增強方法
- 無線運維的起源與專案建設思考
- 淘寶購物車5年技術升級與沉澱
- 從標準到開源,阿里大淘寶技術的“創新擔當”
- 程式設計師如何在業餘時間提升自己?