Categories
程式開發

條件型業務規則的抽象與實現——從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. 修改後的用戶故事似乎太抽象了,這樣能否幫助團隊有效地理解真實的業務場景?

當有大量規則的時候,細粒度的產品配置方式確實有些繁瑣,可能需要“配置專家”才能搞定。

條件型業務規則的抽象與實現——從Spring Profile得到的靈感 1

(大量規則的時候,細粒度的產品配置方式可能需要”配置專家”才能搞定)

這些擔憂不無道理,團隊一下子陷入了兩難的境地。

意外的靈感

我在閱讀該項目一段配置代碼的時候發現了這樣一個細節:

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則涉及到項目團隊和乾係人對產品的思考方式,當我們更傾向於使用具體的場景溝通的時候,團隊更不容易意識到需要從中尋找穩定的抽象。那麼我們需要花費精力去改變用戶的思維方式嗎,如果需要又應該使用什麼樣的方式?又或者我們需要使用更抽象的方式來撰寫用戶故事嗎?在這裡,想听聽大家的意見。

作者介紹

周宇剛,擁有 10 年的 JAVA EE 開發經驗,在 ThoughtWorks 擔任高級諮詢師。在加入 ThoughtWorks 之前,在一家國內領先的航旅企業擔任架構師,專注於持續交付實踐和大型企業應用架構治理。

本文轉載自ThoughtWorks洞見。

原文鏈接

https://insights.thoughtworks.cn/identity-rule-abstraction-implementation/