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