Files
knowledge-kit/Chapter1 - iOS/1.110.md
2023-09-13 18:22:07 +08:00

358 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 设计一个客户端校验器
> 订单在提交的时候会面临不同的校验规则,不同的校验规则会有不同的处理。假设这个处理就是弹窗。
>
> 有的时候会命中规则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 & !isChargedAccountB -> 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 顺序决定了弹窗优先级的高低(这一点不够痛)
## 方案
那能不能优化呢有2个思路责任链设计模式、工厂设计模式
### 责任链设计模式
采用责任链设计模式。基类 `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];
}
}
```
优点:
- 没有重复逻辑,判断方法都守口在基类中
- 优先级的关系维护在不同的子类中,各司其职,独立维护
最后选什么?组合优于继承,个人倾向使用责任链模式去组织。