天天看點

一文教會你如何寫複雜業務的代碼

作者:張建飛

文章來源:微信公衆号"從碼農到工匠"

了解我的人都知道,我一直在緻力于應用架構和代碼複雜度的治理。

這兩天在看零售通商品域的代碼。面對零售通如此複雜的業務場景,如何在架構和代碼層面進行應對,是一個新課題。針對該命題,我進行了比較細緻的思考和研究。結合實際的業務場景,我沉澱了一套“如何寫複雜業務代碼”的方法論,在此分享給大家。

我相信,同樣的方法論可以複制到大部分複雜業務場景。

一個複雜業務的處理過程

業務背景

簡單的介紹下業務背景,零售通是給線下小店供貨的B2B模式,我們希望通過數字化重構傳統供應鍊管道,提升供應鍊效率,為新零售助力。阿裡在中間是一個平台角色,提供的是Bsbc中的service的功能。

一文教會你如何寫複雜業務的代碼

在商品域,營運會操作一個“上架”動作,上架之後,商品就能在零售通上面對小店進行銷售了。是零售通業務非常關鍵的業務操作之一,是以涉及很多的資料校驗和關聯操作。

針對上架,一個簡化的業務流程如下所示:

一文教會你如何寫複雜業務的代碼

過程分解

像這麼複雜的業務,我想應該沒有人會寫在一個service方法中吧。一個類解決不了,那就分治吧。

說實話,能想到分而治之的工程師,已經做的不錯了,至少比沒有分治思維要好很多。我也見過複雜程度相當的業務,連分解都沒有,就是一堆方法和類的堆砌。

不過,這裡存在一個問題:即很多同學過度的依賴工具或是輔助手段來實作分解。比如在我們的商品域中,類似的分解手段至少有3套以上,有自制的流程引擎,有依賴于資料庫配置的流程處理:

一文教會你如何寫複雜業務的代碼

本質上來講,這些輔助手段做的都是一個pipeline的處理流程,沒有其它。是以,我建議此處最好保持KISS(Keep It Simple and Stupid),即最好是什麼工具都不要用,次之是用一個極簡的Pipeline模式,最差是使用像流程引擎這樣的重方法。

除非你的應用有極強的流程可視化和編排的訴求,否則我非常不推薦使用流程引擎等工具。第一,它會引入額外的複雜度,特别是那些需要持久化狀态的流程引擎;第二,它會割裂代碼,導緻閱讀代碼的不順暢。大膽斷言一下,全天下估計80%對流程引擎的使用都是得不償失的。

回到商品上架的問題,這裡問題核心是工具嗎?是設計模式帶來的代碼靈活性嗎?顯然不是,問題的核心應該是如何分解問題和抽象問題,知道金字塔原理的應該知道,此處,我們可以使用結構化分解将問題解構成一個有層級的金字塔結構:

一文教會你如何寫複雜業務的代碼

按照這種分解寫的代碼,就像一本書,目錄和内容清晰明了。

以商品上架為例,程式的入口是一個上架指令(OnSaleCommand), 它由三個階段(Phase)組成。

@Command

public
 
class
 
OnSaleNormalItemCmdExe
 
{



    
@Resource

    
private
 
OnSaleContextInitPhase
 onSaleContextInitPhase
;

    
@Resource

    
private
 
OnSaleDataCheckPhase
 onSaleDataCheckPhase
;

    
@Resource

    
private
 
OnSaleProcessPhase
 onSaleProcessPhase
;



    
@Override

    
public
 
Response
 execute
(
OnSaleNormalItemCmd
 cmd
)
 
{



        
OnSaleContext
 onSaleContext 
=
 init
(
cmd
);



        checkData
(
onSaleContext
);



        process
(
onSaleContext
);



        
return
 
Response
.
buildSuccess
();

    
}



    
private
 
OnSaleContext
 init
(
OnSaleNormalItemCmd
 cmd
)
 
{

        
return
 onSaleContextInitPhase
.
init
(
cmd
);

    
}



    
private
 
void
 checkData
(
OnSaleContext
 onSaleContext
)
 
{

        onSaleDataCheckPhase
.
check
(
onSaleContext
);

    
}



    
private
 
void
 process
(
OnSaleContext
 onSaleContext
)
 
{

        onSaleProcessPhase
.
process
(
onSaleContext
);

    
}

}           

每個Phase又可以拆解成多個步驟(Step),以 OnSaleProcessPhase為例,它是由一系列Step組成的:

@Phase

public
 
class
 
OnSaleProcessPhase
 
{



    
@Resource

    
private
 
PublishOfferStep
 publishOfferStep
;

    
@Resource

    
private
 
BackOfferBindStep
 backOfferBindStep
;

    
//省略其它step



    
public
 
void
 process
(
OnSaleContext
 onSaleContext
){

        
SupplierItem
 supplierItem 
=
 onSaleContext
.
getSupplierItem
();



        
// 生成OfferGroupNo

        generateOfferGroupNo
(
supplierItem
);



       
// 釋出商品

        publishOffer
(
supplierItem
);



        
// 前後端庫存綁定 backoffer域

        bindBackOfferStock
(
supplierItem
);



        
// 同步庫存路由 backoffer域

        syncStockRoute
(
supplierItem
);



        
// 設定虛拟商品拓展字段

        setVirtualProductExtension
(
supplierItem
);



        
// 發貨保障打标 offer域

        markSendProtection
(
supplierItem
);



        
// 記錄變更内容ChangeDetail

        recordChangeDetail
(
supplierItem
);



        
// 同步供貨價到BackOffer

        syncSupplyPriceToBackOffer
(
supplierItem
);



        
// 如果是組合商品打标,寫擴充資訊

        setCombineProductExtension
(
supplierItem
);



        
// 去售罄标

        removeSellOutTag
(
offerId
);



        
// 發送領域事件

        fireDomainEvent
(
supplierItem
);



        
// 關閉關聯的待辦事項

        closeIssues
(
supplierItem
);

    
}

}           

看到了嗎,這就是商品上架這個複雜業務的業務流程。需要流程引擎嗎?不需要,需要設計模式支撐嗎?也不需要。對于這種業務流程的表達,簡單樸素的組合方法模式(Composed Method)是再合适不過的了。

是以,在做過程分解的時候,我建議工程師不要把太多精力放在工具上,放在設計模式帶來的靈活性上。而是應該多花時間在對問題分析,結構化分解,最後通過合理的抽象,形成合适的階段(Phase)和步驟(Step)上。

一文教會你如何寫複雜業務的代碼

過程分解後的兩個問題

的确,使用過程分解之後的代碼,已經比以前的代碼更清晰、更容易維護了。不過,還有兩個問題值得我們去關注一下:

1、領域知識被割裂肢解

什麼叫被肢解?因為我們到目前為止做的都是過程化拆解,導緻沒有一個聚合領域知識的地方。每個Use Case的代碼隻關心自己的處理流程,知識沒有沉澱。

相同的業務邏輯會在多個Use Case中被重複實作,導緻代碼重複度高,即使有複用,最多也就是抽取一個util,代碼對業務語義的表達能力很弱,進而影響代碼的可讀性和可了解性。

2、代碼的業務表達能力缺失

試想下,在過程式的代碼中,所做的事情無外乎就是取資料--做計算--存資料,在這種情況下,要如何通過代碼顯性化的表達我們的業務呢?說實話,很難做到,因為我們缺失了模型,以及模型之間的關系。脫離模型的業務表達,是缺少韻律和靈魂的。

舉個例子,在上架過程中,有一個校驗是檢查庫存的,其中對于組合品(CombineBackOffer)其庫存的處理會和普通品不一樣。原來的代碼是這麼寫的:

boolean
 isCombineProduct 
=
 supplierItem
.
getSign
().
isCombProductQuote
();



// supplier.usc warehouse needn't check

if
 
(
WarehouseTypeEnum
.
isAliWarehouse
(
supplierItem
.
getWarehouseType
()))
 
{

// quote warehosue check

if
 
(
CollectionUtil
.
isEmpty
(
supplierItem
.
getWarehouseIdList
())
 
&&
 
!
isCombineProduct
)
 
{

    
throw
 
ExceptionFactory
.
makeFault
(
ServiceExceptionCode
.
SYSTEM_ERROR
,
 
"親,不能釋出Offer,請聯系倉配營運人員,建立品倉關系!"
);

}

// inventory amount check

Long
 sellableAmount 
=
 
0L
;

if
 
(!
isCombineProduct
)
 
{

    sellableAmount 
=
 normalBiz
.
acquireSellableAmount
(
supplierItem
.
getBackOfferId
(),
 supplierItem
.
getWarehouseIdList
());

}
 
else
 
{

    
//組套商品

    
OfferModel
 backOffer 
=
 backOfferQueryService
.
getBackOffer
(
supplierItem
.
getBackOfferId
());

    
if
 
(
backOffer 
!=
 
null
)
 
{

        sellableAmount 
=
 backOffer
.
getOffer
().
getTradeModel
().
getTradeCondition
().
getAmountOnSale
();

    
}

}

if
 
(
sellableAmount 
<
 
1
)
 
{

    
throw
 
ExceptionFactory
.
makeFault
(
ServiceExceptionCode
.
SYSTEM_ERROR
,
 
"親,實倉庫存必須大于0才能釋出,請确認已補貨.\r[id:"
 
+
 supplierItem
.
getId
()
 
+
 
"]"
);

}

}           

然而,如果我們在系統中引入領域模型之後,其代碼會簡化為如下:

if
(
backOffer
.
isCloudWarehouse
()){

    
return
;

}



if
 
(
backOffer
.
isNonInWarehouse
()){

    
throw
 
new
 
BizException
(
"親,不能釋出Offer,請聯系倉配營運人員,建立品倉關系!"
);

}



if
 
(
backOffer
.
getStockAmount
()
 
<
 
1
){

    
throw
 
new
 
BizException
(
"親,實倉庫存必須大于0才能釋出,請确認已補貨.\r[id:"
 
+
 backOffer
.
getSupplierItem
().
getCspuCode
()
 
+
 
"]"
);

}           

有沒有發現,使用模型的表達要清晰易懂很多,而且也不需要做關于組合品的判斷了,因為我們在系統中引入了更加貼近現實的對象模型(CombineBackOffer繼承BackOffer),通過對象的多态可以消除我們代碼中的大部分的if-else。

一文教會你如何寫複雜業務的代碼

過程分解+對象模型

通過上面的案例,我們可以看到有過程分解要好于沒有分解,過程分解+對象模型要好于僅僅是過程分解。對于商品上架這個case,如果采用過程分解+對象模型的方式,最終我們會得到一個如下的系統結構:

一文教會你如何寫複雜業務的代碼

寫複雜業務的方法論

通過上面案例的講解,我想說,我已經交代了複雜業務代碼要怎麼寫:即自上而下的結構化分解+自下而上的面向對象分析。

接下來,讓我們把上面的案例進行進一步的提煉,形成一個可落地的方法論,進而可以泛化到更多的複雜業務場景。

上下結合

所謂上下結合,是指我們要結合自上而下的過程分解和自下而上的對象模組化,螺旋式的建構我們的應用系統。這是一個動态的過程,兩個步驟可以交替進行、也可以同時進行。

這兩個步驟是相輔相成的,上面的分析可以幫助我們更好的理清模型之間的關系,而下面的模型表達可以提升我們代碼的複用度和業務語義表達能力。

其過程如下圖所示:

一文教會你如何寫複雜業務的代碼

使用這種上下結合的方式,我們就有可能在面對任何複雜的業務場景,都能寫出幹淨整潔、易維護的代碼。

能力下沉

一般來說實踐DDD有兩個過程:

  1. 套概念階段

了解了一些DDD的概念,然後在代碼中“使用”Aggregation Root,Bonded Context,Repository等等這些概念。跟進一步,也會使用一定的分層政策。然而這種做法一般對複雜度的治理并沒有多大作用。

  1. 融會貫通階段

術語已經不再重要,了解DDD的本質是統一語言、邊界劃分和面向對象分析的方法。

大體上而言,我大概是在1.7的階段,因為有一個問題一直在困擾我,就是哪些能力應該放在Domain層,是不是按照傳統的做法,将所有的業務都收攏到Domain上,這樣做合理嗎?說實話,這個問題我一直沒有想清楚。

因為在現實業務中,很多的功能都是用例特有的(Use case specific)的,如果“盲目”的使用Domain收攏業務并不見得能帶來多大的益處。相反,這種收攏會導緻Domain層的膨脹過厚,不夠純粹,反而會影響複用性和表達能力。

鑒于此,我最近的思考是我們應該采用能力下沉的政策。

所謂的能力下沉,是指我們不強求一次就能設計出Domain的能力,也不需要強制讓把所有的業務功能都放到Domain層,而是采用實用主義的态度,即隻對那些需要在多個場景中需要被複用的能力進行抽象下沉,而不需要複用的,就暫時放在App層的Use Case裡就好了。

注:Use Case是《架構整潔之道》裡面的術語,簡單了解就是響應一個Request的處理過程

通過實踐,我發現這種循序漸進的能力下沉政策,應該是一種更符合實際、更靈活的方法。因為我們承認模型不是一次性設計出來的,而是疊代演化出來的。

下沉的過程如下圖所示,假設兩個use case中,我們發現uc1的step3和uc2的step1有類似的功能,我們就可以考慮讓其下沉到Domain層,進而增加代碼的複用性。

一文教會你如何寫複雜業務的代碼

指導下沉有兩個關鍵名額:代碼的複用性和内聚性。

複用性是告訴我們When(什麼時候該下沉了),即有重複代碼的時候。内聚性是告訴我們How(要下沉到哪裡),功能有沒有内聚到恰當的實體上,有沒有放到合适的層次上(因為Domain層的能力也是有兩個層次的,一個是Domain Service這是相對比較粗的粒度,另一個是Domain的Model這個是最細粒度的複用)。

比如,在我們的商品域,經常需要判斷一個商品是不是最小機關,是不是中包商品。像這種能力就非常有必要直接挂載在Model上。

public
 
class
 CSPU 
{

    
private
 
String
 code
;

    
private
 
String
 baseCode
;

    
//省略其它屬性



    
/**

     * 單品是否為最小機關。

     *

     */

    
public
 
boolean
 isMinimumUnit
(){

        
return
 
StringUtils
.
equals
(
code
,
 baseCode
);

    
}



    
/**

     * 針對中包的特殊處理

     *

     */

    
public
 
boolean
 isMidPackage
(){

        
return
 
StringUtils
.
equals
(
code
,
 midPackageCode
);

    
}

}           

之前,因為老系統中沒有領域模型,沒有CSPU這個實體。你會發現像判斷單品是否為最小機關的邏輯是以 StringUtils.equals(code,baseCode)的形式散落在代碼的各個角落。這種代碼的可了解性是可想而知的,至少在第一眼看到的時候,是完全不知道什麼意思。

業務技術要怎麼做

寫到這裡,我想順便回答一下很多業務技術同學的困惑,也是我之前的困惑:即業務技術到底是在做業務,還是做技術?業務技術的技術性展現在哪裡?

通過上面的案例,我們可以看到業務所面臨的複雜性并不亞于底層技術,要想寫好業務代碼也不是一件容易的事情。業務技術和底層技術人員唯一的差別是他們所面臨的問題域不一樣。

業務技術面對的問題域變化更多、面對的人更加龐雜。而底層技術面對的問題域更加穩定、但對技術的要求更加深。比如,如果你需要去開發Pandora,你就要對Classloader有更加深入的了解才行。

但是,不管是業務技術還是底層技術人員,有一些思維和能力都是共通的。比如,分解問題的能力,抽象思維,結構化思維等等。

一文教會你如何寫複雜業務的代碼

用我的話說就是:“做不好業務開發的,也做不好技術底層開發,反之亦然。業務開發一點都不簡單,隻是我們很多人把它做“簡單”了

是以,如果從變化的角度來看,業務技術的難度一點不遜色于底層技術,其面臨的挑戰甚至更大。是以,我想對廣大的從事業務技術開發說:沉下心來,夯實自己的基礎技術能力、OO能力、模組化能力... 不斷提升抽象思維、結構化思維、思辨思維... 持續學習精進,寫好代碼。我們可以在業務技術崗做的很”技術“!。

作者簡介:張建飛,阿裡巴巴進階技術專家,2007年雲南大學計算機應用工程碩士,12年軟體設計和應用架構經驗。熱衷于複雜業務分析和代碼複雜度治理,在外企工作6年,阿裡工作5年。

繼續閱讀