## 设计一个客户端校验器 > 订单在提交的时候会面临不同的校验规则,不同的校验规则会有不同的处理。假设这个处理就是弹窗。 > > 有的时候会命中规则1,则弹窗1,有的时候同时命中规则1、2、3,但由于存在规则的优先级,则会处理优先级最高的弹窗1。 > > 老的业务背景下,弹窗优先级或者说校验规则是统一的。直接用函数翻译实现,写多个 if 问题不大。 > > 但在新业务背景下,不同的条件,弹窗优先级不一致,之前的写法需要写大量的嵌套判断,代码难以维护。 > > 所以问题抽象为:如何设计一个校验器 ## 问题背景 为了清晰说明问题,假设线上的弹窗校验规则为:A -> B -> C ```Plain typedef NS_ENUM(NSUInteger, OrderSubmitReminderType) { OrderSubmitReminderTypeNormal = 0, // 没有命中校验规则 OrderSubmitReminderTypeA, // 命中校验规则 A OrderSubmitReminderTypeB, // 命中校验规则 B OrderSubmitReminderTypeC, // 命中校验规则 C } ``` 老规则比较简单,不存在不同的校验规则,所以需求可以直接用代码翻译,不需要额外设计 ```Shell + (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 如果直接改,代码就是一坨垃圾了 ```Shell + (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个方法,照样很简洁 ```Shell + (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;` 用于处理校验,内部默认实现是传递给下一个校验器。 ```Shell //.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。 子类去重写父类方法 ```Shell OrderSubmitAValidator { - (BOOL)isValidate:(id)params { // 处理是否满足校验规则A } - (void)handleWhenCapture { // 当不满足条件规则的时候的处理逻辑 displayDialogA(); } } ``` 为了设计的健壮,假设没有命中任何校验规则,需要如何处理?这个能力需要有兜底默认的行为,比如打印日志:`NSLog(@"暂无命中任何弹窗类型,参数为:%@",params);` 也可以由业务方传递 ```Shell OrderSubmitDefaultValidator *defaultValidator = [OrderSubmitDefaultValidator validateWithBloock:^ { SafeBlock(self.deafaultHandler, params); if (!self.deafaultHandler) { NSLog(@"暂无命中任何弹窗类型,参数为:%@",params); } }]; ``` 初始化多个校验规则 ```Shell OrderSubmitAValidator *aValidator = [[OrderSubmitAValidator alloc] initWithParams:params]; OrderSubmitBValidator *bValidator = [[OrderSubmitBValidator alloc] initWithParams:params]; OrderSubmitCValidator *cValidator = [[OrderSubmitCValidator alloc] initWithParams:params]; ``` 不同优先级的校验如何指定: ```Shell if (isVIP) { if (isChargedAccount) { aValidator.nextValidator = bValidator; bValidator.nextValidator = cValidator; } else { bValidator.nextValidator = cValidator; cValidator.nextValidator = aValidator; } } else { cValidator.nextValidator = bValidator; bValidator.nextValidator = aValidator; } ``` 但还是不够优雅,这个优先级需要用户感知。能不能做到业务方只传递参数,内部判断命中什么弹窗优先级组合。所以接口可以设计为 ```Shell [OrderSubmitValidator validateWithParams:params handleWhenNotCapture:^{ NSLog(@"暂无命中任何弹窗类型,参数为:%@",params); }]; ``` 上述方法其实等价于 ```Shell 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 存在不同优先级顺序,如果按照现有的方案实施,则会存在很多冗余代码 ### 工厂设计模式 设计基类 ```Shell 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` 方法 ```Shell 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`,提供工厂初始化方法 ```Shell 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]; } } ``` 优点: - 没有重复逻辑,判断方法都守口在基类中 - 优先级的关系维护在不同的子类中,各司其职,独立维护 最后选什么?组合优于继承,个人倾向使用责任链模式去组织。