在《重構》這本書中,提到了不少種的代碼的壞味道,有一種就是重複的代碼,以及各類各樣的Switch 與 if/else 判斷,面對這種狀況,能夠利用 java 的多態來進行替換。java
今天要講的模板方法就是其中一種利用多態減小重複代碼的手段~設計模式
注:文中代碼片斷在實際項目中均已廢棄,不過畢竟與業務需求相關,所以代碼片斷僅保留與模板方法相關的部分,不保證代碼片斷的實際運行緩存
以往咱們的修改資源屬性和路由時,都是實時生效的,改了就是改了。ide
那如今用戶有了這麼一種需求,個人路由修改時,不及時生效,當用戶確認修改時再生效,過程當中不滿意還能夠回滾到屬性與路由關係最開始的狀態。ui
咱們將這種操做稱爲流程中電路(其實這裏比較相似於Oracle自身的回滾操做實現)編碼
那麼這種需求該怎麼實現呢?設計
以電路資源爲例,電路的這種路由關聯關係存儲於 電路路由表中,咱們再搞個歷史路由表,專門用來存放最初始的路由關係狀態。只要確保每次修改資源時,都確定已將最初始狀態緩存入歷史路由表中便可。日誌
這樣便可確保路由資源在修改時,其原始信息不會丟失。code
一句話總結:確保每次修改路由 / 屬性時,都已將相關信息備份。對象
圍繞上述業務場景,咱們來看看最開始的需求:
最開始僅僅要求了電路資源的流程需求,在電路資源的路由實現角度而言,分爲這麼幾個步驟:
來讓咱們看看代碼:
public int saveRouteHistory(ResIdentify identify, List<OperationResEntity> routes) { int result = ResCommConst.ZERO; String woId = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.WO_ID); String recordOprType = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.RECORD_OPR_TYPE); // 根據工單與事務狀態決定是否走流程中處理邏輯 if(StringUtil.hasText(woId)&& (ResDictValueConst.MNT_ADD.equals(recordOprType) ||ResDictValueConst.MNT_DELETE.equals(recordOprType) ||ResDictValueConst.MNT_UPDATE.equals(recordOprType))){ // 一、將電路路由表的數據寫入到回滾表 List<Map<String, String>> columnInfo = new ArrayList<Map<String, String>>(); Map<String, String> column = new HashMap<String, String>(); column.put(ResAttrConst.COLUMN_NAME, ResAttrConst.CIRCUIT_ID); column.put(ResAttrConst.COLUMN_VALUE, identify.getResId()); columnInfo.add(column); trsCallOuterModuleService.writeTableLog(columnInfo, ResAttrConst.CIRCUIT_ROUTE, woId); // 二、帶工單刪除只須要更新路由的工單和事物狀態 Map<String, Object> params = new HashMap<String, Object>(); params.put(ResAttrConst.CIRCUIT_ID, identify.getResId()); params.put(ResAttrConst.WO_ID, woId); params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE); circuitDao.updateTrsCirRoute(params); // 三、路由表中資源實例寫入到回滾表中並更新路由狀態 for (OperationResEntity route : routes) { result++; intelligentWriteResHis(woId, route); route.addOrUpdateProperty(ResAttrConst.WO_ID, woId); route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE); route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID,ResDictValueConst.OPR_STATE_PREE_FREE); route.updatePropertys(); } } return result; }
一開始這樣寫其實沒什麼問題,代碼一共不到 40 行,同時相對清晰的實現了功能需求。
若是需求就是這樣,後期維護成本不怎麼高,其實沒什麼改的必要(我比較懶。。。)
然而生活與工做中卻老是 「驚喜」 多過平淡,挫折多過順風。那該怎麼辦?日子仍是要過,積極面對唄
這不,客戶又來了個需求時,不只電路要這樣作,電路的路由——通道也須要支持這種流程中操做。
因爲通道自身也有路由,因此其實上述相同的代碼邏輯通道也須要實現一份。
先來看看常規的代碼是寫的呢?
ChannelDataOperation.java public int saveRouteHistory(ResIdentify identify, List<OperationResEntity> routes) { int result = ResCommConst.ZERO; String woId = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.WO_ID); String recordOprType = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.RECORD_OPR_TYPE); // 根據工單與事務狀態決定是否走流程中處理邏輯 if(StringUtil.hasText(woId)&& (ResDictValueConst.MNT_ADD.equals(recordOprType) ||ResDictValueConst.MNT_DELETE.equals(recordOprType) ||ResDictValueConst.MNT_UPDATE.equals(recordOprType))){ // 一、將電路路由表的數據寫入到回滾表 List<Map<String, String>> columnInfo = new ArrayList<Map<String, String>>(); Map<String, String> column = new HashMap<String, String>(); column.put(ResAttrConst.COLUMN_NAME, ResAttrConst.CHANNEL_ID); column.put(ResAttrConst.COLUMN_VALUE, identify.getResId()); columnInfo.add(column); trsCallOuterModuleService.writeTableLog(columnInfo, ResAttrConst.CHANNEL_ROUTE, woId); // 二、帶工單刪除只須要更新路由的工單和事物狀態 Map<String, Object> params = new HashMap<String, Object>(); params.put(ResAttrConst.CHANNEL_ID, identify.getResId()); params.put(ResAttrConst.WO_ID, woId); params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE); channelDao.updateTrsChannelRoute(params); // 三、路由表中資源實例寫入到回滾表中並更新路由狀態 for (OperationResEntity route : routes) { result++; intelligentWriteResHis(woId, route); route.addOrUpdateProperty(ResAttrConst.WO_ID, woId); route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE); route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID,ResDictValueConst.OPR_STATE_PREE_FREE); route.updatePropertys(); } } return result; }
乍一看是否是都同樣呢?其實在第16行,19行,26行仍是能發現少量不一樣的。
這種開發方式,其實就是咱們最多見的 Ctrl C/V 開發法。
這種開發辦法有什麼弊端呢?
拋去剛纔說的第16行,19行,26行不論,既然其餘的代碼都是同樣的,那咱們就先把能抽取的重複代碼抽取出來唄~
那麼問題來了,抽到哪裏?
對於一個類內部的重複代碼,咱們能夠將重複代碼抽取到這個類內部的一個獨立方法中(ps: IDEA 中抽取方法的快捷鍵是 ctrl + alt + M)
可是這個例子中,重複代碼分散在了不一樣的類中。因此,咱們只能新建一個新類,將重複的方法都放在這個新類中。
HisRouteResDataOperation.java protected int logRouteAndUpdateState(String woId, List<OperationResEntity> routes) { int result = ResCommConst.ZERO; for (OperationResEntity route : routes) { result++; // 一、路由表中資源實例寫入到回滾表中 intelligentWriteResHis(woId, route); route.addOrUpdateProperty(ResAttrConst.WO_ID, woId); route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE); route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID,ResDictValueConst.OPR_STATE_PREE_FREE); route.updatePropertys(); } return result; }
如上述代碼,咱們將電路和通道中徹底重複的一段代碼抽取成了方法,放在了 HisRouteResDataOperation 中,接下來使電路和通道的操做類繼承這個類就能夠正常使用了。
接下來看看這時 CircuitDataOperation.java 是怎樣的:
public int saveRouteHistory(ResIdentify identify, List<OperationResEntity> routes) { int result = ResCommConst.ZERO; String woId = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.WO_ID); String recordOprType = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.RECORD_OPR_TYPE); if(StringUtil.hasText(woId)&& (ResDictValueConst.MNT_ADD.equals(recordOprType) ||ResDictValueConst.MNT_DELETE.equals(recordOprType) ||ResDictValueConst.MNT_UPDATE.equals(recordOprType))){ // 1.0、將電路路由表的數據寫入到回滾表 List<Map<String, String>> columnInfo = new ArrayList<Map<String, String>>(); Map<String, String> column = new HashMap<String, String>(); column.put(ResAttrConst.COLUMN_NAME, ResAttrConst.CIRCUIT_ID); column.put(ResAttrConst.COLUMN_VALUE, identify.getResId()); columnInfo.add(column); trsCallOuterModuleService.writeTableLog(columnInfo, ResAttrConst.CIRCUIT_ROUTE, woId); // 1.一、帶工單刪除只須要更新路由的工單和事物狀態 Map<String, Object> params = new HashMap<String, Object>(); params.put(ResAttrConst.CIRCUIT_ID, identify.getResId()); params.put(ResAttrConst.WO_ID, woId); params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE); circuitDao.updateTrsCirRoute(params); result += logRouteAndUpdateState(woId, routes); } return result; }
是否是簡化了一點?
這樣,咱們就將通道和電路的其中兩塊重複代碼提取出來了。
不過同時也能夠看到,在第3行,第5行,還有第8行,咱們用了 Spring 的註解,留心記一下,這會在後面致使一個小問題。
雖然咱們已經將代碼中的兩部分重複代碼移植入父類中,看起來清晰了一點。可是尚未結束,咱們發現其實電路和通道在寫回滾的邏輯上其實挺類似的,
1. 先判斷下是否在流程中, 2. 將路由關係寫回滾表並更新路由狀態, 3. 將路由資源狀態寫回滾表並更新狀態。
禪師:那麼咱們若是想將這種流程上的前後順序進行復用,又該怎麼辦呢?
王小黑:既然你們這麼類似,那麼將這部分代碼直接放入父類中很差嗎?
禪師:嗯,小黑你再好好考慮考慮,還記得咱們在進階需求中的常規解決辦法中說的嗎?
王小黑:我知道了,通道與電路的保存邏輯,在第16行,19行,26行有一些區別!由於這少量的不一樣(其實就是咱們常說的硬編碼),因此咱們不能直接將方法抽取到父類中。
禪師:嗯,很好,那你知道該怎麼解決嗎?
前文講到,因爲存在硬編碼,咱們沒有辦法直接將代碼邏輯移植入父類中。
而模板方法模式專門爲此而生,讓咱們看看該怎麼寫吧~
TrsHisRouteResDataOperation.java ······ public int saveRouteHistory(ResIdentify identify, List<OperationResEntity> routes) { int result = ResCommConst.ZERO; String woId = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.WO_ID); String recordOprType = ResEntityUtil.getPropertyValueByIdentify(context, identify, ResAttrConst.RECORD_OPR_TYPE); if (StringUtil.hasText(woId) && (ResDictValueConst.MNT_ADD.equals(recordOprType) || ResDictValueConst.MNT_DELETE.equals(recordOprType) || ResDictValueConst.MNT_UPDATE.equals(recordOprType))) { // 一、將路由關係表的數據寫入到回滾表 List<Map<String, String>> columnInfo = new ArrayList<Map<String, String>>(); Map<String, String> column = new HashMap<String, String>(); column.put(ResAttrConst.COLUMN_NAME, getResId()); column.put(ResAttrConst.COLUMN_VALUE, identify.getResId()); columnInfo.add(column); getTrsCallOuterModuleService().logResProperties(columnInfo, getRouteTableName(), woId); // 二、更新當前路由關係表中路由記錄的工單和事物狀態爲刪除態 Map<String, Object> params = new HashMap<String, Object>(); params.put(ResAttrConst.WO_ID, woId); params.put(getResId(), identify.getResId()); params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE); updateRouteRecord(params); for (OperationResEntity route : routes) { result++; // 一、路由表中資源實例寫入到回滾表中 trsRouteOperationService.logPropertiesToHis(route.getIdentify(), woId); // 二、更新路由狀態 route.addOrUpdateProperty(ResAttrConst.WO_ID, woId); route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE); route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID, ResDictValueConst.OPR_STATE_PREE_FREE); route.updatePropertys(); } } // TODO wang.xi 解決正常的路由保存歷史表邏輯 return result; } /** * 鉤子方法,更新路由的工單與事務狀態 * @param params 工單與事務狀態信息 */ protected void updateRouteRecord(Map<String, Object> params){}; protected String getRouteTableName(){return ""}; protected String getResId(){return ""};
單純的看這種飽含業務規則的代碼確定是看不進去的,因此這裏咱們着重看下第16行,19行,26行。
禪師:前文講了,這幾行裏面由於存在硬編碼,若是簡單的將電路的代碼上移至父類中,那麼通道資源使用這套代碼就會有問題了,小黑,你有什麼好辦法嗎?
王小黑:這個我知道,有個最簡單的解決方案,反正電路和通道類內都有相似的方法需求,針對第 26 行,咱們在 TrsHisRouteResDataOperation 中編寫一個空的 updateRouteRecord() 方法使他能找到這個方法,不報錯不就行了嗎?子類利用 java 的多態機制,實現一下這個方法就行了。(其餘部分雷同)
禪師:嗯,你說的確實有用,上面這幾行代碼也確實是按照你說的作的。可是這樣有個缺點,仍是以前說的,若是之後傳輸段也要拓展呢?採起這種方案,即使傳輸段沒有實現這個方法,方法編譯時期也不會報錯啊!
王小黑:那咱們退一步,還有個解決方案,將這個 updateRouteRecord() 方法定義爲抽象方法,這不就解決你說的拓展問題了嗎?
禪師:根據 java 的語法,若是你將一個方法定義爲抽象方法,那麼這個類也必須是抽象類了。
王小黑:抽象類就抽象類,又有什麼所謂?
禪師:小黑,too young 了吧~ 你仔細看看第 5 行與第 32 行代碼,是否是有個 context 與 trsRouteOperationService 對象? 這兩個對象都是 Spring 中動態注入的對象,你能夠查查 Spring 動態注入與 java 抽象類的關係,就會絕望的發現,Spring 竟然不支持抽象類的自動注入。。。(箇中緣由,等有機會再介紹 Spring 原理的時候再介紹給你們吧)
王小黑:唉,這也是坑那也是坑,橫豎都有問題,那麼咱們還玩不玩了?
再仔細思考下剛纔示例中的 updateRouteRecord() 方法,咱們在父類引入這個鉤子方法,就是爲了利用 java 的多態機制,使父類可以只關心方法的存在與否,而不用再關心具體的實現。
雖然 Spring 不支持抽象類的自動注入,咱們依舊能夠進一步靈活運用模板方法模式中的鉤子方法思想,將類中所須要的屬性,建立好getter 方法做爲鉤子,這樣就再也不侷限與 Spring 自身的限制了。
新的代碼以下:
TrsHisRouteResDataOperation.java public int saveRouteHistory(ResIdentify identify, List<OperationResEntity> routes) { int result = ResCommConst.ZERO; String woId = ResEntityUtil.getPropertyValueByIdentify(getContext(), identify, ResAttrConst.WO_ID); String recordOprType = ResEntityUtil.getPropertyValueByIdentify(getContext(), identify, ResAttrConst.RECORD_OPR_TYPE); if (StringUtil.hasText(woId) && (ResDictValueConst.MNT_ADD.equals(recordOprType) || ResDictValueConst.MNT_DELETE.equals(recordOprType) || ResDictValueConst.MNT_UPDATE.equals(recordOprType))) { // 一、將路由關係表的數據寫入到回滾表 List<Map<String, String>> columnInfo = new ArrayList<Map<String, String>>(); Map<String, String> column = new HashMap<String, String>(); column.put(ResAttrConst.COLUMN_NAME, getResId()); column.put(ResAttrConst.COLUMN_VALUE, identify.getResId()); columnInfo.add(column); getTrsCallOuterModuleService().logResProperties(columnInfo, getRouteTableName(), woId); // 二、更新當前路由關係表中路由記錄的工單和事物狀態爲刪除態 Map<String, Object> params = new HashMap<String, Object>(); params.put(ResAttrConst.WO_ID, woId); params.put(getResId(), identify.getResId()); params.put(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_DELETE); updateRouteRecord(params); for (OperationResEntity route : routes) { result++; // 一、路由表中資源實例寫入到回滾表中 getTrsRouteOperationService().logPropertiesToHis(route.getIdentify(), woId); // 二、更新路由狀態 route.addOrUpdateProperty(ResAttrConst.WO_ID, woId); route.addOrUpdateProperty(ResAttrConst.RECORD_OPR_TYPE, ResDictValueConst.MNT_UPDATE); route.addOrUpdateProperty(ResAttrConst.OPR_STATE_ID, ResDictValueConst.OPR_STATE_PREE_FREE); route.updatePropertys(); } } return result; } /** * 鉤子方法,更新路由的工單與事務狀態 * @param params 工單與事務狀態信息 */ protected abstract void updateRouteRecord(Map<String, Object> params); public abstract String getRouteTableName(); public abstract String getResId(); public abstract ResContext getContext() ; public abstract TrsRouteOperationService getTrsRouteOperationService() ;
以上就是模板方法的所有思想了,但願對你們有所幫助 ^_^
在設計模式中,模板方法應該算是比較簡單易懂的了,這是理論上而言。
在實際項目中,咱們總會由於各類各樣的困難,好比懶惰(別笑,這真的是個很充分的理由),好比對象類型不一樣,好比某一步方法名不一樣等等的緣由,而沒法將其抽象爲一個模板方法。
可是不論是由於什麼緣由,卻終究是形成了代碼中各類雷同邏輯的冗餘。好比更早之前的傳輸帶路由資源(通道,電路等)的保存邏輯。由於從流程上來講,就那麼幾個:
準備對象 —>
刪除路由 —>
驗證路由狀態並計算序號 —>
保存路由 —>
設置路由狀態爲佔用 —>
刷新 A/Z 端屬性信息 —>
刷新文本路由信息 —>
記錄日誌
試想,這麼 8 個流程,換作是你,每一個方法得用多少行來實現?同時具備這 8 個流程的資源還有 傳輸通道,傳輸電路,傳輸段三種。
算算開發的複雜度是幾乘幾呢?後期維護時,流程有變換時,又須要該多少行代碼呢?
不過雖說了這麼多,可是傳輸路由保存的代碼並無使用模板方法,同時也依舊很清晰,至因而怎麼作到的,先賣個關子,咱們下回再聊。
對了,你們能夠圍繞今天講的模板方法先思考一下本身模塊的代碼中是否也有應該使用模板方法的場景呢~