數(shù)字農(nóng)業(yè) WMS 庫存操作重構(gòu)及思考(數(shù)字農(nóng)業(yè)系統(tǒng))
一 問題背景
數(shù)字農(nóng)業(yè)庫存管理系統(tǒng)(以下簡稱數(shù)農(nóng)WMS)是在2020年時,部門對產(chǎn)地倉生鮮水果生產(chǎn)加工數(shù)字化的背景下應(yīng)運而生。項目一期的數(shù)農(nóng)WMS中的各類庫存操作(如庫存增加、占用、轉(zhuǎn)移等)均為單獨編寫。而伴隨著后續(xù)的不斷迭代,這些庫存操作間慢慢積累了大量的共性邏輯:如參數(shù)校驗、冪等性控制、操作明細構(gòu)建、同步任務(wù)構(gòu)建、數(shù)據(jù)庫操作CAS重試、庫存動賬事件發(fā)布等等……大量重復(fù)或相似的代碼不利于后續(xù)維護及高效迭代,因此我們決定借鑒并比較模板方法(Template Method)和回調(diào)(Callback)的思路進行重構(gòu):我們需要為各類庫存操作搭建一個統(tǒng)一的框架,對其中固定不變的共性邏輯進行復(fù)用,而對會隨場景變化的部分提供靈活擴展的能力支持。
二 模板方法
GoF的《設(shè)計模式》一書中對模板方法的定義是:「定義一個操作中的算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以不改變一個算法的結(jié)構(gòu)即可重定義該算法的某些特定步驟?!?—— 其核心是對算法或業(yè)務(wù)邏輯骨架的復(fù)用,以及其中部分操作的個性化擴展。在正式介紹對數(shù)農(nóng)WMS庫存操作的重構(gòu)工作前,我們先以一個具體案例 —— AbstractQueuedSynchronizer(注1)(以下簡稱AQS) —— 來了解模板方法設(shè)計模式。雖然通過AQS這個相對復(fù)雜的例子來介紹模板方法顯得有些小題大做,但由于AQS一方面是Java并發(fā)包的核心框架,另一方面也是模板方法在JDK中的現(xiàn)實案例,對它的剖析能使我們了解其背后精心的設(shè)計思路,同時與下文將介紹的回調(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)建的同步器類中,最基本的操作包括各種形式的「獲取操作」和「釋放操作」。在不同的同步器中可以定義一些靈活的標準,來判斷某個線程是應(yīng)該通過還是需要等待。比如當使用鎖或信號量時,獲取操作的含義就很直觀,即「獲取的是鎖或者許可」。AQS負責管理同步器類中的狀態(tài)(synchronization state),它管理了一個整數(shù)狀態(tài)信息,用于表示任意狀態(tài)。例如,ReentrantLock用它來表示所有者線程已經(jīng)重復(fù)獲取該鎖的次數(shù),Semaphore用它來表示剩余的可被獲取的許可數(shù)量。
對照我們在前文中引用的GoF對模板模式的定義,這里提到的「鎖和同步器的框架」即對應(yīng)「算法的骨架」,「靈活的標準」即對應(yīng)「重定義該算法的某些特定步驟」;而synchronization state(以下簡稱「同步狀態(tài)」)可以說是這兩者之間交互的橋梁。Doug Lea對AQS框架的「獲取操作」和「釋放操作」的算法骨架的基本思路描述如下方偽代碼所示??梢钥吹?,在獲取和釋放操作中,對同步狀態(tài)的判斷和更新,是算法骨架中可被各類同步器靈活擴展的部分;而相應(yīng)的對操作線程的入隊、阻塞、喚起和出隊操作,則是算法骨架中被各類同步器所復(fù)用的部分。
// 「獲取操作」偽代碼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 AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements 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)封裝并屏蔽了復(fù)雜的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,主要是為了部分代碼的復(fù)用而設(shè)計)。
三 回調(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é)的某種特定的設(shè)計模式,但是在觀察者(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)。同時,管道的打開和關(guān)閉(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 @Override public 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(); // * 擴展點:關(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的庫存操作重構(gòu)中,我們定義了ContainerInventoryOperationTemplate「模板類」,作為承載庫存操作業(yè)務(wù)邏輯的框架。下述為其中的庫存操作核心代碼片段。可以看到,框架統(tǒng)一定義了庫存操作流程,并對其中的通用邏輯提供了支持,使各類不同的庫存操作得以復(fù)用:如構(gòu)建庫存操作明細、持久化操作明細及同步任務(wù)、并發(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以容器托盤作為基本的庫存管理單元)。
@Servicepublic class ContainerInventoryOperationTemplate { private Boolean doOperateInTransaction(OperationContext context) { final Boolean transactionSuccess = transactionTemplate.execute(transactionStatus -> { try { ContainerInventoryOperationHandler handler = context.getHandler(); // 庫存操作回調(diào)handler handler.getAndCheckCurrentInventory(context); // 獲取并校驗庫存數(shù)據(jù) buildInventoryDetail(context); // 構(gòu)建庫存操作明細 handler.operateInventory(context); // * 擴展點:操作庫存數(shù)據(jù) handler.confirmPreOperationIfNecessary(context); // * 擴展點:確認前置操作(如庫存占用) handler.persistInventoryOperation(context); // * 擴展點:持久化庫存數(shù)據(jù) persistInventoryDetailAndSyncTask(context); // 持久化操作明細及同步任務(wù) 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)是基于對象之間的組合關(guān)系(composition)實現(xiàn),而模板方法是基于類之間的繼承關(guān)系(inheritance)實現(xiàn),我們結(jié)合系統(tǒng)實際情況,并基于「組合優(yōu)先于繼承」的考量,最終選擇了使用回調(diào)的方式進行代碼重構(gòu)。其原因大致如下:
- 繼承打破封裝性:《Effective Java》在《第18條:復(fù)合優(yōu)先于繼承》中提到,繼承是實現(xiàn)代碼重用的有力手段,但它并非永遠是完成這項工作的最佳工具。使用不當會導致軟件變得很脆弱。與方法調(diào)用不同的是,繼承打破了封裝性。換句話說,子類依賴于其超類中特定功能的實現(xiàn)細節(jié)。超類的實現(xiàn)有可能會隨著發(fā)行版本的不同而有所變化,如果真的發(fā)生了變化,子類可能會遭到破壞,即使它的代碼完全沒有改變。同時,子類可能繼承了定義在父類,但其自身并不需要的方法,有違最小知識原則(Least Knowledge Principle)。子類可能因此錯誤地覆蓋并改變了父類中的方法實現(xiàn),導致父類功能的封裝性被破壞。而如果我們使用對象間組合的方式,則可以避免此類問題的出現(xiàn)。
- 接口優(yōu)于抽象類:仍舊是《Effective Java》,在《第20條:接口優(yōu)于抽象類》中提到,因為Java只允許單繼承,所以用抽象類(模板方法便是基于抽象類實現(xiàn))作為類型定義受到了限制。而現(xiàn)有的類可以很容易被更新,以實現(xiàn)新的接口。接口是定義混合類型(mixin)的理想選擇,允許構(gòu)造非層次結(jié)構(gòu)的類型框架。與之相反的做法是編寫一個臃腫的類層次,對于每一種要被支持的屬性組合,都包含一個單獨的類。如果整個類型系統(tǒng)中有n個屬性,那么就必須支持2n種可能的組合,這種現(xiàn)象被稱為「組合爆炸」,即需要定義過多的類。
- 組合替代繼承:最后,王爭的《設(shè)計模式之美》中提到,繼承主要有三個作用:表示is-a關(guān)系,支持多態(tài)性,以及代碼復(fù)用。而這三個作用都可以通過其他手段達成:is-a關(guān)系可以通過組合和接口的has-a關(guān)系來替代;多態(tài)性可以利用接口來實現(xiàn);代碼復(fù)用則可以通過組合和委托來實現(xiàn)。因此從理論上講,通過組合、接口、委托三個技術(shù)手段,我們可以替換掉繼承,在項目中不用或者少用復(fù)雜的繼承關(guān)系。這種對象間組合的設(shè)計方式比類間繼承的方式更加符合開閉原則(Open-Closed Principle)(注4)。
結(jié)合我們前文中介紹的AbstractQueuedSynchronizer的案例,仔細閱讀其源碼可以發(fā)現(xiàn),作者通過代碼上的精心設(shè)計規(guī)避了上文提到的「繼承打破封裝性」的問題。比如,為了不使模板中的骨架邏輯錯誤地被子類覆蓋,相關(guān)方法(如acquire和release)均使用了final關(guān)鍵字進行修飾;而對于某些必須由子類實現(xiàn)的擴展點,在AQS抽象類中均會拋出UnsupportedOperationException異常。然而此處不將擴展點定義為抽象方法,而是提供拋出異常的默認實現(xiàn)的原因,個人認為是由于AQS中定義了不同形式的獲取和釋放操作,而其鎖和同步器的具體實現(xiàn)雖然會繼承所有這些方法,但依據(jù)自身的應(yīng)用場景往往只關(guān)心其中某種版本。比如ReentrantLock中的AQS實現(xiàn)僅關(guān)心排他式的版本(即tryAcquire和tryRelease),而Semaphore中的AQS實現(xiàn)僅關(guā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ā)框架,應(yīng)用場景相對固定,且更加側(cè)重性能方面的考慮,其擴展性較低無可厚非。而對于如Spring的上層框架,在設(shè)計時就必須更多地考慮可擴展性的支持。如前文提到的RedisTemplate,借助其維護的RedisConnectionFactory即可獲得不同類型的底層Redis連接實現(xiàn);而對于其不同形式的管道執(zhí)行方法(管道/事務(wù) 管道),用戶只需要實現(xiàn)并傳入對應(yīng)的回調(diào)接口(RedisCallback/SessionCallback)即可,而不必感知其不需要的方法定義。這兩點便是通過組合委托和回調(diào)的方式實現(xiàn)的,相較AQS而言顯得更加靈活簡潔,如下圖所示:
五 再論重構(gòu)
回到我們的數(shù)農(nóng)WMS庫存操作重構(gòu),雖然ContainerInventoryOperationTemplate與ContainerInventoryOperationHandler之間的關(guān)系非常接近策略模式(Strategy),但由于我們的「模板類」使用Spring的單例模式進行管理,其中并沒有單獨維護某個指定的庫存操作handler,而是通過方法傳參的方式觸達它們,因此筆者更傾向于使用回調(diào)描述兩者之間的代碼結(jié)構(gòu)。不過讀者不必對兩者命名的差異過于糾結(jié),因為它們的思路是非常相近的。
隨著數(shù)農(nóng)WMS代碼重構(gòu)的推進,以及對更多庫存操作業(yè)務(wù)場景的覆蓋,我們不斷發(fā)現(xiàn)這套重構(gòu)后的代碼框架具備優(yōu)秀的可擴展性。例如,當我們需要為上游系統(tǒng)提供「庫存增加并占用」的庫存操作原子能力支持時,我們發(fā)現(xiàn)可以使用組合委托的方式復(fù)用「庫存增加」和「庫存占用」的基本庫存操作能力,從而簡潔高效地完成功能開發(fā)。而這點若是單純基于模板方法的類間繼承的方式是無法實現(xiàn)的。具體代碼和類圖如下:
// 庫存增加并占用@Componentpublic class IncreaseAndOccupyOperationHandler implements ContainerInventoryOperationHandler { @Resource private IncreaseOperationHandler increaseOperationHandler; // 組合「庫存增加」操作handler @Resource private OccupyOperationHandler occupyOperationHandler; // 組合「庫存占用」操作handler // 委托「庫存占用」操作handler進行前置操作校驗,判斷是否單據(jù)占用已存在 @Override public void checkPreOperationIfNecessary(ContainerInventoryOperationTemplate.OperationContext context) { occupyOperationHandler.checkPreOperationIfNecessary(context); } // 委托「庫存增加」操作handler進行庫存信息校驗 @Override public void getAndCheckCurrentInventory(ContainerInventoryOperationTemplate.OperationContext context) { increaseOperationHandler.getAndCheckCurrentInventory(context); } // 委托「庫存增加」、「庫存占用」操作handler進行「庫存增加并占用」操作 @Override public void operateInventory(ContainerInventoryOperationTemplate.OperationContext context) { increaseOperationHandler.operateInventory(context); occupyOperationHandler.operateInventory(context); } // 其余代碼略}
最后,無論是基于模板方法還是回調(diào)的方式對庫存操作進行重構(gòu),雖然我們可以獲得代碼復(fù)用以及擴展便利的好處,但是「模板類」中骨架邏輯的復(fù)雜性,其實是所有庫存操作復(fù)雜性的總和(個人認為這一點在Spring框架的代碼中也有所體現(xiàn))。比如,庫存增加操作在某些場景下需要在開啟數(shù)據(jù)庫事務(wù)前獲取分布式鎖,庫存占用操作需要判斷相關(guān)單據(jù)是否已經(jīng)占用了庫存等。而模板代碼中的骨架邏輯需要為所有這些流程分支提供擴展點,從而支持各種類型的庫存操作。此外,修改模板骨架邏輯的代碼時也需要小心謹慎,因為一旦模板代碼本身出錯,可能會影響所有的庫存操作。這些都對我們代碼編寫的質(zhì)量和可維護性提出更高的要求。
六 結(jié)語
代碼重構(gòu)并且總結(jié)成文的過程要求不斷地學習、思辨和實踐,也讓自己獲益良多。
注解
- 對AQS使用了模板方法設(shè)計模式的「官方論斷」可見于其作者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. 此外,文中還包含了對等待線程FIFO隊列(CLH變體)、公平性、框架性能等方面的詳細討論。 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上的某個問答可作為參考: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è)計模式》
https://book.douban.com/subject/1052241/
- The java.util.concurrent Synchronizer Framework
http://gee.cs.oswego.edu/dl/papers/aqs.pdf
- 《Java并發(fā)編程實戰(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è)計模式之美》
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)載。