Files
knowledge-kit/Chapter1 - iOS/1.110.md
2023-10-05 11:04:12 +08:00

401 lines
14 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 顺序决定了弹窗优先级的高低(这一点不够痛)
## 方案
那能不能优化呢有3个思路责任链设计模式、工厂设计模式、策略模式
策略模式:当需要根据客户端的条件选择算法、策略时,可用该模式,客户端会根据条件选择合适的算法或策略,并将其传递给使用它的对象。典型设计前端 Vue-Validator form 各种 rules
职责链模式:当需要根据请求的内容选择处理器时,可用该模式,请求会沿着链传递,直到被处理,如 Node 洋葱模型
不过目前来看,策略模式被 Pass 了
### 责任链设计模式
责任链模式即 Chain Of Responsibility属于行为型模式。行为型模式不仅秒死对象或类的模式还描述他们之间的通信模式比如对操作的处理该如何传递等等。
为什么会有这个思路?
主要来源于2个方向Node 的洋葱模式、移动端的点击事件传递。
移动端的事件响应模型:点击 view 看看能不能响应,不能响应则继续向上抛,知道抛到 window 为止;
前端 JS 事件冒泡机制:点击事件假设是动态绑定到 DOM 节点上的,浏览器本身不知道哪些地方会处理点击事件,但又要让每层 DOM 拥有对该点击事件的平等处理权,所以就诞生了事件冒泡和组织冒泡的能力 `event.stopPropagation()`
Node 洋葱模式:发送一个 Request 一层层中间件去处理,比如添加日志、添加请求拦截转发、处理核心业务逻辑、添加日志、添加自定义 response header等一个中间件层只关注聚焦自己层需要做的事情处理完继续向下一层抛。
设想下如果没有中间价模型,假设实现一个记录请求事件和自定义 HTTP Header 的需求,业务逻辑 curd 代码和记录请求时间和自定义 Header 代码全都杂糅在一起,难以维护。
责任链的核心就是:**使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。**
- 降低处理者对象之间的耦合度。一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
- 增强了系统的可扩展性。可以根据业务需求增加或者调整新的请求处理类,满足开闭原则(类似维护链表的节点信息)
- 可插拔,增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
- 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
- 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
采用责任链设计模式。基类 `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];
}
}
```
优点:
- 没有重复逻辑,判断方法都守口在基类中
- 优先级的关系维护在不同的子类中,各司其职,独立维护
最后选什么?组合优于继承,个人倾向使用责任链模式去组织代码。