狀態與策略——審批操作的兩種方案
審批操作是ERP或OA系統中必不可少的功能之一。這裡介紹兩種我設計的用於審批操作的方案,並藉此就“狀態模式”與“策略模式”提出一點自己的理解。
別問我為什麼不使用工作流引擎等工具來實現審批功能。做第一版方案時,我孤陋寡聞得並不知道有這個東西。後來引入工作流框架會導致學習曲線驟然上揚,不太划算。
背景
背景無需過多介紹,不外乎有一些資料/任務/請求,需要由領導們點一下頭或者按鈕。
思路
由於孤陋寡聞,在得到需求之後,我第一反應不是“工作流”,而是“狀態機”。它從“提交”狀態開始,流經“已初審”“已終審”或者“初審駁回”“終審駁回”等狀態,進入終態。
這個狀態機如下圖所示:
當然,狀態機中狀態的名稱、狀態間的流轉,是與業務需求緊密相關的。例如,有些業務會要求在“已終審”狀態下執行“駁回”操作後進入“終審駁回”狀態,而有些則要求返回“已初審”狀態。不過萬變不離其宗,種種流程最終都能歸納到“狀態機”中來。
在這個思路下,我用了兩種不同的設計模式來實現需求——狀態模式和策略模式,它們都很好的完成了任務。需要多說一句的是,這是兩個不同系統下獨立的兩次實現,而不是一個系統中的“原始版”和“改進版”。因而,兩個方案之間並沒有非常顯著的優劣對比,本文的重點也不是二者的“優劣”對比。
方案一:狀態模式
-
狀態模式
首先來回顧一下我們常說的狀態模式。簡便起見,這裡只提供類圖。
其中的核心是“狀態”介面。這個介面中有N個方法,對應的是狀態機中的N個狀態。每個方法負責從當前狀態遷移到另一個狀態上——一般是別的狀態,也可以仍然是當前狀態。
每個具體的狀態都繼承自這個介面,並在實現類中封裝自己所需要的資料、重寫自己的狀態遷移操作。 -
我的方案
在我的設計方案中,類圖則是這個樣子的:
與“教科書”上的類圖相似的,是“狀態”介面(Examiner),以及各個實際狀態所對應的子類。
與之不同的是,雖然我的狀態機中有五個狀態,但是由於每個狀態最多都只有兩個狀態遷移操作(通過,或者駁回),因此,狀態介面中我只定義了兩個方法。
還有一點不同在於,我在Examiner介面下,加了一個預設的實現類(ExaminerAsDefault)。這個類實際上什麼都不做,每個方法都直接丟擲UnSupportedOperationException。這個類的作用是簡化子類,使得每個子類只需要重寫自己關心的方法,而不需要重寫無關方法。當然,Java 8為介面引入的預設方法,可以實現同樣的功能,這是後話。此外,由於業務需求中每次只做一步狀態遷移,因此Examiner介面不需要再返回自己。還有一點不同的是,這個方案中,狀態遷移操作與狀態資料被拆開了——遷移操作由Examiner定義,狀態資料則用Dto來封裝。 -
擴充套件
當出現新的狀態、或者新的遷移操作怎麼辦呢?
出現新的狀態時,建立一個新的“狀態”子類,並實現對應的“狀態遷移”方法就行了。出現新的遷移操作時則更簡單,只需要做第二步就可以了。
方案二:策略模式
-
我的方案
在我的設計方案中,類圖則是這樣的:
可以說這是一個“標準”的策略模式類圖。介面定義從一個狀態到另一個狀態的遷移動作,不同的子類用不同的“策略”去實現它——例如從“已提交”到“已初審”,或者從“已初審”到“初審駁回”,等等。
狀態相關的資料,仍然由單獨的Dto來儲存和傳遞。 -
擴充套件
策略模式下,如何增加新的狀態、新的遷移操作呢?
由於策略模式僅僅定義了“狀態遷移”動作,因此,無論是增加新的狀態、還是增加新的遷移操作,都只需要增加對應的子類即可。
對比
我並不喜歡比較不同設計模式之間的區別。但這裡仍可以多說幾句。
用狀態模式實現狀態機,大概是一個最直觀、最容易想到的設計。但是,標準的狀態模式將狀態資料也封裝到狀態類中。這使得這個類無法用單例實現。另外,由於狀態介面中,對應每一個狀態都有一個方法,這可能會使得部分子類非常的大。
用策略模式實現狀態機,與狀態機思想是有衝突的。狀態機是以“狀態”為本,狀態遷移操作為輔;而策略模式卻專注於狀態遷移操作,“狀態”的概念淡化得幾乎消失了。此外,與狀態模式中的“超級類”相反,策略模式可能導致“類爆炸”。
兩種模式之間的分界線,也許只是概念上的“以狀態為本”或“以操作為本”。就實踐上來說,像我的方案中那樣,將狀態模式中的資料與操作拆分開,那麼整個方案與策略模式其實相去無幾。
這是我不喜歡比較不同設計模式之間區別的原因。由於設計模式的變化、組合非常多,很多時候不同設計模式之間的界限僅僅存在於概念上、思想上,而不在實踐中。費盡心思去分析“如何區分23種設計模式”,只在學習階段有一點意義。我們更應該關注設計模式適用的業務場景、業務問題,以及如何實現它們。
畢竟,科學可以滿足於“認識世界”,技術必須要以“改造世界”為目標。
參考文獻: