Skip to content

Latest commit

 

History

History
405 lines (309 loc) · 14.4 KB

1.110.md

File metadata and controls

405 lines (309 loc) · 14.4 KB

妙用设计模式来设计一个客户端校验器

业务逻辑千变万化,弹窗优先级不断改变,代码冗余问题和难以维护问题如何解决? 本篇文章从设计模式角度出发,讨论责任链设计模式和工厂设计模式2个方式,如何去设计一个校验器,同时解决代码冗余和难以维护的问题

问题背景

订单在提交的时候会面临不同的校验规则,不同的校验规则会有不同的处理。假设这个处理就是弹窗。 有的时候会命中规则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 了

责任链设计模式

责任链模式即 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; 用于处理校验,内部利用模版模式,默认实现是传递给下一个校验器
//.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],
}

这部分策略的生成也可以单独抽取出去,比如 ValidateStrategyFactory 去根据不同的信息,生成不同的策略。

优点:

  1. 解决了现在的错误弹窗的隐含逻辑,后续人接手,弹窗优先级清晰可见,提高可维护性,减少出错概率
  2. 对于判断(校验)的增减都无需关心其他的校验规则。类似维护链表,仅在一开始指定即可,符合“开闭原则”
  3. 对于现有校验规则的修改足够收口,每个规则都有自己的 validator 和 validate 方法
  4. 目前弹窗优先级针对 isVIP、isCharged 存在不同优先级顺序,如果按照现有的方案实施,则会存在很多冗余代码
  5. 按照策略模式,不同的校验规则,组装不同的策略,也可以单独抽取出去,独立维护,更清晰
  6. validate 内部按照模版模式,调用 isValidate 方法,每个单独的 Validator 不需要额外去调用 next,设计更加健壮,防止别人漏写

工厂设计模式

设计基类

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];
    }
}

优点:

  • 没有重复逻辑,判断方法都守口在基类中
  • 优先级的关系维护在不同的子类中,各司其职,独立维护

最后选什么?组合优于继承,个人倾向使用责任链模式去组织代码。关于责任链设计模式的文章也可以看这篇文章