摘要
當(dāng)我們更傾向于使用具體的場景溝通的時候,團(tuán)隊(duì)更不容易意識到需要從中尋找穩(wěn)定的抽象。那么我們需要花費(fèi)精力去改變用戶的思維方式嗎,如果需要又應(yīng)該使用什么樣的方式?又或者我們需要使用更抽象的方式來撰寫用戶故事嗎?
最近,有幸參與了一個平臺型的項(xiàng)目,該平臺支持多種類型的產(chǎn)品預(yù)訂,并且對于不同的產(chǎn)品類型,支持不同的預(yù)訂規(guī)則。開發(fā)團(tuán)隊(duì)想盡可能地將主流程實(shí)現(xiàn)得更通用,以便在將來更快速地支持新的產(chǎn)品類型。因此,團(tuán)隊(duì)決定在主流程中,以產(chǎn)品類型作為條件,決定是否應(yīng)用某個給定的預(yù)訂規(guī)則。
例如其中有一個對于配送地址的驗(yàn)證規(guī)則,它只對特定產(chǎn)品類型(火車票)生效:
(經(jīng)過簡化的用戶故事——火車票預(yù)訂)
作為用戶,當(dāng)我預(yù)訂火車票時,我應(yīng)該被告知配送地址無法送達(dá),以便我調(diào)整配送地址或選擇上門取票
該平臺還支持預(yù)訂酒店,不過由于沒有憑據(jù)需要配送,所以并不需要檢查配送地址是否可達(dá)。于是有了以下實(shí)現(xiàn):
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
}
}
}
預(yù)訂主流程會依次執(zhí)行所有的PlaceOrderRule,并由各個PlaceOrderRule的實(shí)現(xiàn)決定需要對哪些產(chǎn)品生效。
幾個迭代過后有了新的產(chǎn)品需要支持:觀光景點(diǎn),需要配送門票給用戶,所以一個類似的用戶故事誕生了:
(經(jīng)過簡化的用戶故事——門票預(yù)訂)
作為用戶,當(dāng)我預(yù)訂景點(diǎn)門票時,我應(yīng)該被告知配送地址無法送達(dá),以便我調(diào)整配送地址或選擇上門取票
于是,團(tuán)隊(duì)修改了條件表達(dá)式,增加了對門票景點(diǎn)的判斷:
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
}
}
}
到這里,我們聞到到了一些”壞味道”:隨著需要驗(yàn)證地址是否達(dá)的產(chǎn)品類型增加,代碼的圈復(fù)雜度會隨之升高,意味著需要更多的測試用例來保護(hù)。如果將來再有一個新的類型需要檢查配送地址是否可達(dá),可以預(yù)見此處還會修改;如果系統(tǒng)中有越來越多的條件型業(yè)務(wù)規(guī)則使用當(dāng)前的方式實(shí)現(xiàn),系統(tǒng)將會越來越脆弱。
找到穩(wěn)定的抽象
那么問題出在哪里?我認(rèn)為這是由于沒有找到正確的抽象,對于條件型的業(yè)務(wù)規(guī)則,其實(shí)是有穩(wěn)定的步驟的:
- 檢測當(dāng)前情況是否需要驗(yàn)證給定的業(yè)務(wù)規(guī)則
- 如需要,執(zhí)行驗(yàn)證;如不需要則略過
如果將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
}
}
}
這樣,條件表達(dá)式依賴了穩(wěn)定的抽象。代碼不需要再關(guān)心產(chǎn)品類型了,當(dāng)新的產(chǎn)品加入平臺時,只需要知道該產(chǎn)品是否需要驗(yàn)證配送地址就行了。這樣就做到了當(dāng)新產(chǎn)品加入時,核心的規(guī)則驗(yàn)證邏輯不需要變更,系統(tǒng)更加穩(wěn)定。
但這樣好難用
工程師對這個重構(gòu)感到滿意,于是找到了BA(業(yè)務(wù)分析師),嘗試對用戶故事做一些變化
(經(jīng)過簡化的用戶故事——產(chǎn)品預(yù)訂)
- 作為用戶,當(dāng)我預(yù)訂需要檢查配送地址是否可達(dá)的產(chǎn)品時,我應(yīng)該被告知配送地址無法送達(dá),以便我調(diào)整配送地址或選擇上門取票
- 作為運(yùn)營人員,我可以設(shè)置產(chǎn)品在預(yù)訂時是否需要檢查配送地址,以避免預(yù)訂后無法配送憑證的情況
BA對此提出了擔(dān)心:
- 在這個實(shí)現(xiàn)方案中,平臺運(yùn)營團(tuán)隊(duì)需要為不同的產(chǎn)品設(shè)置不同的規(guī)則嗎?如果規(guī)則數(shù)量很多,配置起來是不是很麻煩?因?yàn)閷τ谀硞€產(chǎn)品類型,幾乎不需要做規(guī)則的調(diào)整,要求運(yùn)營團(tuán)隊(duì)去配置這些功能在現(xiàn)階段反而使他們的工作變復(fù)雜了
- 平臺運(yùn)營團(tuán)隊(duì)在平時的工作中,還是按照產(chǎn)品類型的思維在工作的,他們更習(xí)慣于”如果產(chǎn)品類型是火車,那么。。?!边@樣的溝通方式,想要改變這樣的思維方式不是那么容易
- 修改后的用戶故事似乎太抽象了,這樣能否幫助團(tuán)隊(duì)有效地理解真實(shí)的業(yè)務(wù)場景?
當(dāng)有大量規(guī)則的時候,細(xì)粒度的產(chǎn)品配置方式確實(shí)有些繁瑣,可能需要“配置專家”才能搞定。
(大量規(guī)則的時候,細(xì)粒度的產(chǎn)品配置方式可能需要”配置專家”才能搞定)
這些擔(dān)憂不無道理,團(tuán)隊(duì)一下子陷入了兩難的境地。
意外的靈感
我在閱讀該項(xiàng)目一段配置代碼的時候發(fā)現(xiàn)了這樣一個細(xì)節(jié):
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
這段代碼表示,在不同的環(huán)境中,通過細(xì)粒度的配置項(xiàng),可以精確地控制某個特定功能是否起效。配置項(xiàng)的控制范圍很小,而且可能會有許多這樣的配置項(xiàng),但團(tuán)隊(duì)根據(jù)各個環(huán)境上的測試約定,將這些配置項(xiàng)歸攏到以環(huán)境命名的配置文件中,這是spring boot提供的Profile機(jī)制。在啟動應(yīng)用的時候,并不需要一一指定各個配置項(xiàng)的值,而是指定粗粒度的profile即可: --spring.profiles.active=prod
這個方案給了我一個靈感:能否將之前的預(yù)訂規(guī)則表達(dá)式類比為配置項(xiàng),產(chǎn)品類型類比為Profile呢?
在這個思路下,我們保持AddressIsAvailableToDelivery依賴穩(wěn)定的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
}
}
}
而在實(shí)例化Product時,注入預(yù)先設(shè)置的配置項(xiàng),將產(chǎn)品類型和配置項(xiàng)的轉(zhuǎn)換從核心的規(guī)則校驗(yàn)中剝離出去。
# 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
這樣,既能讓核心的規(guī)則校驗(yàn)依賴穩(wěn)定的抽象,在變化時保持結(jié)構(gòu)穩(wěn)定,又暫時避免了給運(yùn)營團(tuán)隊(duì)帶來繁瑣的配置工作。
遺留的問題
回顧這個過程,實(shí)在有些偶然,而且我認(rèn)為我們只是用了最熟悉的技術(shù)手段暫時緩解了之前BA提出的第一點(diǎn)擔(dān)心。
- 平臺運(yùn)營團(tuán)隊(duì)在平時的工作中,還是按照產(chǎn)品類型的思維在工作的,他們更習(xí)慣于”如果產(chǎn)品類型是火車,那么。。?!边@樣的溝通方式,想要改變這樣的思維方式不是那么容易。
- 修改后的用戶故事感覺太抽象了,這樣能否幫助團(tuán)隊(duì)有效地理解真實(shí)的業(yè)務(wù)場景?
而2、3則涉及到項(xiàng)目團(tuán)隊(duì)和干系人對產(chǎn)品的思考方式,當(dāng)我們更傾向于使用具體的場景溝通的時候,團(tuán)隊(duì)更不容易意識到需要從中尋找穩(wěn)定的抽象。那么我們需要花費(fèi)精力去改變用戶的思維方式嗎,如果需要又應(yīng)該使用什么樣的方式?又或者我們需要使用更抽象的方式來撰寫用戶故事嗎?在這里,想聽聽大家的意見
更多精彩內(nèi)容,請關(guān)注 ThoughtWorks洞見
