数字农业WMS库存操作重构及思考
簡介:?數(shù)字農(nóng)業(yè)庫存管理系統(tǒng)在2020年時,部門對產(chǎn)地倉生鮮水果生產(chǎn)加工數(shù)字化的背景下應運而生。項目一期的數(shù)農(nóng)WMS中的各類庫存操作均為單獨編寫。而伴隨著后續(xù)的不斷迭代,這些庫存操作間慢慢積累了大量的共性邏輯:如參數(shù)校驗、冪等性控制、操作明細構(gòu)建、同步任務構(gòu)建、數(shù)據(jù)庫操作CAS重試、庫存動賬事件發(fā)布等等……大量重復或相似的代碼不利于后續(xù)維護及高效迭代,因此我們決定借鑒并比較模板方法(Template Method)和回調(diào)(Callback)的思路進行重構(gòu):我們需要為各類庫存操作搭建一個統(tǒng)一的框架,對其中固定不變的共性邏輯進行復用,而對會隨場景變化的部分提供靈活擴展的能力支持。
作者 | 在田
來源 | 阿里技術公眾號
一 問題背景
數(shù)字農(nóng)業(yè)庫存管理系統(tǒng)(以下簡稱數(shù)農(nóng)WMS)是在2020年時,部門對產(chǎn)地倉生鮮水果生產(chǎn)加工數(shù)字化的背景下應運而生。項目一期的數(shù)農(nóng)WMS中的各類庫存操作(如庫存增加、占用、轉(zhuǎn)移等)均為單獨編寫。而伴隨著后續(xù)的不斷迭代,這些庫存操作間慢慢積累了大量的共性邏輯:如參數(shù)校驗、冪等性控制、操作明細構(gòu)建、同步任務構(gòu)建、數(shù)據(jù)庫操作CAS重試、庫存動賬事件發(fā)布等等……大量重復或相似的代碼不利于后續(xù)維護及高效迭代,因此我們決定借鑒并比較模板方法(Template Method)和回調(diào)(Callback)的思路進行重構(gòu):我們需要為各類庫存操作搭建一個統(tǒng)一的框架,對其中固定不變的共性邏輯進行復用,而對會隨場景變化的部分提供靈活擴展的能力支持。
二 模板方法
GoF的《設計模式》一書中對模板方法的定義是:「定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個算法的結(jié)構(gòu)即可重定義該算法的某些特定步驟。」 —— 其核心是對算法或業(yè)務邏輯骨架的復用,以及其中部分操作的個性化擴展。在正式介紹對數(shù)農(nóng)WMS庫存操作的重構(gòu)工作前,我們先以一個具體案例 —— AbstractQueuedSynchronizer(注1)(以下簡稱AQS) —— 來了解模板方法設計模式。雖然通過AQS這個相對復雜的例子來介紹模板方法顯得有些小題大做,但由于AQS一方面是Java并發(fā)包的核心框架,另一方面也是模板方法在JDK中的現(xiàn)實案例,對它的剖析能使我們了解其背后精心的設計思路,同時與下文將介紹的回調(diào)的重構(gòu)方式進行對比,值得我們多花一些時間研究。
《Java并發(fā)編程實戰(zhàn)》中對AQS的描述是:AQS是一個用于構(gòu)建鎖和同步器的框架,許多同步器都可以通過AQS很容易并且高效地構(gòu)造出來。不僅ReentrantLock和Semaphore是基于AQS構(gòu)建的,還包括CountDownLatch、ReentrantReadWriteLock等。AQS解決了在實現(xiàn)同步器時涉及的大量細節(jié)問題(例如等待線程采用FIFO隊列操作順序)。在基于AQS構(gòu)建的同步器類中,最基本的操作包括各種形式的「獲取操作」和「釋放操作」。在不同的同步器中可以定義一些靈活的標準,來判斷某個線程是應該通過還是需要等待。比如當使用鎖或信號量時,獲取操作的含義就很直觀,即「獲取的是鎖或者許可」。AQS負責管理同步器類中的狀態(tài)(synchronization state),它管理了一個整數(shù)狀態(tài)信息,用于表示任意狀態(tài)。例如,ReentrantLock用它來表示所有者線程已經(jīng)重復獲取該鎖的次數(shù),Semaphore用它來表示剩余的可被獲取的許可數(shù)量。
對照我們在前文中引用的GoF對模板模式的定義,這里提到的「鎖和同步器的框架」即對應「算法的骨架」,「靈活的標準」即對應「重定義該算法的某些特定步驟」;而synchronization state(以下簡稱「同步狀態(tài)」)可以說是這兩者之間交互的橋梁。Doug Lea對AQS框架的「獲取操作」和「釋放操作」的算法骨架的基本思路描述如下方偽代碼所示。可以看到,在獲取和釋放操作中,對同步狀態(tài)的判斷和更新,是算法骨架中可被各類同步器靈活擴展的部分;而相應的對操作線程的入隊、阻塞、喚起和出隊操作,則是算法骨架中被各類同步器所復用的部分。
// 「獲取操作」偽代碼 While(synchronization state does not allow acquire) { // * 骨架擴展點enqueue current thread if not already queued; // 線程結(jié)點入隊possibly block current thread; // 阻塞當前線程 } dequeue current thread if it was queued; // 線程結(jié)點出隊// 「釋放操作」偽代碼 update synchronization state // * 骨架擴展點 if (state may permit a blocked thread to acquire) { // * 骨架擴展點unblock one or more queued threads; // 喚起被阻塞的線程 }下面我們以大家熟悉的ReentrantLock為例具體分析。ReentrantLock實例內(nèi)部維護了一個AQS的具體實現(xiàn),用戶的lock/unlock請求最終是借助AQS實例的acquire/release方法實現(xiàn)。同時,AQS實例在被構(gòu)造時有兩種選擇:非公平性鎖實現(xiàn)和公平性鎖實現(xiàn)。我們來看下AQS算法骨架部分的代碼:
// AQS acquire/release 操作算法骨架代碼 public abstract class AbstractQueuedSynchronizerextends AbstractOwnableSynchronizerimplements java.io.Serializable {// 同步狀態(tài) synchronization state private volatile int state; // 排他式「獲取操作」public final void acquire(int arg) {if (!tryAcquire(arg) && // * 骨架擴展點acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 線程結(jié)點入隊selfInterrupt();}// 針對已入隊線程結(jié)點的排他式「獲取操作」final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) { // * 骨架擴展點setHead(node); // 線程結(jié)點出隊(隊列head為啞結(jié)點)p.next = null;failed = false;return interrupted;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt()) // 阻塞當前線程interrupted = true;}} finally {if (failed)cancelAcquire(node);}}// 排他式「釋放操作」public final boolean release(int arg) {if (tryRelease(arg)) { // * 骨架擴展點Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h); // 喚起被阻塞的線程return true;}return false;}// * 排他式「獲取操作」骨架擴展點protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();}// * 排他式「釋放操作」骨架擴展點protected boolean tryRelease(int arg) {throw new UnsupportedOperationException();}}可以看到,AQS骨架代碼為其子類的具體實現(xiàn)封裝并屏蔽了復雜的FIFO隊列和線程控制邏輯。ReentrantLock中的AQS實例只需實現(xiàn)其中的個性化邏輯部分:tryAcquire和tryRelease方法。比如在tryAcquire方法中,如果發(fā)現(xiàn)同步狀態(tài)為0,會嘗試以CAS的方式更新同步狀態(tài)為1,以獲取鎖;如果發(fā)現(xiàn)同步狀態(tài)大于0,且當前線程就是持有鎖的線程,則會將同步狀態(tài)加1,表示鎖的重入;否則方法返回false,表示獲取鎖失敗。而其中非公平性鎖(ReentrantLock.NonfairSync)和公平性鎖(ReentrantLock.FairSync)的區(qū)別主要在于,公平性鎖在嘗試獲取鎖時,會檢查是否已有其他線程先于當前線程等待獲取鎖,如果沒有,才會按照前述的方式嘗試加鎖。下圖是ReentrantLock中AQS具體實現(xiàn)的類圖(中間有一層額外的ReentrantLock.Sync,主要是為了部分代碼的復用而設計)。
三 回調(diào)方式
但是,數(shù)農(nóng)WMS最終使用的重構(gòu)方式,實際上并不是模板方法模式,而是借鑒了Spring的風格,基于回調(diào)(Callback)的方式實現(xiàn)算法骨架中的擴展點。維基百科中對回調(diào)的定義是:「一段可執(zhí)行代碼被作為參數(shù)傳遞到另一段代碼中,并將在某個時機被這段代碼回調(diào)(執(zhí)行)」。回調(diào)雖然不屬于GoF的書中總結(jié)的某種特定的設計模式,但是在觀察者(Observer)、策略(Strategy)和訪問者(Visitor)這些模式中都可以發(fā)現(xiàn)它的身影(注2),可以說是一種常見的編程方式。
如下述RedisTemplate中的管道模式命令執(zhí)行方法,其中的RedisCallback< ?> action參數(shù)即是作為函數(shù)式回調(diào)接口,接收用戶傳入的具體實現(xiàn)(自定義Redis命令操作),并在管道模式下進行回調(diào)執(zhí)行(action.doInRedis或session.execute)。同時,管道的打開和關閉(connection.openPipeline/connection.closePipeline)也支持不同的實現(xiàn)方式:如我們熟悉的JedisConnection和Spring Boot 2開始默認使用的LettuceConnection。值得注意的是,雖然在Spring框架中存在各類以Template后綴命名的類(如RedisTemplate、TransactionTemplate、JdbcTemplate等),但是仔細觀察可以發(fā)現(xiàn),它們實際上使用的并不是模板方法,而是回調(diào)的方式(注3)。
public class RedisTemplate< K, V> extends RedisAccessor implements RedisOperations< K, V>, BeanClassLoaderAware {// 管道模式命令執(zhí)行,RedisCallback@Overridepublic List< Object> executePipelined(RedisCallback< ?> action, @Nullable RedisSerializer< ?> resultSerializer) {return execute((RedisCallback< List< Object>>) connection -> {connection.openPipeline(); // * 擴展點:開啟管道模式boolean pipelinedClosed = false;try {Object result = action.doInRedis(connection); // * 擴展點:回調(diào)執(zhí)行用戶自定義操作if (result != null) {throw new InvalidDataAccessApiUsageException("Callback cannot return a non-null value as it gets overwritten by the pipeline");}List< Object> closePipeline = connection.closePipeline(); // * 擴展點:關閉管道模式pipelinedClosed = true;return deserializeMixedResults(closePipeline, resultSerializer, hashKeySerializer, hashValueSerializer);} finally {if (!pipelinedClosed) {connection.closePipeline();}}});}// 事務+管道模式命令執(zhí)行@Overridepublic List< Object> executePipelined(SessionCallback< ?> session, @Nullable RedisSerializer< ?> resultSerializer) {// 具體代碼省略}}類似地,在數(shù)農(nóng)WMS的庫存操作重構(gòu)中,我們定義了ContainerInventoryOperationTemplate「模板類」,作為承載庫存操作業(yè)務邏輯的框架。下述為其中的庫存操作核心代碼片段。可以看到,框架統(tǒng)一定義了庫存操作流程,并對其中的通用邏輯提供了支持,使各類不同的庫存操作得以復用:如構(gòu)建庫存操作明細、持久化操作明細及同步任務、并發(fā)沖突重試等;而對于其中隨不同庫存操作類型變動的邏輯 —— 如操作庫存數(shù)據(jù)、確認前置操作、持久化庫存數(shù)據(jù)等 —— 則通過對ContainerInventoryOperationHandler接口實例的回調(diào)實現(xiàn),它們可以被看作是庫存操作框架代碼中的擴展點。接口由不同類型的庫存操作分別實現(xiàn),如庫存增加、庫存占用、庫存轉(zhuǎn)移、庫存釋放等等。如此,如果我們后續(xù)需要添加某種新類型的庫存操作,只需要實現(xiàn)ContainerInventoryOperationHandler接口中定義的個性化邏輯即可;而如果我們需要對整個庫存操作流程進行迭代,也只需要修改ContainerInventoryOperationTemplate中的框架代碼,而不是像先前那樣,需要同時修改多處代碼(這里模板類和庫存操作handler的命名均以Container作為前綴,是因為數(shù)農(nóng)WMS以容器托盤作為基本的庫存管理單元)。
@Service public class ContainerInventoryOperationTemplate {private Boolean doOperateInTransaction(OperationContext context) {final Boolean transactionSuccess = transactionTemplate.execute(transactionStatus -> {try {ContainerInventoryOperationHandler handler = context.getHandler(); // 庫存操作回調(diào)handlerhandler.getAndCheckCurrentInventory(context); // 獲取并校驗庫存數(shù)據(jù)buildInventoryDetail(context); // 構(gòu)建庫存操作明細handler.operateInventory(context); // * 擴展點:操作庫存數(shù)據(jù)handler.confirmPreOperationIfNecessary(context); // * 擴展點:確認前置操作(如庫存占用) handler.persistInventoryOperation(context); // * 擴展點:持久化庫存數(shù)據(jù)persistInventoryDetailAndSyncTask(context); // 持久化操作明細及同步任務 doSyncOperationIfNecessary(context); // 庫存同步操作return Boolean.TRUE;} catch (WhException we) {context.setWhException(we);// 遇到并發(fā)沖突異常,需要重試if (Objects.equals(we.getErrorCode(), ErrorCodeEnum.CAS_SAVE_ERROR.getCode())) {context.setCanRetry(true);}}// 省略部分代碼transactionStatus.setRollbackOnly();return Boolean.FALSE;});// 省略部分代碼return transactionSuccess;}}四 組合與繼承
為什么我們選擇了基于回調(diào),而非模板方法的方式,來實現(xiàn)數(shù)農(nóng)WMS的庫存操作重構(gòu)呢?由于回調(diào)是基于對象之間的組合關系(composition)實現(xiàn),而模板方法是基于類之間的繼承關系(inheritance)實現(xiàn),我們結(jié)合系統(tǒng)實際情況,并基于「組合優(yōu)先于繼承」的考量,最終選擇了使用回調(diào)的方式進行代碼重構(gòu)。其原因大致如下:
結(jié)合我們前文中介紹的AbstractQueuedSynchronizer的案例,仔細閱讀其源碼可以發(fā)現(xiàn),作者通過代碼上的精心設計規(guī)避了上文提到的「繼承打破封裝性」的問題。比如,為了不使模板中的骨架邏輯錯誤地被子類覆蓋,相關方法(如acquire和release)均使用了final關鍵字進行修飾;而對于某些必須由子類實現(xiàn)的擴展點,在AQS抽象類中均會拋出UnsupportedOperationException異常。然而此處不將擴展點定義為抽象方法,而是提供拋出異常的默認實現(xiàn)的原因,個人認為是由于AQS中定義了不同形式的獲取和釋放操作,而其鎖和同步器的具體實現(xiàn)雖然會繼承所有這些方法,但依據(jù)自身的應用場景往往只關心其中某種版本。比如ReentrantLock中的AQS實現(xiàn)僅關心排他式的版本(即tryAcquire和tryRelease),而Semaphore中的AQS實現(xiàn)僅關心共享式的版本(即tryAcquireShared和tryReleaseShared)。解決這類問題的另一種思路便是對這些不同形式的擴展方法進行拆分,歸置到不同的接口,并以回調(diào)的方式進行具體功能實現(xiàn),從而避免暴露不必要的方法。
此外,AQS內(nèi)部維護的等待線程隊列采用的是基于CLH思想實現(xiàn)的FIFO隊列。如果我們同時需要一種優(yōu)先級隊列的內(nèi)部實現(xiàn)(注5),并嚴格按照模板方法的模式對AQS進行擴展,則最終可能得到的是一個稍顯臃腫的類層次,如下圖所示:
AQS作為JDK的底層并發(fā)框架,應用場景相對固定,且更加側(cè)重性能方面的考慮,其擴展性較低無可厚非。而對于如Spring的上層框架,在設計時就必須更多地考慮可擴展性的支持。如前文提到的RedisTemplate,借助其維護的RedisConnectionFactory即可獲得不同類型的底層Redis連接實現(xiàn);而對于其不同形式的管道執(zhí)行方法(管道/事務+管道),用戶只需要實現(xiàn)并傳入對應的回調(diào)接口(RedisCallback/SessionCallback)即可,而不必感知其不需要的方法定義。這兩點便是通過組合委托和回調(diào)的方式實現(xiàn)的,相較AQS而言顯得更加靈活簡潔,如下圖所示:
五 再論重構(gòu)
回到我們的數(shù)農(nóng)WMS庫存操作重構(gòu),雖然ContainerInventoryOperationTemplate與ContainerInventoryOperationHandler之間的關系非常接近策略模式(Strategy),但由于我們的「模板類」使用Spring的單例模式進行管理,其中并沒有單獨維護某個指定的庫存操作handler,而是通過方法傳參的方式觸達它們,因此筆者更傾向于使用回調(diào)描述兩者之間的代碼結(jié)構(gòu)。不過讀者不必對兩者命名的差異過于糾結(jié),因為它們的思路是非常相近的。
隨著數(shù)農(nóng)WMS代碼重構(gòu)的推進,以及對更多庫存操作業(yè)務場景的覆蓋,我們不斷發(fā)現(xiàn)這套重構(gòu)后的代碼框架具備優(yōu)秀的可擴展性。例如,當我們需要為上游系統(tǒng)提供「庫存增加并占用」的庫存操作原子能力支持時,我們發(fā)現(xiàn)可以使用組合委托的方式復用「庫存增加」和「庫存占用」的基本庫存操作能力,從而簡潔高效地完成功能開發(fā)。而這點若是單純基于模板方法的類間繼承的方式是無法實現(xiàn)的。具體代碼和類圖如下:
// 庫存增加并占用 @Component public class IncreaseAndOccupyOperationHandler implements ContainerInventoryOperationHandler {@Resourceprivate IncreaseOperationHandler increaseOperationHandler; // 組合「庫存增加」操作handler@Resourceprivate OccupyOperationHandler occupyOperationHandler; // 組合「庫存占用」操作handler// 委托「庫存占用」操作handler進行前置操作校驗,判斷是否單據(jù)占用已存在@Overridepublic void checkPreOperationIfNecessary(ContainerInventoryOperationTemplate.OperationContext context) {occupyOperationHandler.checkPreOperationIfNecessary(context); }// 委托「庫存增加」操作handler進行庫存信息校驗@Overridepublic void getAndCheckCurrentInventory(ContainerInventoryOperationTemplate.OperationContext context) {increaseOperationHandler.getAndCheckCurrentInventory(context);}// 委托「庫存增加」、「庫存占用」操作handler進行「庫存增加并占用」操作@Overridepublic void operateInventory(ContainerInventoryOperationTemplate.OperationContext context) {increaseOperationHandler.operateInventory(context);occupyOperationHandler.operateInventory(context);}// 其余代碼略}最后,無論是基于模板方法還是回調(diào)的方式對庫存操作進行重構(gòu),雖然我們可以獲得代碼復用以及擴展便利的好處,但是「模板類」中骨架邏輯的復雜性,其實是所有庫存操作復雜性的總和(個人認為這一點在Spring框架的代碼中也有所體現(xiàn))。比如,庫存增加操作在某些場景下需要在開啟數(shù)據(jù)庫事務前獲取分布式鎖,庫存占用操作需要判斷相關單據(jù)是否已經(jīng)占用了庫存等。而模板代碼中的骨架邏輯需要為所有這些流程分支提供擴展點,從而支持各種類型的庫存操作。此外,修改模板骨架邏輯的代碼時也需要小心謹慎,因為一旦模板代碼本身出錯,可能會影響所有的庫存操作。這些都對我們代碼編寫的質(zhì)量和可維護性提出更高的要求。
六 結(jié)語
代碼重構(gòu)并且總結(jié)成文的過程要求不斷地學習、思辨和實踐,也讓自己獲益良多。
注解
https://en.wikipedia.org/wiki/Callback_(computer_programming)
http://gee.cs.oswego.edu/dl/papers/aqs.pdf
參考資料
- 《設計模式》
設計模式 (豆瓣)
- The java.util.concurrent Synchronizer Framework
http://gee.cs.oswego.edu/dl/papers/aqs.pdf
- 《Java并發(fā)編程實戰(zhàn)》
Java并發(fā)編程實戰(zhàn) (豆瓣)
- 維基百科Callback詞條
https://en.wikipedia.org/wiki/Callback_(computer_programming)
- why is jdbctemplate an example of the template method design pattern
java - Why is JdbcTemplate an example of the Template method design pattern - Stack Overflow
- 《Effective Java 3》
Effective Java (豆瓣)
- 《設計模式之美》
設計模式之美_設計模式_代碼重構(gòu)-極客時間
- 維基百科Strategy pattern詞條
https://en.wikipedia.org/wiki/Strategy_pattern
原文鏈接
本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。?
總結(jié)
以上是生活随笔為你收集整理的数字农业WMS库存操作重构及思考的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SSD( Single Shot Mul
- 下一篇: 阿里云表格存储全面升级,打造一站式物联网