本文在“掃一掃功能的不斷迭代,基于設計模式的基本原則,逐步采用設計模式思想進行代碼和架構優化”的背景下,對設計模式在掃一掃中新的應用進行了總結。
背景
掃一掃是淘寶鏡頭頁中的一個重要組成,功能運行久遠,其歷史代碼中較少采用面向對象編程思想,而較多采用面向過程的程序設計。
隨著掃一掃功能的不斷迭代,我們基于設計模式的基本原則,逐步采用設計模式思想進行代碼和架構優化。本文就是在這個背景下,對設計模式在掃一掃中新的應用進行了總結。
掃一掃原架構
掃一掃的原架構如圖所示。其中邏輯&展現層的功能邏輯很多,并沒有良好的設計和拆分,舉幾個例子:
- 所有碼的處理邏輯都寫在同一個方法體里,一個方法就接近 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 <TBScanPipelineEventDeletate>)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<TBPipelineDataCreatorDelegate> dataCreator))addDataCreator;
/// 添加處理單元 Pipeline
- (TBPipelineManager *(^)(id<TBPipelineDelegate> pipeline))addPipeline;
/// 拋出經過一系列 Pipeline 的數據。當 Creator 開始調用 generateDataBlock 后,Pipeline 就開始執行
@property (nonatomic, strong) void(^throwDataBlock)(id data);
@end
實現代碼示例如下
@implementation TBPipelineManager
- (TBPipelineManager *(^)(id<TBPipelineDataCreatorDelegate> dataCreator))addDataCreator {
@weakify
return ^(id<TBPipelineDataCreatorDelegate> dataCreator) {
@strongify
if (dataCreator) {
[self.dataGenArr addObject:dataCreator];
}
return self;
};
}
- (TBPipelineManager *(^)(id<TBPipelineDelegate> pipeline))addPipeline {
@weakify
return ^(id<TBPipelineDelegate> 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<TBPipelineDataCreatorDelegate> _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
obj.generateDataBlock = ^(id<TBPipelineBaseDataProtocol> data, NSInteger dataId) {
@strongify
data.dataId = dataId;
//開始遞歸處理數據
[self handleData:data];
};
}];
}
- (void)handleData:(id)data {
[self recurPipeline:self.pipelineArr.firstObject data:data];
}
- (void)recurPipeline:(id<TBPipelineDelegate>)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 <TBCodeDataDelegate>)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];
}
};
}
狀態模式
回頭來看下碼展示的邏輯,這是我們用戶體驗優化的重要一項內容。碼展示的意思是對于當前幀/圖片,識別到的碼位置,我們進行錨點的高亮并跳轉。這里包含三種情況:1. 未識別到碼的時候,無錨點展示 2. 識別到單碼的時候,展示錨點并在指定時間后跳轉 3. 識別到多碼額時候,展示錨點并等待用戶點擊
可以看到,這里涉及到簡單的展示狀態切換,這里就引出改造的第二步:狀態模式
狀態模式是一種行為設計模式, 能在一個對象的內部狀態變化時改變其行為, 使其看上去就像改變了自身所屬的類一樣。
本文設計的狀態模式,包含兩部分:
- 狀態的信息 StateInfo
- 狀態的基類 BaseState
兩者結構如圖所示
? 狀態的信息 StateInfo
包含的功能和特點:
- 當前上下文僅有一種狀態信息流轉
- 業務方可以保存多個狀態鍵值對,狀態根據需要執行相應的代碼邏輯。
狀態信息的聲明和實現代碼示例如下
@interface TBBaseStateInfo : NSObject {
@private
TBBaseState<TBBaseStateDelegate> *_currentState; //記錄當前的 State
}
//使用當前的 State 執行
- (void)performAction;
//更新當前的 State
- (void)setState:(TBBaseState <TBBaseStateDelegate> *)state;
//獲取當前的 State
- (TBBaseState<TBBaseStateDelegate> *)getState;
@end
@implementation TBBaseStateInfo
- (void)performAction {
//當前狀態開始執行
[_currentState perfromAction:self];
}
- (void)setState:(TBBaseState <TBBaseStateDelegate> *)state {
_currentState = state;
}
- (TBBaseState<TBBaseStateDelegate> *)getState {
return _currentState;
}
@end
上層業務代碼示例如下
typedef NS_ENUM(NSInteger, TBStateType) {
TBStateTypeNormal, //空狀態
TBStateTypeSingleCode, //單碼展示態
TBStateTypeMultiCode, //多碼展示態
};
@interface TBStateInfo : TBBaseStateInfo
//以 key-value 的方式存儲業務 type 和對應的狀態 state
- (void)setState:(TBBaseState<TBBaseStateDelegate> *)state forType:(TBStateType)type;
//更新 type,并執行 state
- (void)setType:(TBStateType)type;
@end
@implementation TBStateInfo
- (void)setState:(TBBaseState<TBBaseStateDelegate> *)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 <NSObject>
- (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 | ... | ... | ... |
代理模式
在開發過程中,我們會在越來越多的地方使用到上圖能力,比如「淘寶拍照」的相冊中、「掃一掃」的相冊中,用到解碼、碼展示、碼處理的能力。
因此,我們需要把這些能力封裝并做成插件化,以便在任何地方都能夠使用。這里就引出了我們改造的第三步:代理模式。
代理模式是一種結構型設計模式,能夠提供對象的替代品或其占位符。代理控制著對于原對象的訪問, 并允許在將請求提交給對象前后進行一些處理。
本文設計的狀態模式,包含兩部分:
- 代理單例 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 <TBScanProtocol>)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)];
}
掃一掃新架構?
基于上述的改造優化,我們將原掃一掃架構進行了優化:將邏輯&展現層進行代碼分拆,分為展現層、邏輯層,接口層。以達到層次分明、職責清晰、解耦的目的。
總結
上述沉淀的三個設計模式作為掃拍業務的 Foundation 的 Public 能力,應用在鏡頭頁的業務邏輯中。
通過此次重構,提高了掃碼能力的復用性,結構和邏輯的清晰帶來的是維護成本的降低,不用再大海撈針從代碼“巨無霸”中尋找問題,降低了開發人日。