數(shù)字農(nóng)業(yè) WMS 庫(kù)存操作重構(gòu)及思考(數(shù)字農(nóng)業(yè)系統(tǒng))
一 問題背景
數(shù)字農(nóng)業(yè)庫(kù)存管理系統(tǒng)(以下簡(jiǎn)稱數(shù)農(nóng)WMS)是在2020年時(shí),部門對(duì)產(chǎn)地倉(cāng)生鮮水果生產(chǎn)加工數(shù)字化的背景下應(yīng)運(yùn)而生。項(xiàng)目一期的數(shù)農(nóng)WMS中的各類庫(kù)存操作(如庫(kù)存增加、占用、轉(zhuǎn)移等)均為單獨(dú)編寫。而伴隨著后續(xù)的不斷迭代,這些庫(kù)存操作間慢慢積累了大量的共性邏輯:如參數(shù)校驗(yàn)、冪等性控制、操作明細(xì)構(gòu)建、同步任務(wù)構(gòu)建、數(shù)據(jù)庫(kù)操作CAS重試、庫(kù)存動(dòng)賬事件發(fā)布等等……大量重復(fù)或相似的代碼不利于后續(xù)維護(hù)及高效迭代,因此我們決定借鑒并比較模板方法(Template Method)和回調(diào)(Callback)的思路進(jìn)行重構(gòu):我們需要為各類庫(kù)存操作搭建一個(gè)統(tǒng)一的框架,對(duì)其中固定不變的共性邏輯進(jìn)行復(fù)用,而對(duì)會(huì)隨場(chǎng)景變化的部分提供靈活擴(kuò)展的能力支持。
二 模板方法
GoF的《設(shè)計(jì)模式》一書中對(duì)模板方法的定義是:「定義一個(gè)操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個(gè)算法的結(jié)構(gòu)即可重定義該算法的某些特定步驟?!?—— 其核心是對(duì)算法或業(yè)務(wù)邏輯骨架的復(fù)用,以及其中部分操作的個(gè)性化擴(kuò)展。在正式介紹對(duì)數(shù)農(nóng)WMS庫(kù)存操作的重構(gòu)工作前,我們先以一個(gè)具體案例 —— AbstractQueuedSynchronizer(注1)(以下簡(jiǎn)稱AQS) —— 來了解模板方法設(shè)計(jì)模式。雖然通過AQS這個(gè)相對(duì)復(fù)雜的例子來介紹模板方法顯得有些小題大做,但由于AQS一方面是Java并發(fā)包的核心框架,另一方面也是模板方法在JDK中的現(xiàn)實(shí)案例,對(duì)它的剖析能使我們了解其背后精心的設(shè)計(jì)思路,同時(shí)與下文將介紹的回調(diào)的重構(gòu)方式進(jìn)行對(duì)比,值得我們多花一些時(shí)間研究。
《Java并發(fā)編程實(shí)戰(zhàn)》中對(duì)AQS的描述是:AQS是一個(gè)用于構(gòu)建鎖和同步器的框架,許多同步器都可以通過AQS很容易并且高效地構(gòu)造出來。不僅ReentrantLock和Semaphore是基于AQS構(gòu)建的,還包括CountDownLatch、ReentrantReadWriteLock等。AQS解決了在實(shí)現(xiàn)同步器時(shí)涉及的大量細(xì)節(jié)問題(例如等待線程采用FIFO隊(duì)列操作順序)。在基于AQS構(gòu)建的同步器類中,最基本的操作包括各種形式的「獲取操作」和「釋放操作」。在不同的同步器中可以定義一些靈活的標(biāo)準(zhǔn),來判斷某個(gè)線程是應(yīng)該通過還是需要等待。比如當(dāng)使用鎖或信號(hào)量時(shí),獲取操作的含義就很直觀,即「獲取的是鎖或者許可」。AQS負(fù)責(zé)管理同步器類中的狀態(tài)(synchronization state),它管理了一個(gè)整數(shù)狀態(tài)信息,用于表示任意狀態(tài)。例如,ReentrantLock用它來表示所有者線程已經(jīng)重復(fù)獲取該鎖的次數(shù),Semaphore用它來表示剩余的可被獲取的許可數(shù)量。
對(duì)照我們?cè)谇拔闹幸玫腉oF對(duì)模板模式的定義,這里提到的「鎖和同步器的框架」即對(duì)應(yīng)「算法的骨架」,「靈活的標(biāo)準(zhǔn)」即對(duì)應(yīng)「重定義該算法的某些特定步驟」;而synchronization state(以下簡(jiǎn)稱「同步狀態(tài)」)可以說是這兩者之間交互的橋梁。Doug Lea對(duì)AQS框架的「獲取操作」和「釋放操作」的算法骨架的基本思路描述如下方偽代碼所示??梢钥吹剑讷@取和釋放操作中,對(duì)同步狀態(tài)的判斷和更新,是算法骨架中可被各類同步器靈活擴(kuò)展的部分;而相應(yīng)的對(duì)操作線程的入隊(duì)、阻塞、喚起和出隊(duì)操作,則是算法骨架中被各類同步器所復(fù)用的部分。
// 「獲取操作」偽代碼While(synchronization state does not allow acquire) { // * 骨架擴(kuò)展點(diǎn) enqueue current thread if not already queued; // 線程結(jié)點(diǎn)入隊(duì) possibly block current thread; // 阻塞當(dāng)前線程}dequeue current thread if it was queued; // 線程結(jié)點(diǎn)出隊(duì)// 「釋放操作」偽代碼update synchronization state // * 骨架擴(kuò)展點(diǎn)if (state may permit a blocked thread to acquire) { // * 骨架擴(kuò)展點(diǎn) unblock one or more queued threads; // 喚起被阻塞的線程}
下面我們以大家熟悉的ReentrantLock為例具體分析。ReentrantLock實(shí)例內(nèi)部維護(hù)了一個(gè)AQS的具體實(shí)現(xiàn),用戶的lock/unlock請(qǐng)求最終是借助AQS實(shí)例的acquire/release方法實(shí)現(xiàn)。同時(shí),AQS實(shí)例在被構(gòu)造時(shí)有兩種選擇:非公平性鎖實(shí)現(xiàn)和公平性鎖實(shí)現(xiàn)。我們來看下AQS算法骨架部分的代碼:
// AQS acquire/release 操作算法骨架代碼public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { // 同步狀態(tài) synchronization state private volatile int state; // 排他式「獲取操作」 public final void acquire(int arg) { if (!tryAcquire(arg) && // * 骨架擴(kuò)展點(diǎn) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 線程結(jié)點(diǎn)入隊(duì) selfInterrupt(); } // 針對(duì)已入隊(duì)線程結(jié)點(diǎn)的排他式「獲取操作」 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)) { // * 骨架擴(kuò)展點(diǎn) setHead(node); // 線程結(jié)點(diǎn)出隊(duì)(隊(duì)列head為啞結(jié)點(diǎn)) p.next = null; failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 阻塞當(dāng)前線程 interrupted = true; } } finally { if (failed) cancelAcquire(node); } } // 排他式「釋放操作」 public final boolean release(int arg) { if (tryRelease(arg)) { // * 骨架擴(kuò)展點(diǎn) Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 喚起被阻塞的線程 return true; } return false; } // * 排他式「獲取操作」骨架擴(kuò)展點(diǎn) protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); } // * 排他式「釋放操作」骨架擴(kuò)展點(diǎn) protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }}
可以看到,AQS骨架代碼為其子類的具體實(shí)現(xiàn)封裝并屏蔽了復(fù)雜的FIFO隊(duì)列和線程控制邏輯。ReentrantLock中的AQS實(shí)例只需實(shí)現(xiàn)其中的個(gè)性化邏輯部分:tryAcquire和tryRelease方法。比如在tryAcquire方法中,如果發(fā)現(xiàn)同步狀態(tài)為0,會(huì)嘗試以CAS的方式更新同步狀態(tài)為1,以獲取鎖;如果發(fā)現(xiàn)同步狀態(tài)大于0,且當(dāng)前線程就是持有鎖的線程,則會(huì)將同步狀態(tài)加1,表示鎖的重入;否則方法返回false,表示獲取鎖失敗。而其中非公平性鎖(ReentrantLock.NonfairSync)和公平性鎖(ReentrantLock.FairSync)的區(qū)別主要在于,公平性鎖在嘗試獲取鎖時(shí),會(huì)檢查是否已有其他線程先于當(dāng)前線程等待獲取鎖,如果沒有,才會(huì)按照前述的方式嘗試加鎖。下圖是ReentrantLock中AQS具體實(shí)現(xiàn)的類圖(中間有一層額外的ReentrantLock.Sync,主要是為了部分代碼的復(fù)用而設(shè)計(jì))。
三 回調(diào)方式
但是,數(shù)農(nóng)WMS最終使用的重構(gòu)方式,實(shí)際上并不是模板方法模式,而是借鑒了Spring的風(fēng)格,基于回調(diào)(Callback)的方式實(shí)現(xiàn)算法骨架中的擴(kuò)展點(diǎn)。維基百科中對(duì)回調(diào)的定義是:「一段可執(zhí)行代碼被作為參數(shù)傳遞到另一段代碼中,并將在某個(gè)時(shí)機(jī)被這段代碼回調(diào)(執(zhí)行)」?;卣{(diào)雖然不屬于GoF的書中總結(jié)的某種特定的設(shè)計(jì)模式,但是在觀察者(Observer)、策略(Strategy)和訪問者(Visitor)這些模式中都可以發(fā)現(xiàn)它的身影(注2),可以說是一種常見的編程方式。
如下述RedisTemplate中的管道模式命令執(zhí)行方法,其中的RedisCallback< ?> action參數(shù)即是作為函數(shù)式回調(diào)接口,接收用戶傳入的具體實(shí)現(xiàn)(自定義Redis命令操作),并在管道模式下進(jìn)行回調(diào)執(zhí)行(action.doInRedis或session.execute)。同時(shí),管道的打開和關(guān)閉(connection.openPipeline/connection.closePipeline)也支持不同的實(shí)現(xiàn)方式:如我們熟悉的JedisConnection和Spring Boot 2開始默認(rèn)使用的LettuceConnection。值得注意的是,雖然在Spring框架中存在各類以Template后綴命名的類(如RedisTemplate、TransactionTemplate、JdbcTemplate等),但是仔細(xì)觀察可以發(fā)現(xiàn),它們實(shí)際上使用的并不是模板方法,而是回調(diào)的方式(注3)。
public class RedisTemplate< K, V> extends RedisAccessor implements RedisOperations< K, V>, BeanClassLoaderAware { // 管道模式命令執(zhí)行,RedisCallback @Override public List< Object> executePipelined(RedisCallback< ?> action, @Nullable RedisSerializer< ?> resultSerializer) { return execute((RedisCallback< List< Object>>) connection -> { connection.openPipeline(); // * 擴(kuò)展點(diǎn):開啟管道模式 boolean pipelinedClosed = false; try { Object result = action.doInRedis(connection); // * 擴(kuò)展點(diǎn):回調(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(); // * 擴(kuò)展點(diǎn):關(guān)閉管道模式 pipelinedClosed = true; return deserializeMixedResults(closePipeline, resultSerializer, hashKeySerializer, hashValueSerializer); } finally { if (!pipelinedClosed) { connection.closePipeline(); } } }); } // 事務(wù) 管道模式命令執(zhí)行 @Override public List< Object> executePipelined(SessionCallback< ?> session, @Nullable RedisSerializer< ?> resultSerializer) { // 具體代碼省略 } }
類似地,在數(shù)農(nóng)WMS的庫(kù)存操作重構(gòu)中,我們定義了ContainerInventoryOperationTemplate「模板類」,作為承載庫(kù)存操作業(yè)務(wù)邏輯的框架。下述為其中的庫(kù)存操作核心代碼片段。可以看到,框架統(tǒng)一定義了庫(kù)存操作流程,并對(duì)其中的通用邏輯提供了支持,使各類不同的庫(kù)存操作得以復(fù)用:如構(gòu)建庫(kù)存操作明細(xì)、持久化操作明細(xì)及同步任務(wù)、并發(fā)沖突重試等;而對(duì)于其中隨不同庫(kù)存操作類型變動(dòng)的邏輯 —— 如操作庫(kù)存數(shù)據(jù)、確認(rèn)前置操作、持久化庫(kù)存數(shù)據(jù)等 —— 則通過對(duì)ContainerInventoryOperationHandler接口實(shí)例的回調(diào)實(shí)現(xiàn),它們可以被看作是庫(kù)存操作框架代碼中的擴(kuò)展點(diǎn)。接口由不同類型的庫(kù)存操作分別實(shí)現(xiàn),如庫(kù)存增加、庫(kù)存占用、庫(kù)存轉(zhuǎn)移、庫(kù)存釋放等等。如此,如果我們后續(xù)需要添加某種新類型的庫(kù)存操作,只需要實(shí)現(xiàn)ContainerInventoryOperationHandler接口中定義的個(gè)性化邏輯即可;而如果我們需要對(duì)整個(gè)庫(kù)存操作流程進(jìn)行迭代,也只需要修改ContainerInventoryOperationTemplate中的框架代碼,而不是像先前那樣,需要同時(shí)修改多處代碼(這里模板類和庫(kù)存操作handler的命名均以Container作為前綴,是因?yàn)閿?shù)農(nóng)WMS以容器托盤作為基本的庫(kù)存管理單元)。
@Servicepublic class ContainerInventoryOperationTemplate { private Boolean doOperateInTransaction(OperationContext context) { final Boolean transactionSuccess = transactionTemplate.execute(transactionStatus -> { try { ContainerInventoryOperationHandler handler = context.getHandler(); // 庫(kù)存操作回調(diào)handler handler.getAndCheckCurrentInventory(context); // 獲取并校驗(yàn)庫(kù)存數(shù)據(jù) buildInventoryDetail(context); // 構(gòu)建庫(kù)存操作明細(xì) handler.operateInventory(context); // * 擴(kuò)展點(diǎn):操作庫(kù)存數(shù)據(jù) handler.confirmPreOperationIfNecessary(context); // * 擴(kuò)展點(diǎn):確認(rèn)前置操作(如庫(kù)存占用) handler.persistInventoryOperation(context); // * 擴(kuò)展點(diǎn):持久化庫(kù)存數(shù)據(jù) persistInventoryDetailAndSyncTask(context); // 持久化操作明細(xì)及同步任務(wù) doSyncOperationIfNecessary(context); // 庫(kù)存同步操作 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),而非模板方法的方式,來實(shí)現(xiàn)數(shù)農(nóng)WMS的庫(kù)存操作重構(gòu)呢?由于回調(diào)是基于對(duì)象之間的組合關(guān)系(composition)實(shí)現(xiàn),而模板方法是基于類之間的繼承關(guān)系(inheritance)實(shí)現(xiàn),我們結(jié)合系統(tǒng)實(shí)際情況,并基于「組合優(yōu)先于繼承」的考量,最終選擇了使用回調(diào)的方式進(jìn)行代碼重構(gòu)。其原因大致如下:
- 繼承打破封裝性:《Effective Java》在《第18條:復(fù)合優(yōu)先于繼承》中提到,繼承是實(shí)現(xiàn)代碼重用的有力手段,但它并非永遠(yuǎn)是完成這項(xiàng)工作的最佳工具。使用不當(dāng)會(huì)導(dǎo)致軟件變得很脆弱。與方法調(diào)用不同的是,繼承打破了封裝性。換句話說,子類依賴于其超類中特定功能的實(shí)現(xiàn)細(xì)節(jié)。超類的實(shí)現(xiàn)有可能會(huì)隨著發(fā)行版本的不同而有所變化,如果真的發(fā)生了變化,子類可能會(huì)遭到破壞,即使它的代碼完全沒有改變。同時(shí),子類可能繼承了定義在父類,但其自身并不需要的方法,有違最小知識(shí)原則(Least Knowledge Principle)。子類可能因此錯(cuò)誤地覆蓋并改變了父類中的方法實(shí)現(xiàn),導(dǎo)致父類功能的封裝性被破壞。而如果我們使用對(duì)象間組合的方式,則可以避免此類問題的出現(xiàn)。
- 接口優(yōu)于抽象類:仍舊是《Effective Java》,在《第20條:接口優(yōu)于抽象類》中提到,因?yàn)镴ava只允許單繼承,所以用抽象類(模板方法便是基于抽象類實(shí)現(xiàn))作為類型定義受到了限制。而現(xiàn)有的類可以很容易被更新,以實(shí)現(xiàn)新的接口。接口是定義混合類型(mixin)的理想選擇,允許構(gòu)造非層次結(jié)構(gòu)的類型框架。與之相反的做法是編寫一個(gè)臃腫的類層次,對(duì)于每一種要被支持的屬性組合,都包含一個(gè)單獨(dú)的類。如果整個(gè)類型系統(tǒng)中有n個(gè)屬性,那么就必須支持2n種可能的組合,這種現(xiàn)象被稱為「組合爆炸」,即需要定義過多的類。
- 組合替代繼承:最后,王爭(zhēng)的《設(shè)計(jì)模式之美》中提到,繼承主要有三個(gè)作用:表示is-a關(guān)系,支持多態(tài)性,以及代碼復(fù)用。而這三個(gè)作用都可以通過其他手段達(dá)成:is-a關(guān)系可以通過組合和接口的has-a關(guān)系來替代;多態(tài)性可以利用接口來實(shí)現(xiàn);代碼復(fù)用則可以通過組合和委托來實(shí)現(xiàn)。因此從理論上講,通過組合、接口、委托三個(gè)技術(shù)手段,我們可以替換掉繼承,在項(xiàng)目中不用或者少用復(fù)雜的繼承關(guān)系。這種對(duì)象間組合的設(shè)計(jì)方式比類間繼承的方式更加符合開閉原則(Open-Closed Principle)(注4)。
結(jié)合我們前文中介紹的AbstractQueuedSynchronizer的案例,仔細(xì)閱讀其源碼可以發(fā)現(xiàn),作者通過代碼上的精心設(shè)計(jì)規(guī)避了上文提到的「繼承打破封裝性」的問題。比如,為了不使模板中的骨架邏輯錯(cuò)誤地被子類覆蓋,相關(guān)方法(如acquire和release)均使用了final關(guān)鍵字進(jìn)行修飾;而對(duì)于某些必須由子類實(shí)現(xiàn)的擴(kuò)展點(diǎn),在AQS抽象類中均會(huì)拋出UnsupportedOperationException異常。然而此處不將擴(kuò)展點(diǎn)定義為抽象方法,而是提供拋出異常的默認(rèn)實(shí)現(xiàn)的原因,個(gè)人認(rèn)為是由于AQS中定義了不同形式的獲取和釋放操作,而其鎖和同步器的具體實(shí)現(xiàn)雖然會(huì)繼承所有這些方法,但依據(jù)自身的應(yīng)用場(chǎng)景往往只關(guān)心其中某種版本。比如ReentrantLock中的AQS實(shí)現(xiàn)僅關(guān)心排他式的版本(即tryAcquire和tryRelease),而Semaphore中的AQS實(shí)現(xiàn)僅關(guān)心共享式的版本(即tryAcquireShared和tryReleaseShared)。解決這類問題的另一種思路便是對(duì)這些不同形式的擴(kuò)展方法進(jìn)行拆分,歸置到不同的接口,并以回調(diào)的方式進(jìn)行具體功能實(shí)現(xiàn),從而避免暴露不必要的方法。
此外,AQS內(nèi)部維護(hù)的等待線程隊(duì)列采用的是基于CLH思想實(shí)現(xiàn)的FIFO隊(duì)列。如果我們同時(shí)需要一種優(yōu)先級(jí)隊(duì)列的內(nèi)部實(shí)現(xiàn)(注5),并嚴(yán)格按照模板方法的模式對(duì)AQS進(jìn)行擴(kuò)展,則最終可能得到的是一個(gè)稍顯臃腫的類層次,如下圖所示:
AQS作為JDK的底層并發(fā)框架,應(yīng)用場(chǎng)景相對(duì)固定,且更加側(cè)重性能方面的考慮,其擴(kuò)展性較低無(wú)可厚非。而對(duì)于如Spring的上層框架,在設(shè)計(jì)時(shí)就必須更多地考慮可擴(kuò)展性的支持。如前文提到的RedisTemplate,借助其維護(hù)的RedisConnectionFactory即可獲得不同類型的底層Redis連接實(shí)現(xiàn);而對(duì)于其不同形式的管道執(zhí)行方法(管道/事務(wù) 管道),用戶只需要實(shí)現(xiàn)并傳入對(duì)應(yīng)的回調(diào)接口(RedisCallback/SessionCallback)即可,而不必感知其不需要的方法定義。這兩點(diǎn)便是通過組合委托和回調(diào)的方式實(shí)現(xiàn)的,相較AQS而言顯得更加靈活簡(jiǎn)潔,如下圖所示:
五 再論重構(gòu)
回到我們的數(shù)農(nóng)WMS庫(kù)存操作重構(gòu),雖然ContainerInventoryOperationTemplate與ContainerInventoryOperationHandler之間的關(guān)系非常接近策略模式(Strategy),但由于我們的「模板類」使用Spring的單例模式進(jìn)行管理,其中并沒有單獨(dú)維護(hù)某個(gè)指定的庫(kù)存操作handler,而是通過方法傳參的方式觸達(dá)它們,因此筆者更傾向于使用回調(diào)描述兩者之間的代碼結(jié)構(gòu)。不過讀者不必對(duì)兩者命名的差異過于糾結(jié),因?yàn)樗鼈兊乃悸肥欠浅O嘟摹?/span>
隨著數(shù)農(nóng)WMS代碼重構(gòu)的推進(jìn),以及對(duì)更多庫(kù)存操作業(yè)務(wù)場(chǎng)景的覆蓋,我們不斷發(fā)現(xiàn)這套重構(gòu)后的代碼框架具備優(yōu)秀的可擴(kuò)展性。例如,當(dāng)我們需要為上游系統(tǒng)提供「庫(kù)存增加并占用」的庫(kù)存操作原子能力支持時(shí),我們發(fā)現(xiàn)可以使用組合委托的方式復(fù)用「庫(kù)存增加」和「庫(kù)存占用」的基本庫(kù)存操作能力,從而簡(jiǎn)潔高效地完成功能開發(fā)。而這點(diǎn)若是單純基于模板方法的類間繼承的方式是無(wú)法實(shí)現(xiàn)的。具體代碼和類圖如下:
// 庫(kù)存增加并占用@Componentpublic class IncreaseAndOccupyOperationHandler implements ContainerInventoryOperationHandler { @Resource private IncreaseOperationHandler increaseOperationHandler; // 組合「庫(kù)存增加」操作handler @Resource private OccupyOperationHandler occupyOperationHandler; // 組合「庫(kù)存占用」操作handler // 委托「庫(kù)存占用」操作handler進(jìn)行前置操作校驗(yàn),判斷是否單據(jù)占用已存在 @Override public void checkPreOperationIfNecessary(ContainerInventoryOperationTemplate.OperationContext context) { occupyOperationHandler.checkPreOperationIfNecessary(context); } // 委托「庫(kù)存增加」操作handler進(jìn)行庫(kù)存信息校驗(yàn) @Override public void getAndCheckCurrentInventory(ContainerInventoryOperationTemplate.OperationContext context) { increaseOperationHandler.getAndCheckCurrentInventory(context); } // 委托「庫(kù)存增加」、「庫(kù)存占用」操作handler進(jìn)行「庫(kù)存增加并占用」操作 @Override public void operateInventory(ContainerInventoryOperationTemplate.OperationContext context) { increaseOperationHandler.operateInventory(context); occupyOperationHandler.operateInventory(context); } // 其余代碼略}
最后,無(wú)論是基于模板方法還是回調(diào)的方式對(duì)庫(kù)存操作進(jìn)行重構(gòu),雖然我們可以獲得代碼復(fù)用以及擴(kuò)展便利的好處,但是「模板類」中骨架邏輯的復(fù)雜性,其實(shí)是所有庫(kù)存操作復(fù)雜性的總和(個(gè)人認(rèn)為這一點(diǎn)在Spring框架的代碼中也有所體現(xiàn))。比如,庫(kù)存增加操作在某些場(chǎng)景下需要在開啟數(shù)據(jù)庫(kù)事務(wù)前獲取分布式鎖,庫(kù)存占用操作需要判斷相關(guān)單據(jù)是否已經(jīng)占用了庫(kù)存等。而模板代碼中的骨架邏輯需要為所有這些流程分支提供擴(kuò)展點(diǎn),從而支持各種類型的庫(kù)存操作。此外,修改模板骨架邏輯的代碼時(shí)也需要小心謹(jǐn)慎,因?yàn)橐坏┠0宕a本身出錯(cuò),可能會(huì)影響所有的庫(kù)存操作。這些都對(duì)我們代碼編寫的質(zhì)量和可維護(hù)性提出更高的要求。
六 結(jié)語(yǔ)
代碼重構(gòu)并且總結(jié)成文的過程要求不斷地學(xué)習(xí)、思辨和實(shí)踐,也讓自己獲益良多。
注解
- 對(duì)AQS使用了模板方法設(shè)計(jì)模式的「官方論斷」可見于其作者Doug Lea在The java.util.concurrent Synchronizer Framework一文中的論述:Class AbstractQueuedSynchronizer ties together the above functionality and serves as a "template method pattern" base class for synchronizers. Subclasses define only the methods that implement the state inspections and updates that control acquire and release. 此外,文中還包含了對(duì)等待線程FIFO隊(duì)列(CLH變體)、公平性、框架性能等方面的詳細(xì)討論。 http://gee.cs.oswego.edu/dl/papers/aqs.pdf
- 參考維基百科Callback詞條:In object-oriented programming languages without function-valued arguments, such as in Java before its 8 version, callbacks can be simulated by passing an instance of an abstract class or interface, of which the receiver will call one or more methods, while the calling end provides a concrete implementation. Such objects are effectively a bundle of callbacks, plus the data they need to manipulate. They are useful in implementing various design patterns such as Visitor, Observer, and Strategy.
https://en.wikipedia.org/wiki/Callback_(computer_programming) - Stack Overflow上的某個(gè)問答可作為參考:I concur – JdbcTemplate isn't an example of template method design pattern. The design pattern used is callback. Note that the goal and effect of both patterns is very similar, the main difference is that template method uses inheritance while callback uses composition (sort of) – by Jiri Tousekh. https://stackoverflow.com/questions/33153252/why-is-jdbctemplate-an-example-of-the-template-method-design-pattern
- 參考維基百科Strategy pattern詞條:The strategy pattern uses composition instead of inheritance. In the strategy pattern, behaviors are defined as separate interfaces and specific classes that implement these interfaces. This allows better decoupling between the behavior and the class that uses the behavior. The behavior can be changed without breaking the classes that use it, and the classes can switch between behaviors by changing the specific implementation used without requiring any significant code changes. This is compatible with the open/closed principle (OCP), which proposes that classes should be open for extension but closed for modification. https://en.wikipedia.org/wiki/Strategy_pattern
- Doug Lea在The java.util.concurrent Synchronizer Framework中提到:The heart of the framework is maintenance of queues of blocked threads, which are restricted here to FIFO queues. Thus, the framework does not support priority-based synchronization.
http://gee.cs.oswego.edu/dl/papers/aqs.pdf
參考資料
- 《設(shè)計(jì)模式》
https://book.douban.com/subject/1052241/
- The java.util.concurrent Synchronizer Framework
http://gee.cs.oswego.edu/dl/papers/aqs.pdf
- 《Java并發(fā)編程實(shí)戰(zhàn)》
https://book.douban.com/subject/10484692/
- 維基百科Callback詞條
https://en.wikipedia.org/wiki/Callback_(computer_programming)
- why is jdbctemplate an example of the template method design pattern
https://stackoverflow.com/questions/33153252/why-is-jdbctemplate-an-example-of-the-template-method-design-pattern
- 《Effective Java 3》
https://book.douban.com/subject/27047716/
- 《設(shè)計(jì)模式之美》
https://time.geekbang.org/column/intro/250
- 維基百科Strategy pattern詞條
https://en.wikipedia.org/wiki/Strategy_pattern
作者 | 在田
原文鏈接:http://click.aliyun.com/m/1000305749/
本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。