摘要:雖然不支持抽象類的自動注入,我們依舊可以進一步靈活運用模板方法模式中的鉤子方法思想,將類中所需要的屬性,創建好方法作為鉤子,這樣就不再局限與自身的限制了。
前言
在《重構》這本書中,提到了很多種的代碼的壞味道,有一種就是重復的代碼,以及各種各樣的Switch 與 if/else 判斷,面對這種情況,可以利用 java 的多態來進行替換。
今天要講的模板方法就是其中一種利用多態減少重復代碼的手段~
注:文中代碼片段在實際項目中均已廢棄,不過畢竟與業務需求相關,因此代碼片段僅保留與模板方法相關的部分,不保證代碼片段的實際運行
業務場景以往我們的修改資源屬性和路由時,都是實時生效的,改了就是改了。
那現在用戶有了這么一種需求,我的路由修改時,不及時生效,當用戶確認修改時再生效,過程中不滿意還可以回滾到屬性與路由關系最開始的狀態。
我們將這種操作稱為流程中電路(其實這里比較類似于Oracle自身的回滾操作實現)
那么這種需求該怎么實現呢?
以電路資源為例,電路的這種路由關聯關系存儲于 電路路由表中,我們再搞個歷史路由表,專門用來存放最初始的路由關系狀態。只要確保每次修改資源時,都肯定已將最初始狀態緩存入歷史路由表中即可。
這樣即可確保路由資源在修改時,其原始信息不會丟失。
一句話總結:確保每次修改路由 / 屬性時,都已將相關信息備份。
最初的需求:圍繞上述業務場景,我們來看看最開始的需求:
最開始僅僅要求了電路資源的流程需求,在電路資源的路由實現角度而言,分為這么幾個步驟:
將電路路由關系寫入回滾表
將當前路由關系表中記錄刪除
將各個路由資源的屬性寫入回滾表
將路由表中涉及的資源狀態都設置為流程中的修改狀態
解決方案:來讓我們看看代碼:
public int saveRouteHistory(ResIdentify identify, Listroutes) { 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、將電路路由表的數據寫入到回滾表 List
一開始這樣寫其實沒什么問題,代碼一共不到 40 行,同時相對清晰的實現了功能需求。
如果需求就是這樣,后期維護成本不怎么高,其實沒什么改的必要(我比較懶。。。)
進階需求:然而生活與工作中卻總是 “驚喜” 多過平淡,挫折多過順風。那該怎么辦?日子還是要過,積極面對唄
這不,客戶又來了個需求時,不僅電路要這樣做,電路的路由——通道也需要支持這種流程中操作。
由于通道自身也有路由,所以其實上述相同的代碼邏輯通道也需要實現一份。
解決方案1——常規模式:先來看看常規的代碼是寫的呢?
ChannelDataOperation.java public int saveRouteHistory(ResIdentify identify, Listroutes) { 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、將電路路由表的數據寫入到回滾表 List > columnInfo = new ArrayList >(); Map column = new HashMap (); 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); // 2、帶工單刪除只需要更新路由的工單和事物狀態 Map params = new HashMap (); 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); // 3、路由表中資源實例寫入到回滾表中并更新路由狀態 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, Listroutes) { int result = ResCommConst.ZERO; for (OperationResEntity route : routes) { result++; // 1、路由表中資源實例寫入到回滾表中 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, Listroutes) { 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 > columnInfo = new ArrayList >(); Map column = new HashMap (); 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.1、帶工單刪除只需要更新路由的工單和事物狀態 Map params = new HashMap (); 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, Listroutes) { 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、將路由關系表的數據寫入到回滾表 List > columnInfo = new ArrayList >(); Map column = new HashMap (); column.put(ResAttrConst.COLUMN_NAME, getResId()); column.put(ResAttrConst.COLUMN_VALUE, identify.getResId()); columnInfo.add(column); getTrsCallOuterModuleService().logResProperties(columnInfo, getRouteTableName(), woId); // 2、更新當前路由關系表中路由記錄的工單和事物狀態為刪除態 Map params = new HashMap (); 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++; // 1、路由表中資源實例寫入到回滾表中 trsRouteOperationService.logPropertiesToHis(route.getIdentify(), woId); // 2、更新路由狀態 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 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, Listroutes) { 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))) { // 1、將路由關系表的數據寫入到回滾表 List > columnInfo = new ArrayList >(); Map column = new HashMap (); column.put(ResAttrConst.COLUMN_NAME, getResId()); column.put(ResAttrConst.COLUMN_VALUE, identify.getResId()); columnInfo.add(column); getTrsCallOuterModuleService().logResProperties(columnInfo, getRouteTableName(), woId); // 2、更新當前路由關系表中路由記錄的工單和事物狀態為刪除態 Map params = new HashMap (); 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++; // 1、路由表中資源實例寫入到回滾表中 getTrsRouteOperationService().logPropertiesToHis(route.getIdentify(), woId); // 2、更新路由狀態 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 params); public abstract String getRouteTableName(); public abstract String getResId(); public abstract ResContext getContext() ; public abstract TrsRouteOperationService getTrsRouteOperationService() ;
以上就是模板方法的全部思想了,希望對大家有所幫助 ^_^
小結在設計模式中,模板方法應該算是比較簡單易懂的了,這是理論上而言。
在實際項目中,我們總會因為各種各樣的困難,比如懶惰(別笑,這真的是個很充分的理由),比如對象類型不同,比如某一步方法名不同等等的原因,而無法將其抽象為一個模板方法。
但是不管是因為什么原因,卻終究是造成了代碼中各種雷同邏輯的冗余。比如更早以前的傳輸帶路由資源(通道,電路等)的保存邏輯。因為從流程上來說,就那么幾個:
準備對象 —>
刪除路由 —>
驗證路由狀態并計算序號 —>
保存路由 —>
設置路由狀態為占用 —>
刷新 A/Z 端屬性信息 —>
刷新文本路由信息 —>
記錄日志
試想,這么 8 個流程,換做是你,每個方法得用多少行來實現?同時具有這 8 個流程的資源還有 傳輸通道,傳輸電路,傳輸段三種。
算算開發的復雜度是幾乘幾呢?后期維護時,流程有變換時,又需要該多少行代碼呢?
不過雖然說了這么多,但是傳輸路由保存的代碼并沒有使用模板方法,同時也依舊很清晰,至于是怎么做到的,先賣個關子,我們下回再聊。
對了,大家可以圍繞今天講的模板方法先思考一下自己模塊的代碼中是否也有應該使用模板方法的場景呢~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70661.html
摘要:簡介官網上對它的定位是一個微開發框架。另外一個必須理解的概念是,簡單來說就是一套和框架應用之間的協議。功能比較豐富,支持解析自動防止攻擊繼承變量過濾器流程邏輯支持代碼邏輯集成等等。那么,從下一篇文章,我們就正式開始源碼之旅了 文章屬于作者原創,原文發布在個人博客。 flask 簡介 Flask 官網上對它的定位是一個微 python web 開發框架。 Flask is a micro...
摘要:面向對象三大特征繼承性多態性封裝性接口。第五階段封裝一個屬于自己的框架框架封裝基礎事件流冒泡捕獲事件對象事件框架選擇框架。核心模塊和對象全局對象,,,事件驅動,事件發射器加密解密,路徑操作,序列化和反序列化文件流操作服務端與客戶端。 第一階段: HTML+CSS:HTML進階、CSS進階、div+css布局、HTML+css整站開發、 JavaScript基礎:Js基礎教程、js內置對...
摘要:面向對象三大特征繼承性多態性封裝性接口。第五階段封裝一個屬于自己的框架框架封裝基礎事件流冒泡捕獲事件對象事件框架選擇框架。核心模塊和對象全局對象,,,事件驅動,事件發射器加密解密,路徑操作,序列化和反序列化文件流操作服務端與客戶端。 第一階段: HTML+CSS:HTML進階、CSS進階、div+css布局、HTML+css整站開發、 JavaScript基礎:Js基礎教程、js內置對...
摘要:簡介安裝完成后輸入開始初始化,生成默認的配置文件命令的實現在文件中目錄則包含了初始化相關的模板命令類類繼承了的類,實現為一個命令行的命令構造函數構造函數主要初始化了的和兩個變量是一個包含了多個模板的初始化器具體實現就是下面將要分析的是初始化 0 簡介 Deployer安裝完成后輸入dep init開始初始化,生成默認的配置文件deploy.phpinit命令的實現在srcConsole...
摘要:最近在寫一個微信編輯器,然后已經在編輯器那一塊選定了,想想覺得雖然不錯,但是似乎已經很不更新了。補充一句,這個框架比誕生早了一個月,還是以為核心。自稱是一個模板驅動的庫,在上說是下一代的操作。下面是一個簡單的,。 最近在寫一個微信編輯器,然后已經在編輯器那一塊選定了CKEditor,想想覺得UEditor雖然不錯,但是似乎已經很不更新了。 想想覺得編輯器這種東西,對于一般人來說還算挺常...
閱讀 2000·2021-11-15 18:09
閱讀 896·2021-09-06 15:13
閱讀 2641·2021-08-23 09:43
閱讀 2023·2019-08-30 15:54
閱讀 2215·2019-08-30 13:56
閱讀 2482·2019-08-26 11:31
閱讀 3075·2019-08-26 10:56
閱讀 694·2019-08-26 10:28