妙用設計模式來設計一個校驗器

發表於2023-09-19

訂單在提交的時候會面臨不同的校驗規則,不同的校驗規則會有不同的處理。假設這個處理就是彈窗。

有的時候會命中規則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],
}

優點:

  1. 解決了現在的錯誤彈窗的隱含邏輯,後續人接手,彈窗優先順序清晰可見,提高可維護性,減少出錯機率
  2. 對於判斷(校驗)的增減都無需關心其他的校驗規則。類似維護連結串列,僅在一開始指定即可,符合“開閉原則
  3. 對於現有校驗規則的修改足夠收口,每個規則都有自己的 validator 和 validate 方法
  4. 目前彈窗優先順序針對 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個不同的子類:OrderSubmitAValidatorOrderSubmitBValidatorOrderSubmitCValidator。各自重寫 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];
    }
}

優點:

  • 沒有重複邏輯,判斷方法都守口在基類中
  • 優先順序的關係維護在不同的子類中,各司其職,獨立維護

最後選什麼?組合優於繼承,個人傾向使用責任鏈模式去組織。

相關文章