訂單在提交的時候會面臨不同的校驗規則,不同的校驗規則會有不同的處理。假設這個處理就是彈窗。
有的時候會命中規則1,則彈窗1,有的時候同時命中規則1、2、3,但由於存在規則的優先順序,則會處理優先順序最高的彈窗1。
老的業務背景下,彈窗優先順序或者說校驗規則是統一的。直接用函式翻譯實現,寫多個 if 問題不大。
但在新業務背景下,不同的條件,彈窗優先順序不一致,之前的寫法需要寫大量的巢狀判斷,程式碼難以維護。
所以問題抽象為:如何設計一個校驗器
問題背景
為了清晰說明問題,假設線上的彈窗校驗規則為:A -> B -> C
typedef NS_ENUM(NSUInteger, OrderSubmitReminderType) {
OrderSubmitReminderTypeNormal = 0, // 沒有命中校驗規則
OrderSubmitReminderTypeA, // 命中校驗規則 A
OrderSubmitReminderTypeB, // 命中校驗規則 B
OrderSubmitReminderTypeC, // 命中校驗規則 C
}
老規則比較簡單,不存在不同的校驗規則,所以需求可以直接用程式碼翻譯,不需要額外設計
+ (OrderSubmitReminderType)acquireOrderValidateType:(id)params {
if ([OrderSubmitUtils validateA:params]) {
return OrderSubmitReminderTypeA;
}
if ([OrderSubmitUtils validateB:params]) {
return OrderSubmitReminderTypeB;
}
if ([OrderSubmitUtils validateC:params]) {
return OrderSubmitReminderTypeC;
}
return OrderSubmitReminderTypeNormal;
}
假設只有2個彈窗條件:是否是 VIP 賬戶(isVIP)、是否是付費使用者(isChargedAccount)。
- isVIP & isChargedAccount: A -> B -> C
- isVIP & !isChargedAccount:B -> C-> A
- !isVIP: C -> B -> A
如果直接改,程式碼就是一坨垃圾了
+ (OrderSubmitReminderType)acquireOrderValidateType:(id)params {
if (isVIP) {
if (isChargedAccount) {
if ([OrderSubmitUtils validateA:params]) {
return OrderSubmitReminderTypeA;
}
if ([OrderSubmitUtils validateB:params]) {
return OrderSubmitReminderTypeB;
}
if ([OrderSubmitUtils validateC:params]) {
return OrderSubmitReminderTypeC;
}
return OrderSubmitReminderTypeNormal;
} else {
if ([OrderSubmitUtils validateB:params]) {
return OrderSubmitReminderTypeB;
}
if ([OrderSubmitUtils validateC:params]) {
return OrderSubmitReminderTypeC;
}
if ([OrderSubmitUtils validateA:params]) {
return OrderSubmitReminderTypeA;
}
return OrderSubmitReminderTypeNormal;
}
} else {
if ([OrderSubmitUtils validateC:params]) {
return OrderSubmitReminderTypeC;
}
if ([OrderSubmitUtils validateB:params]) {
return OrderSubmitReminderTypeB;
}
if ([OrderSubmitUtils validateA:params]) {
return OrderSubmitReminderTypeA;
}
return OrderSubmitReminderTypeNormal;
}
}
思路
可能有些人會覺得,那不簡單,我將不同組合條件下的彈窗抽取為3個方法,照樣很簡潔
+ (OrderSubmitReminderType)acquireOrderValidateTypeWhenIsVIPAndChargedAccount:(id)params {
// A->B->C
if ([OrderSubmitUtils validateA:params]) {
return OrderSubmitReminderTypeA;
}
if ([OrderSubmitUtils validateB:params]) {
return OrderSubmitReminderTypeB;
}
if ([OrderSubmitUtils validateC:params]) {
return OrderSubmitReminderTypeC;
}
return OrderSubmitReminderTypeNormal;
}
+ (OrderSubmitReminderType)acquireOrderValidateTypeWhenIsVIPAndNotChargedAccount:(id)params {
// B -> C-> A
if ([OrderSubmitUtils validateB:params]) {
return OrderSubmitReminderTypeB;
}
if ([OrderSubmitUtils validateC:params]) {
return OrderSubmitReminderTypeC;
}
if ([OrderSubmitUtils validateA:params]) {
return OrderSubmitReminderTypeA;
}
return OrderSubmitReminderTypeNormal;
}
+ (OrderSubmitReminderType)acquireOrderValidateTypeWhenIsNotVIP:(id)params {
// C -> B-> A
if ([OrderSubmitUtils validateC:params]) {
return OrderSubmitReminderTypeC;
}
if ([OrderSubmitUtils validateB:params]) {
return OrderSubmitReminderTypeB;
}
if ([OrderSubmitUtils validateA:params]) {
return OrderSubmitReminderTypeA;
}
return OrderSubmitReminderTypeNormal;
}
其實不然,問題還是很多:
- 雖然抽取為不同方法,但是每個方法內部存在大量冗餘程式碼,因為每個校驗規則的程式碼是一樣的,重複存在,只不過先後順序不同
- 存在隱含邏輯。 return 順序決定了彈窗優先順序的高低(這一點不夠痛)
方案
那能不能最佳化呢?有3個思路:責任鏈設計模式、工廠設計模式、策略模式
策略模式:當需要根據客戶端的條件選擇演演算法、策略時,可用該模式,客戶端會根據條件選擇合適的演演算法或策略,並將其傳遞給使用它的物件。典型設計前端 Vue-Validator form 各種 rules
職責鏈模式:當需要根據請求的內容選擇處理器時,可用該模式,請求會沿著鏈傳遞,直到被處理,如 Node 洋蔥模型
不過目前來看,策略模式被 Pass 了
責任鏈設計模式
採用責任鏈設計模式。基類 OrderSubmitBaseValidator
宣告介面,是一個抽象類:
- 有一個屬性
nextValidator
用於指向下一個校驗器 - 有一個方法
- (void)validate:(id)params;
用於處理校驗,內部預設實現是傳遞給下一個校驗器。
//.h
OrderSubmitBaseValidator {
@property nextValidator;
- (void)validate:(id)params;
- (BOOL)isValidate:(id)params;
- (void)handleWhenCapture;
}
// .m
#pragma mark - public Method
- (BOOL)isValidate:(id)params {
Assert(0, @"must override by subclass");
return NO;
}
- (void)validate:(id)params {
BOOL isValid = [self isValidate:params];
if (isValid) {
[self.nextValidator validate:params];
} else {
[self handleWhenCapture];
}
}
- (void)handleWhenCapture {
Assert(0, @"must override by subclass");
}
然後針對不同的校驗規則宣告不同的子類,繼承自 OrderSubmitBaseValidator
。根據A、B、C 3個校驗規則,有:OrderSubmitAValidator、OrderSubmitBValidator、OrderSubmitCValidator。
子類去重寫父類方法
OrderSubmitAValidator {
- (BOOL)isValidate:(id)params {
// 處理是否滿足校驗規則A
}
- (void)handleWhenCapture {
// 當不滿足條件規則的時候的處理邏輯
displayDialogA();
}
}
為了設計的健壯,假設沒有命中任何校驗規則,需要如何處理?這個能力需要有兜底預設的行為,比如列印日誌:NSLog(@"暫無命中任何彈窗型別,引數為:%@",params);
也可以由業務方傳遞
OrderSubmitDefaultValidator *defaultValidator = [OrderSubmitDefaultValidator validateWithBloock:^ {
SafeBlock(self.deafaultHandler, params);
if (!self.deafaultHandler) {
NSLog(@"暫無命中任何彈窗型別,引數為:%@",params);
}
}];
初始化多個校驗規則
OrderSubmitAValidator *aValidator = [[OrderSubmitAValidator alloc] initWithParams:params];
OrderSubmitBValidator *bValidator = [[OrderSubmitBValidator alloc] initWithParams:params];
OrderSubmitCValidator *cValidator = [[OrderSubmitCValidator alloc] initWithParams:params];
不同優先順序的校驗如何指定:
if (isVIP) {
if (isChargedAccount) {
aValidator.nextValidator = bValidator;
bValidator.nextValidator = cValidator;
} else {
bValidator.nextValidator = cValidator;
cValidator.nextValidator = aValidator;
}
} else {
cValidator.nextValidator = bValidator;
bValidator.nextValidator = aValidator;
}
但還是不夠優雅,這個優先順序需要使用者感知。能不能做到業務方只傳遞引數,內部判斷命中什麼彈窗優先順序組合。所以介面可以設計為
[OrderSubmitValidator validateWithParams:params handleWhenNotCapture:^{
NSLog(@"暫無命中任何彈窗型別,引數為:%@",params);
}];
上述方法其實等價於
let validateType = [OrderSubmitValidator generateTypeWithParams:params];
[OrderSubmitValidator validateWith:validateType];
validateWith
方法內部根據 validateType 去組裝 Map 的 key,然後從 Map 中取出具體規則組合,然後依次迭代遍歷執行
let rulesMap = {
isVIP && isCharged : [a-b-c-d],
isVIP && !isCharged: [a-b-d-c],
!isVIP: [a-c-d-b],
}
優點:
- 解決了現在的錯誤彈窗的隱含邏輯,後續人接手,彈窗優先順序清晰可見,提高可維護性,減少出錯機率
- 對於判斷(校驗)的增減都無需關心其他的校驗規則。類似維護連結串列,僅在一開始指定即可,符合“開閉原則
- 對於現有校驗規則的修改足夠收口,每個規則都有自己的 validator 和 validate 方法
- 目前彈窗優先順序針對 EVA 、BTC 存在不同優先順序順序,如果按照現有的方案實施,則會存在很多冗餘程式碼
工廠設計模式
設計基類
OrderSubmitBaseValidator {
- (void)validate;
- (BOOL)validateA;
- (BOOL)validateB;
- (BOOL)validateC;
}
- (void)validate {
Assert(0, @"must override by subclass");
}
- (BOOL)validateA {
// 判斷是否命中規則 A
}
- (BOOL)validateB {
// 判斷是否命中規則 B
}
- (BOOL)validateC {
// 判斷是否命中規則 C
}
根據不同的彈窗優先順序條件,宣告3個不同的子類:OrderSubmitAValidator
、OrderSubmitBValidator
、OrderSubmitCValidator
。各自重寫 validate
方法
OrderSubmitAValidator {
- (void)validate {
[self validateA];
[self validateB];
[self validateC];
}
}
OrderSubmitBValidator {
- (void)validate {
[self validateB];
[self validateC];
[self validateA];
}
}
OrderSubmitCValidator {
- (void)validate {
[self validateC];
[self validateB];
[self validateA];
}
}
設計工廠類OrderSumitValidatorFactory
,提供工廠初始化方法
OrderSumitValidatorFactory {
+ (OrderSubmitBaseValidator *)generateValidatorWithParams:(id)params;
}
+ (OrderSubmitBaseValidator *)generateValidatorWithParams:(id)params {
if (isVIP) {
if (isChargedAccount) {
return [[OrderSubmitAValidator alloc] initWithParams:params];
} else {
return [[OrderSubmitBValidator alloc] initWithParams:params];
}
} else {
return [[OrderSubmitCValidator alloc] initWithParams:params];
}
}
優點:
- 沒有重複邏輯,判斷方法都守口在基類中
- 優先順序的關係維護在不同的子類中,各司其職,獨立維護
最後選什麼?組合優於繼承,個人傾向使用責任鏈模式去組織。