淘寶iOS掃一掃架構升級 - 設計模式的應用

語言: CN / TW / HK

本文在“掃一掃功能的不斷迭代,基於設計模式的基本原則,逐步採用設計模式思想進行程式碼和架構優化”的背景下,對設計模式在掃一掃中新的應用進行了總結。

背景

掃一掃是淘寶鏡頭頁中的一個重要組成,功能執行久遠,其歷史程式碼中較少採用面向物件程式設計思想,而較多采用面向過程的程式設計。

隨著掃一掃功能的不斷迭代,我們基於設計模式的基本原則,逐步採用設計模式思想進行程式碼和架構優化。本文就是在這個背景下,對設計模式在掃一掃中新的應用進行了總結。

掃一掃原架構

掃一掃的原架構如圖所示。其中邏輯&展現層的功能邏輯很多,並沒有良好的設計和拆分,舉幾個例子:

  1. 所有碼的處理邏輯都寫在同一個方法體裡,一個方法就接近 2000 多行。
  2. 龐大的碼處理邏輯寫在 viewController 中,與 UI 邏輯耦合。

按照現有的程式碼設計,若要對某種碼邏輯進行修改,都必須將所有邏輯全量編譯。如果繼續沿用此程式碼,掃一掃的可維護性會越來越低。

圖片

因此我們需要對程式碼和架構進行優化,在這裡優化遵循的思路是:

  1. 瞭解業務能力
  2. 瞭解原有程式碼邏輯,不確定的地方通過埋點等方式線上驗證
  3. 對原有程式碼功能進行重寫/重構
  4. 編寫單元測試,提供測試用例
  5. 測試&上線

掃碼能力綜述

掃一掃的解碼能力決定了掃一掃能夠處理的碼型別,這裡稱為一級分類。基於一級分類,掃一掃會根據碼的內容和型別,再進行二級分類。之後的邏輯,就是針對不同的二級型別,做相應的處理,如下圖為技術鏈路流程。

圖片

設計模式

責任鏈模式

圖片

上述技術鏈路流程中,碼處理流程對應的就是原有的 viewController 裡面的巨無霸邏輯。通過梳理我們看到,碼處理其實是一條鏈式的處理,且有前後依賴關係。優化方案有兩個,方案一是拆解成多個方法順序呼叫;方案二是參考蘋果的 NSOperation 獨立計算單元的思路,拆解成多個碼處理單元。方案一本質還是沒解決開閉原則(對擴充套件開放,對修改封閉)問的題。方案二是一個比較好的實踐方式。那麼怎麼設計一個簡單的結構來實現此邏輯呢?

碼處理鏈路的特點是,鏈式處理,可控制處理的順序,每個碼處理單元都是單一職責,因此這裡引出改造第一步:責任鏈模式。

責任鏈模式是一種行為設計模式, 它將請求沿著處理者鏈進行傳送。收到請求後, 每個處理者均可對請求進行處理, 或將其傳遞給鏈上的下個處理者。

本文設計的責任鏈模式,包含三部分:

  1. 建立資料的 Creator
  2. 管理處理單元的 Manager
  3. 處理單元 Pipeline

三者結構如圖所示

圖片

建立資料的 Creator

包含的功能和特點:

  1. 因為資料是基於業務的,所以它只被宣告為一個 Protocol ,由上層實現。
  2. 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 )delegate { //對資料做物件化 TBCodeData data = [TBCodeData new]; data.scanResult = scanResult; data.delegate = delegate;

NSInteger dataId = 100; 
//開始執行遞迴
self.generateDataBlock(data, dataId);

} @end ````

管理處理單元的 Manager

包含的功能和特點:

  1. 管理建立資料的 Creator
  2. 管理處理單元的 Pipeline
  3. 採用支援鏈式的點語法,方便書寫

API 程式碼示例如下

``` @interface TBPipelineManager : NSObject /// 新增建立資料 Creator - (TBPipelineManager (^)(id dataCreator))addDataCreator; /// 新增處理單元 Pipeline - (TBPipelineManager (^)(id pipeline))addPipeline; /// 丟擲經過一系列 Pipeline 的資料。當 Creator 開始呼叫 generateDataBlock 後,Pipeline 就開始執行 @property (nonatomic, strong) void(^throwDataBlock)(id data); @end 實現程式碼示例如下

@implementation TBPipelineManager - (TBPipelineManager *(^)(id dataCreator))addDataCreator {
@weakify return ^(id dataCreator) { @strongify if (dataCreator) { [self.dataGenArr addObject:dataCreator]; } return self; }; }

  • (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 )data throwDataBlock:(void (^)(id data))block {
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];
    }
};

} ````

狀態模式

image.png

image.png

回頭來看下碼展示的邏輯,這是我們使用者體驗優化的一項重要內容。碼展示的意思是對於當前幀/圖片,識別到碼位置,我們進行錨點的高亮並跳轉。這裡包含三種情況: 1. 未識別到碼的時候,無錨點展示 1. 識別到單碼的時候,展示錨點並在指定時間後跳轉 1. 識別到多碼額時候,展示錨點並等待使用者點選

可以看到,這裡涉及到簡單的展示狀態切換,這裡就引出改造的第二步:狀態模式

image.png

狀態模式是一種行為設計模式, 能在一個物件的內部狀態變化時改變其行為, 使其看上去就像改變了自身所屬的類一樣。

本文設計的狀態模式,包含兩部分:

  1. 狀態的資訊 StateInfo
  2. 狀態的基類 BaseState

兩者結構如圖所示

image.png

狀態的資訊 StateInfo

包含的功能和特點:

  1. 當前上下文僅有一種狀態資訊流轉
  2. 業務方可以儲存多個狀態鍵值對,狀態根據需要執行相應的程式碼邏輯。

狀態資訊的宣告和實現程式碼示例如下 ```` @interface TBBaseStateInfo : NSObject { @private TBBaseState _currentState; //記錄當前的 State } //使用當前的 State 執行 - (void)performAction; //更新當前的 State - (void)setState:(TBBaseState )state; //獲取當前的 State - (TBBaseState *)getState; @end

@implementation TBBaseStateInfo - (void)performAction { //當前狀態開始執行 [_currentState perfromAction:self]; } - (void)setState:(TBBaseState )state { _currentState = state; } - (TBBaseState )getState { return _currentState; } @end 上層業務程式碼示例如下 typedef NS_ENUM(NSInteger,TBStateType) { TBStateTypeNormal, //空狀態 TBStateTypeSingleCode, //單碼展示態 TBStateTypeMultiCode, //多碼展示態 };

@interface TBStateInfo : TBBaseStateInfo //以 key-value 的方式儲存業務 type 和對應的狀態 state - (void)setState:(TBBaseState *)state forType:(TBStateType)type; //更新 type,並執行 state - (void)setType:(TBStateType)type; @end

@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

包含的功能和特點:

  1. 定義了狀態的基類
  2. 聲明瞭狀態的基類需要遵循的 Protocol

Protocol 如下,基類為空實現,子類繼承後,實現對 StateInfo 的處理。

``` @protocol TBBaseStateDelegate - (void)perfromAction:(TBBaseStateInfo *)stateInfo; @end 上層(以單碼 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 | ... | ... | ...

代理模式

圖片

在開發過程中,我們會在越來越多的地方使用到上圖能力,比如「淘寶拍照」的相簿中、「掃一掃」的相簿中,用到解碼碼展示碼處理的能力。

因此,我們需要把這些能力封裝並做成外掛化,以便在任何地方都能夠使用。這裡就引出了我們改造的第三步:代理模式。

代理模式是一種結構型設計模式,能夠提供物件的替代品或其佔位符。代理控制著對於原物件的訪問, 並允許在將請求提交給物件前後進行一些處理。 本文設計的狀態模式,包含兩部分:

  1. 代理單例 GlobalProxy
  2. 代理的管理 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 )scanProxy { if (!_scanProxy) { _scanProxy = [TBGlobalProxy proxyForProtocol:@protocol(TBScanProtocol)]; } _scanProxy.proxyImpl = self; return _scanProxy; }

//寫入 Proxy 的示例(解耦呼叫) - (void)registerGlobalProxy { //碼處理能力 [TBGlobalProxy registerProxy:[[NSClassFromString(@"TBScanProxy") alloc] init] withProtocol:@protocol(TBScanProtocol)]; //解碼能力 [TBGlobalProxy registerProxy:[[NSClassFromString(@"TBDecodeProxy") alloc] init] withProtocol:@protocol(TBDecodeProtocol)];} ````

掃一掃新架構

基於上述的改造優化,我們將原掃一掃架構進行了優化:將邏輯&展現層進行程式碼分拆,分為屬現層、邏輯層、介面層。已達到層次分明、職責清晰、解耦的目的。

image.png

總結

上述沉澱的三個設計模式作為掃拍業務的 Foundation 的  Public 能力,應用在鏡頭頁的業務邏輯中。

通過此次重構,提高了掃碼能力的複用性,結構和邏輯的清晰帶來的是維護成本的降低,不用再大海撈針從程式碼“巨無霸”中尋找問題,降低了開發人日。