条件型业务规则的抽象与实现——从Spring Profile得到的灵感


最近,有幸参与了一个平台型的项目,该平台支持多种类型的产品预订,并且对于不同的产品类型,支持不同的预订规则。开发团队想尽可能地将主流程实现得更通用,以便在将来更快速地支持新的产品类型。因此,团队决定在主流程中,以产品类型作为条件,决定是否应用某个给定的预订规则。

例如其中有一个对于配送地址的验证规则,它只对特定产品类型(火车票)生效:

(经过简化的用户故事——火车票预订)

作为用户,当我预订火车票时,我应该被告知配送地址无法送达,以便我调整配送地址或选择上门取票

该平台还支持预订酒店,不过由于没有凭据需要配送,所以并不需要检查配送地址是否可达。于是有了以下实现:

public class AddressIsAvailableToDelivery implements PlaceOrderRule {

    @Override
    public void verify(PlaceOrderCommand command) { 
        if (command.getProduct().isTypeOf(RAILWAY)) {
            // check if the adress is available for delivery the ticket
        } else {
            // hotel, makes no sense of deliering tickets
        }
    }
}

预订主流程会依次执行所有的PlaceOrderRule,并由各个PlaceOrderRule的实现决定需要对哪些产品生效。

几个迭代过后有了新的产品需要支持:观光景点,需要配送门票给用户,所以一个类似的用户故事诞生了:

(经过简化的用户故事——门票预订)

作为用户,当我预订景点门票时,我应该被告知配送地址无法送达,以便我调整配送地址或选择上门取票

于是,团队修改了条件表达式,增加了对门票景点的判断:

public class AddressIsAvailableToDelivery implements PlaceOrderRule {

    @Override
    public void verify(PlaceOrderCommand command) { 
        if (command.getProduct().isTypeOf(RAILWAY) || command.getProduct().isTypeOf(SIGHTSEEING)) {
            // check if the adress is available for delivery the ticket
        } else {
            // hotel, makes no sense of deliering tickets
        }
    }
}

到这里,我们闻到到了一些”坏味道”:随着需要验证地址是否达的产品类型增加,代码的圈复杂度会随之升高,意味着需要更多的测试用例来保护。如果将来再有一个新的类型需要检查配送地址是否可达,可以预见此处还会修改;如果系统中有越来越多的条件型业务规则使用当前的方式实现,系统将会越来越脆弱。

找到稳定的抽象

那么问题出在哪里?我认为这是由于没有找到正确的抽象,对于条件型的业务规则,其实是有稳定的步骤的:

  1. 检测当前情况是否需要验证给定的业务规则
  2. 如需要,执行验证;如不需要则略过

如果将AddressIsAvailableToDelivery修改为:

public class AddressIsAvailableToDelivery implements PlaceOrderRule {

    @Override
    public void verify(PlaceOrderCommand command) { 
        if (command.getProduct().isDeliverableAddressRequired()) {
            // check if the adress is available for delivery the ticket
        } else {
            // hotel, makes no sense of deliering tickets
        }
    }
}

这样,条件表达式依赖了稳定的抽象。代码不需要再关心产品类型了,当新的产品加入平台时,只需要知道该产品是否需要验证配送地址就行了。这样就做到了当新产品加入时,核心的规则验证逻辑不需要变更,系统更加稳定。

但这样好难用

工程师对这个重构感到满意,于是找到了BA(业务分析师),尝试对用户故事做一些变化

(经过简化的用户故事——产品预订)

  1. 作为用户,当我预订需要检查配送地址是否可达的产品时,我应该被告知配送地址无法送达,以便我调整配送地址或选择上门取票
  2. 作为运营人员,我可以设置产品在预订时是否需要检查配送地址,以避免预订后无法配送凭证的情况

BA对此提出了担心:

  1. 在这个实现方案中,平台运营团队需要为不同的产品设置不同的规则吗?如果规则数量很多,配置起来是不是很麻烦?因为对于某个产品类型,几乎不需要做规则的调整,要求运营团队去配置这些功能在现阶段反而使他们的工作变复杂了
  2. 平台运营团队在平时的工作中,还是按照产品类型的思维在工作的,他们更习惯于”如果产品类型是火车,那么。。。”这样的沟通方式,想要改变这样的思维方式不是那么容易
  3. 修改后的用户故事似乎太抽象了,这样能否帮助团队有效地理解真实的业务场景?

product-configuration

当有大量规则的时候,细粒度的产品配置方式确实有些繁琐,可能需要”配置专家”才能搞定

这些担忧不无道理,团队一下子陷入了两难的境地。

意外的灵感

我在阅读该项目一段配置代码的时候发现了这样一个细节:

if (isSmsEnabled()) {
   //enable sms sending
}

if (isEmailEnabled()) {
   //enable email sending
}



// application.properties
sms.enabled: false
email.enabled: false

// application-dev.properties
sms.enabled: false
email.enabled: false
  
// application-qa.properties
sms.enabled: false 
email.enabled: true
  
// application-prod.properties
sms.enabled: true 
email.enabled: true


这段代码表示,在不同的环境中,通过细粒度的配置项,可以精确地控制某个特定功能是否起效。配置项的控制范围很小,而且可能会有许多这样的配置项,但团队根据各个环境上的测试约定,将这些配置项归拢到以环境命名的配置文件中,这是spring boot提供的Profile机制。在启动应用的时候,并不需要一一指定各个配置项的值,而是指定粗粒度的profile即可: --spring.profiles.active=prod

这个方案给了我一个灵感:能否将之前的预订规则表达式类比为配置项,产品类型类比为Profile呢?

在这个思路下,我们保持AddressIsAvailableToDelivery依赖稳定的isDeliverableAddressRequired

public class AddressIsAvailableToDelivery implements PlaceOrderRule {

    @Override
    public void verify(PlaceOrderCommand command) { 
        if (command.getProduct().isDeliverableAddressRequired()) {
            // check if the adress is available for delivery the ticket
        } else {
            // hotel, makes no sense of deliering tickets
        }
    }
}

而在实例化Product时,注入预先设置的配置项,将产品类型和配置项的转换从核心的规则校验中剥离出去。

# railway
placeOrderRule.RAILWAY.deliverableAddressRequired=true
placeOrderRule.RAILWAY.anotherConstraint1=false
placeOrderRule.RAILWAY.anotherConstraint2=false
# sightseeing
placeOrderRule.SIGHTSEEING.deliverableAddressRequired=true
placeOrderRule.SIGHTSEEING.anotherConstraint1=false
placeOrderRule.SIGHTSEEING.anotherConstraint2=true

这样,既能让核心的规则校验依赖稳定的抽象,在变化时保持结构稳定,又暂时避免了给运营团队带来繁琐的配置工作。

遗留的问题

回顾这个过程,实在有些偶然,而且我认为我们只是用了最熟悉的技术手段暂时缓解了之前BA提出的第一点担心。

  1. 平台运营团队在平时的工作中,还是按照产品类型的思维在工作的,他们更习惯于”如果产品类型是火车,那么。。。”这样的沟通方式,想要改变这样的思维方式不是那么容易
  2. 修改后的用户故事感觉太抽象了,这样能否帮助团队有效地理解真实的业务场景?

而2、3则涉及到项目团队和干系人对产品的思考方式,当我们更倾向于使用具体的场景沟通的时候,团队更不容易意识到需要从中寻找稳定的抽象。那么我们需要花费精力去改变用户的思维方式吗,如果需要又应该使用什么样的方式?又或者我们需要使用更抽象的方式来撰写用户故事吗?在这里,想听听大家的意见。


Published on 05 June 2019.
Comments
Category: Ramblings


软件交付效能度量——从吞吐量和稳定性开始


cover

为了提升软件交付效能,你的团队或组织可能已经开始采取行动,引入敏捷方法、DevOps实践甚至还有架构重构。但你如何知道这些改进措施起了作用呢,或者它们压根就水土不服呢?简单来说,除了感性的工作体验外,你需要一些指标来度量交付效能。

唯快不破

提升交付效能的最重要的目标之一就是能”快起来”,那么如何定义”快”呢?我们更倾向于度量吞吐量,吞吐量是指单位时间内团队完成的工作量。

1.变更前置时间

Lead Time for Changes,变更前置时间指的是开始编码到部署所耗费的时间。如果变更前置时间过长,可能意味着开发或部署过程的某些环节出现了问题或阻碍。这是一个典型的吞吐量指标,更强调的是对于单个用户故事/用例需要花费多长时间才能获得实际反馈。

我之前受邀参与某个项目,当时团队的直观感受是进度滞后。通过度量变更前置时间,我们发现用户故事从进入”开发中”到”准备QA测试”(意味着开发同事已经完成了开发并按照验收标准进行了自行验证)的中位数时间是4.5天,这意味着近一半的用户故事在一个工作周内都不会得到有效的反馈,而在一周之后往往可以”惊喜”地发现实现方案有问题,需要返工。因此,我们把降低变更前置时间、增加吞吐量作为目标,帮助业务分析师和Tech Leader在保证INVEST原则的情况下进一步拆分故事卡作为手段,在三周之后将变更前置时间的中位数降低到2.5天。虽然更细粒度的故事卡带来了更频繁的开卡、关卡活动,看似造成了更多的工作量,但由此带来的频繁交流和目标拉通,降低了返工的可能,使得项目进度逐渐恢复正常。

2.部署频率

Deployment Frequency,部署频率,我认为这是吐吞量的另一种度量方式,更频繁的部署往往意味着单次部署包含的变更更少,但对于某个特性来说,可以更快地获得产生价值,获得实际反馈。而且,同变更前置时间相比,部署频率往往更直接地意味着团队/组织在工程实践方面有着良好的理解和应用。

唯快不破,并非只强调快

我曾经在一家传统企业工作,在没有敏捷方法和工程实践加持的情况下,我们也做到了每周一次发布。但几乎每次发布后,随之而来的是紧急发布,以修复发布后出现的各种问题。在一次对客服中心的拜访中,我了解到客服部门对IT部门的每周发布并没有什么好感,因为每次发布后都如临大敌,客户投诉可能呼啸而至。因此,我们在追求“快”的同时,还得保住“稳”,否则频繁出现故障的使用体验,甚至会抵消快速创新的努力。 那么,我们应该关注哪些“稳定性”指标呢?

3.变更失败率

Change Fail Rate,变更失败率是指发生变更后出现故障的几率。理论上来说,当我们引入敏捷方法和DevOps实践快速迭代时,随着时间的推移和实践及工具的成熟,单次部署/发布包含的变更项会逐渐减少,由此变更失败率也会最终降低。因此,如果变更失败率居高不下,可能意味着在过程或工程实践中出现了某些问题或阻碍

4.服务恢复耗时

Time to restore service,服务恢复耗时是指当服务中断或降级后,需要花费多少时间将服务恢复正常。每次部署包含的变更多寡、技术债务堆积程度对该指标有一定影响,但将该指标放在这里有一些争议,因为在一些合作模式中,造成故障的部分原因或修复措施并不在交付团队的工作范围内(例如基础设施)。

为什么优先度量这些指标

读到这里,你可能会发现以上四个关键指标来自于一份业界知名的DevOps报告,为什么在度量交付效能的时候,要优先考虑DevOps指标呢?

在19年4月的技术雷达中,是这样回答的

研究人员证实,只需四个关键指标就能区分低绩效、中绩效和高绩效人员:前置时间、部署频率、平 均修复时间(MTTR)和变更失败率。的确,我们已经发现这四个关键指标是个简单却强大的工具,可帮助领导和团队专注于衡量并改进重要的环节。

从另一个角度来说,这四个指标距离客户的直观感受更接近,这意味着它们更能体现真实的价值。同样的情况可以参考应用监控,在《Practical Monitoring》一书中,作者极力推荐优先从用户视角建立监控指标,这比底层指标更容易得出结论:例如响应延迟,是用户是能感受到的,延迟升高了可能意味着出现问题,但CPU使用率用户是感受不到的,其增高也未必说明问题,可能有些应用运行在高负载CPU是常态。

最后则是从成本考虑,避免在一开始的时候就规划并实施一个大而全的度量体系,从而付出不必要的管理成本

度量需要投入团队的时间,项目管理人员的时间,质量保障人员的时 间,以及公司管理人员的时间,还可能会有工具和基础设施的投入。围绕 各种目标需要度量体系的设计和实施,体系的运转需要数据的收集、分析 和汇报,这些环节做得不到位是不可能产生预期效果的,而要做到位,所 需的投入可能并不是一个可以忽略的小数目。因此,目标和指标的选择就 显得特别重要,试图实施一个大而全的度量体系,通常弊大于利。

​《精益软件度量》,度量不是什么

诊断型指标

如果说以上四个关键指标告诉我们的是交付效能的变化趋势,那么下一步,我们可以寻找更细粒度的指标来告诉我们如何进一步改进它们。

之前的四个关键指标更像体温计,它们可以快速地反映现在是否健康,但它们不能告诉我们病因。接下来我们需要的是验血报告,报告中有很多明细的指标,单个指标或许并不能说明什么,但若干异常指标的组合在一起往往可以帮助医生判断病灶,或许可以将这些明细指标称作”诊断型”指标。

常见的”诊断型”指标包括但不限于:

  1. 平均构建时间
  2. 测试覆盖率
  3. 代码圈复杂度
  4. 团队速率

以上这些(以及更多其他)指标从代码复杂度、测试策略、基础设施水平等更”底层”的维度解释交付效能健康或不健康的原因,它们可以帮助团队在做出进一步针对性的改进。

总结

  1. 除了感性的工作体验外,我们还需要指标来度量改进措施是否对提升软件交付效能有帮助。
  2. 过多的指标会对团队造成不必要的管理成本,也容易让团队失去关注焦点。
  3. 从吞吐量和稳定性两个维度考量的四个关键指标是简单但有效的指标,建议优先度量。

参考

Accelerate: State of DevOps 2018 Strategies for a New Economy, By DevOps Research & Assessment

https://www.thoughtworks.com/cn/radar/techniques/four-key-metrics

Practical Monitoring, By (author) Mike Julian

《精益软件度量》,作者: 张松


Published on 04 May 2019.
Comments
Category: Rambings


© Copyright2014-2019