条件型业务规则的抽象与实现——从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洞见

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容

  • 谈一谈业务产品设计的一些思考。主要分了六点谈: 1、什么是业务产品? 2、业务产品的定位和目标? 3、业务产品的需...
    拾贰团长阅读 792评论 0 9
  • 在开始下面的话题之前,我们先看一看有赞原有的核心交易架构。 初步看去,这套架构方案似乎看不出什么问题。事实情况也这...
    万物皆有序和公式阅读 3,200评论 0 0
  • 一、核心账务的概述和术语定义 二...
    程序员阿牛阅读 1,434评论 0 5
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,893评论 2 7
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,046评论 0 4