From 0b37f97d50688e98da54177f8713316d73f27abd Mon Sep 17 00:00:00 2001 From: "binpeng.liu" Date: Wed, 13 Sep 2023 18:05:17 +0800 Subject: [PATCH] =?UTF-8?q?feature:=20=E5=A6=82=E4=BD=95=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E6=A0=A1=E9=AA=8C=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Chapter1 - iOS/1.110.md | 348 +++++++++++++++++++++++++++++++++++++ Chapter1 - iOS/chapter1.md | 3 +- SUMMARY.md | 1 + 3 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 Chapter1 - iOS/1.110.md diff --git a/Chapter1 - iOS/1.110.md b/Chapter1 - iOS/1.110.md new file mode 100644 index 0000000..c734e76 --- /dev/null +++ b/Chapter1 - iOS/1.110.md @@ -0,0 +1,348 @@ +## 设计一个客户端校验器 + +> 订单在提交的时候会面临不同的校验规则,不同的校验规则会有不同的处理。假设这个处理就是弹窗。 +> +> 有的时候会命中规则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 顺序决定了弹窗优先级的高低(这一点不够痛) + +## 方案 + +那能不能优化呢?有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]; +``` + +优点: + +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]; + } +} +``` + +优点: + +- 没有重复逻辑,判断方法都守口在基类中 +- 优先级的关系维护在不同的子类中,各司其职,独立维护 + +最后选什么?组合优于继承,个人倾向使用责任链模式去组织。 \ No newline at end of file diff --git a/Chapter1 - iOS/chapter1.md b/Chapter1 - iOS/chapter1.md index 915502a..8843b19 100644 --- a/Chapter1 - iOS/chapter1.md +++ b/Chapter1 - iOS/chapter1.md @@ -112,4 +112,5 @@ * [106、NSUserDefault 底层原理探究](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.106.md) * [107、IM技术](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.107.md) * [108、精准测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.108.md) - * [109、汇编学习](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.109.md) \ No newline at end of file + * [109、汇编学习](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.109.md) + * [110、设计一个客户端校验器](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.110.md) \ No newline at end of file diff --git a/SUMMARY.md b/SUMMARY.md index f0c9892..049f2dd 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -112,6 +112,7 @@ * [107、IM技术](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.107.md) * [108、精准测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.108.md) * [109、汇编学习](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.109.md) + * [110、设计一个客户端校验器](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.110.md) * [Chapter2 - Web FrontEnd](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/chapter2.md) * [1、-last-child与-last-of-type你只是会用,有研究过区别吗?](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.1.md) * [2、正则表达式](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter2%20-%20Web%20FrontEnd/2.2.md)