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

条件型业务规则的抽象与实现——从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. 修改后的用户故事似乎太抽象了,这样能否帮助团队有效地理解真实的业务场景?

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

(大量规则的时候,细粒度的产品配置方式可能需要”配置专家”才能搞定)

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


意外的灵感

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

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

本文分享自微信公众号 - ThoughtWorks洞见(TW-Insights),作者:周宇刚

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 致:有志成为卓越数字产品经理的你!

    当你点开这个链接时,想必你已经了解了什么是“数字产品经理”,如果还没有,我们建议你可以先看一看《数字产品经理的培养》,或者直接看我们为你摘取的这部分:

    ThoughtWorks
  • 银行移动产品从团队敏捷走向产品敏捷 | 洞见

    中国银行业的数字化转型刚刚拉开帷幕,移动产品成为了中国银行业的新战场。为在新战场占有一席之地,各家银行开始纷纷尝试自己移动产品的敏捷转型,更有甚者开始重新组建I...

    ThoughtWorks
  • GoCD的正确打开方式|洞见

    事件:ThoughtWorks在2016年11月发布的技术雷达中将“Jenkins as a deployment pipeline”列为了“暂缓”。 Jenk...

    ThoughtWorks
  • Java设计模式透析之 —— 单例(Singleton)

    Tanyboye
  • 腾讯CDC团队:前端异常监控解决方案

    前端监控包括行为监控、异常监控、性能监控等,本文主要讨论异常监控。对于前端而言,和后端处于同一个监控系统中,前端有自己的监控方案,后端也有自己等监控方案,但两者...

    前端劝退师
  • 前端异常监控解决方案研究

    前端监控包括行为监控、异常监控、性能监控等,本文主要讨论异常监控。对于前端而言,和后端处于同一个监控系统中,前端有自己的监控方案,后端也有自己等监控方案,但两者...

    Fundebug
  • Python 项目实践三(Web应用程序)第四篇

    接着上节继续学习,本章将建立用户账户 Web应用程序的核心是让任何用户都能够注册账户并能够使用它,不管用户身处何方。在本章中,你将创建一些表单,让用户能够添加主...

    用户1198337
  • 云化数据中心容灾解决方案

    时至今日,企业运作和业务运营对于IT系统的依赖性越来越高,对于IT系统的稳定性和可靠性的要求也越来越高。然而,"天有不测风云,人有旦夕祸福",一旦IT系统因为天...

    用户3954264
  • Raft协议精解

    这个和日志复制的机制有关系。首先对于选举,PK的条件不是拼这两个索引值的大小,PK的是最后一条日志的任期号和日志的长度。Leader当选后进行第一次日志复制时,...

    老钱
  • Docker logs 查看实时日志(日志最后的N行、某刻后日志)

    当我们输入 docker logs 的时候会转化为 Docker Client 向 Docker Daemon 发起请求,。

    微风-- 轻许--

扫码关注云+社区

领取腾讯云代金券