在所有關于軟件維護的故事中,功能的擴展是一個永恒的話題。正因為軟件系統需要功能的擴展,需要新功能的加入,才使我們的編程需要那么多的設計。可以說,正是因為新功能的擴展,使得原有的系統質量下降;正是因為軟件質量的下降,才使我們需要進行深入的分析與研究,制訂設計原則,總結設計模式;正是因為要解決軟件質量下降的問題,經過一番艱苦卓絕的摸索過程,我們才認識到系統重構才是解決該問題的最佳方案。
然而,事情總是這樣的,每個系統當我們進行初次的設計時,設計思路、程序結構總是比較完美的。可是當初次設計結束后,我們在日后的維護中,開始往系統里添加新功能時,系統開始不完美了,甚至開始出現問題了,新增的功能總是或多或少有些水土不服。怎么辦呢?要保證每次需求的變更時軟件質量不會下降,必須記住這樣一個原則:先重構再添加新功能。
添加新功能前先重構原有系統,其目的有兩個:
1. 軟件的設計總是與軟件的復雜程度有關的,原有的設計是在原有需求不復雜的條件下做出的,但隨著新功能的加入,軟件復雜度在發生著變化,因此必須要調整原有的設計以適應新的需求;
2. 為了提高軟件的可維護性與易變更性,添加新功能應遵循OCP原則。而要遵循OCP原則,我們應當在不添加新功能的前提下先進行重構,設計出可擴展點出來,然后再添加新功能。
是的,明白這兩點非常重要。軟件維護越來越困難,不是因為客戶提出了需求變更,而是因為我們沒有隨著軟件復雜度的增加改變我們的軟件結構。軟件需求起初比較簡單,是幾乎所有軟件的共性。但隨著軟件復雜度的增加,我們卻不敢有效地調整現有的軟件結構,以適應新的需求,這就真正是我們的問題了。不敢調整,是害怕原有功能會出錯,但不調整,則意味著我們軟件設計的問題會越來越大,進而越來越難于維護。改與不改,我們面臨著兩難的抉擇。
解決這個兩難的難題其實不難,實際上就是一層窗戶紙一桶就破了。試想,我們不敢修改原有代碼的真正原因是因為害怕改出問題影響原有功能的正常運行。那么,我們找一個方法使我們在修改原有代碼時不會出現問題,換句話說是出現問題以后會及時發現,則問題就可以解決,這個方法就是重構與測試。
客戶判斷一個功能是否正常運行的標準,就是當輸入一個值后,能得到客戶期望的結果,不管系統內部是怎樣運行的。因此,建立這樣一個測試用例,讓軟件系統在重構前后都能通過這些測試用例,就可以保證重構的正確性(關于如何建立,我們還會在后面仔細討論)。重構以后,外部功能是一致的,但內部程序結構卻變得更加易于添加新的功能,使新的功能與原有系統可以有機地融為一體,這才是我們的目的。說起來比較抽象,我們來舉一個示例吧:
在許多系統中,只要有報表出現就有需求要實現Excel數據導出功能。在一個系統中,客戶起初提出的需求是實現“全部導出”、“按選擇導出”、“導出本頁”。為此,我們設計了一個單選框,并在后臺程序中編寫了一個if語句,如果選擇的是“全部導出”,則查詢所有記錄并導出;如果選擇的是“按選擇導出”,則從前端獲得一個主鍵列表,即用戶已選擇的行,以此作為條件查詢導出;如果選擇的是“導出本頁”,則查詢本頁數據并導出。
這樣的設計沒有問題,也是大多數人首先想到的設計。但是,多個不同的選擇放在一個類中必將為功能的擴展帶來麻煩。隨后,客戶提出了新的需求,按頁導出,即根據客戶的要求,從第幾頁到第幾頁進行導出。按照前面的設計,我們必然是在原有基礎上再增加一個if語句,實現按頁導出。
但這樣的設計違反了OCP原則,也是大多數系統代碼質量下降的重要原因之一。在一些文章中,if語句被稱為“罪惡之源”,因為大量使用if語句將會大大降低系統的可讀性、可維護性與易變更性,使系統難于維護。因為不斷添加的if語句很快會使代碼由數百行膨脹到幾千行,還會大量摻雜各種重復代碼與糟糕設計。其根本原因就在于,它讓一個類承載了過多的職責,降低了功能內聚而提高了功能耦合。它不僅加大了我們修改代碼的難度,也將加大我們測試代碼的成本,因為任何一項修改都必須要對所有功能進行測試。
因此,我們需要調整我們的代碼結構,改變我們的設計(到這里也許你開始理解我所說的改變代碼結構以適應新的需求的含義了吧)。我們說擴展新功能的設計應當符合OCP原則。怎樣的設計才是符合OCP原則的呢?首先可以想到的是,讓“按頁導出”這個功能的代碼放到另一個類中,而不寫在原有類中。比如,我們可以創建一個新類ExportPageRange,通過接口Exporter接入到原類ExportBus,讓ExportBus調用其相應的方法:
但是,這樣的設計我們依然需要修改原類ExportBus,在if語句中調用接口:
加粗部分是我們不得不在原類中添加的代碼。如果不使用這個if語句而讓Exporter接口的實現類與判斷條件建立一種聯系,則問題可以得到解決。要實現這種聯系有很多方法,其中一個方法就是建立配置文件,讓配置文件中的名稱與實現類關聯起來就可以了,為此我們需要這樣設計:
然后進行這樣的配置:
這樣,配置文件中的entrykey就與導出程序的實現類建立了聯系,因此在ExportBus中原來的那個if語句就演變成了這樣:
加粗的部分實質性替代了原來那個if語句。這樣的設計,讓各個不同類型的導出程序得到有效解耦,然后通過接口與配置文件實現動態地裝配。這就是一種典型的可擴展點設計,當我們還有新的導出類型的功能需要擴展的時候,不需要修改原有的任何代碼,而只需添加一個Exporter接口新的實現類,再進行相應的配置,功能就可以實現。這樣的設計是可以滿足OCP原則的,而在系統中實現這種可擴展性設計的功能點,我們就稱之為“可擴展點”。
以上的設計是我們最終應當實現的設計,是結果。但要達到這樣的設計,即分析整個設計的過程,我們真的沒有修改原程序嗎?不,我們修改了。那么這怎么叫符合OCP原則呢?問題十分犀利哈。轉了那么大一圈,現在才是我要真正提出我的觀點的時候了。我認為,要改善遺留系統的可維護性,要遵守OCP原則,并不是意味著實現新需求時不能修改原有代碼。要遵從“兩頂帽子”的設計原則,先重構原有的代碼,使其具有可擴展功能,然后再添加新程序,使其滿足OCP原則,這才是可擴展設計的關鍵之所在。
具體來說,就是第一步修改代碼,第二步添加功能。第一步,修改原有代碼,在保證原有功能不變的前提下,設計出可擴展點,使其在以后添加新功能時不必修改原有代碼。在本例中就是將原有的三種導出方式從原有代碼中抽取出來,形成Exporter接口與ExportAll、ExportOnePage、ExportChosen三個實現類,以及它們的配置文件。這樣,Exporter接口就是數據導出方式的可擴展點。這時,由于沒有添加任何新功能,我們可以編寫測試代碼進行測試,或者手工測試。
第二步,就是添加新功能。由于可擴展點已經做出來了,剩下的工作其實就很簡單了:編寫實現類ExportPageRange,然后配置到系統中,整個設計符合OCP原則。功能擴展就應當這樣做,才能使我們的軟件在維護中始終能保持高質量的代碼。
(續)
相關文檔
遺留系統:IT攻城獅永遠的痛
需求變更是罪惡之源嗎?
系統重構是個什么玩意兒
我們應當改變我們的設計習慣
小步快跑是這樣玩的(上)
小步快跑是這樣玩的(下)
代碼復用應該這樣做(1)
代碼復用應該這樣做(2)
代碼復用應該這樣做(3)
做好代碼復用不簡單
軟件可以這樣功能擴展
過程擴展與放置鉤子
特別說明:希望網友們在轉載本文時,應當注明作者或出處,以示對作者的尊重,謝謝!
然而,事情總是這樣的,每個系統當我們進行初次的設計時,設計思路、程序結構總是比較完美的。可是當初次設計結束后,我們在日后的維護中,開始往系統里添加新功能時,系統開始不完美了,甚至開始出現問題了,新增的功能總是或多或少有些水土不服。怎么辦呢?要保證每次需求的變更時軟件質量不會下降,必須記住這樣一個原則:先重構再添加新功能。
添加新功能前先重構原有系統,其目的有兩個:
1. 軟件的設計總是與軟件的復雜程度有關的,原有的設計是在原有需求不復雜的條件下做出的,但隨著新功能的加入,軟件復雜度在發生著變化,因此必須要調整原有的設計以適應新的需求;
2. 為了提高軟件的可維護性與易變更性,添加新功能應遵循OCP原則。而要遵循OCP原則,我們應當在不添加新功能的前提下先進行重構,設計出可擴展點出來,然后再添加新功能。
是的,明白這兩點非常重要。軟件維護越來越困難,不是因為客戶提出了需求變更,而是因為我們沒有隨著軟件復雜度的增加改變我們的軟件結構。軟件需求起初比較簡單,是幾乎所有軟件的共性。但隨著軟件復雜度的增加,我們卻不敢有效地調整現有的軟件結構,以適應新的需求,這就真正是我們的問題了。不敢調整,是害怕原有功能會出錯,但不調整,則意味著我們軟件設計的問題會越來越大,進而越來越難于維護。改與不改,我們面臨著兩難的抉擇。
解決這個兩難的難題其實不難,實際上就是一層窗戶紙一桶就破了。試想,我們不敢修改原有代碼的真正原因是因為害怕改出問題影響原有功能的正常運行。那么,我們找一個方法使我們在修改原有代碼時不會出現問題,換句話說是出現問題以后會及時發現,則問題就可以解決,這個方法就是重構與測試。
客戶判斷一個功能是否正常運行的標準,就是當輸入一個值后,能得到客戶期望的結果,不管系統內部是怎樣運行的。因此,建立這樣一個測試用例,讓軟件系統在重構前后都能通過這些測試用例,就可以保證重構的正確性(關于如何建立,我們還會在后面仔細討論)。重構以后,外部功能是一致的,但內部程序結構卻變得更加易于添加新的功能,使新的功能與原有系統可以有機地融為一體,這才是我們的目的。說起來比較抽象,我們來舉一個示例吧:
在許多系統中,只要有報表出現就有需求要實現Excel數據導出功能。在一個系統中,客戶起初提出的需求是實現“全部導出”、“按選擇導出”、“導出本頁”。為此,我們設計了一個單選框,并在后臺程序中編寫了一個if語句,如果選擇的是“全部導出”,則查詢所有記錄并導出;如果選擇的是“按選擇導出”,則從前端獲得一個主鍵列表,即用戶已選擇的行,以此作為條件查詢導出;如果選擇的是“導出本頁”,則查詢本頁數據并導出。
String exportTypeName = (String)params.get("exportType"); if("exportAll".equals(exportTypeName)) { //全部導出的代碼 } else if("exportChoosen".equals(exportTypeName)) { //按選擇導出的代碼 } else if("exportOnePage".equals(exportTypeName)) { //導出本頁的代碼 }
這樣的設計沒有問題,也是大多數人首先想到的設計。但是,多個不同的選擇放在一個類中必將為功能的擴展帶來麻煩。隨后,客戶提出了新的需求,按頁導出,即根據客戶的要求,從第幾頁到第幾頁進行導出。按照前面的設計,我們必然是在原有基礎上再增加一個if語句,實現按頁導出。
String exportTypeName = (String)params.get("exportType"); if("exportAll".equals(exportTypeName)) { //全部導出的代碼 } else if("exportChoosen".equals(exportTypeName)) { //按選擇導出的代碼 } else if("exportOnePage".equals(exportTypeName)) { //導出本頁的代碼 } else if("exportPageRange".equals(exportTypeName)) { //按頁導出的代碼 }
但這樣的設計違反了OCP原則,也是大多數系統代碼質量下降的重要原因之一。在一些文章中,if語句被稱為“罪惡之源”,因為大量使用if語句將會大大降低系統的可讀性、可維護性與易變更性,使系統難于維護。因為不斷添加的if語句很快會使代碼由數百行膨脹到幾千行,還會大量摻雜各種重復代碼與糟糕設計。其根本原因就在于,它讓一個類承載了過多的職責,降低了功能內聚而提高了功能耦合。它不僅加大了我們修改代碼的難度,也將加大我們測試代碼的成本,因為任何一項修改都必須要對所有功能進行測試。
因此,我們需要調整我們的代碼結構,改變我們的設計(到這里也許你開始理解我所說的改變代碼結構以適應新的需求的含義了吧)。我們說擴展新功能的設計應當符合OCP原則。怎樣的設計才是符合OCP原則的呢?首先可以想到的是,讓“按頁導出”這個功能的代碼放到另一個類中,而不寫在原有類中。比如,我們可以創建一個新類ExportPageRange,通過接口Exporter接入到原類ExportBus,讓ExportBus調用其相應的方法:
但是,這樣的設計我們依然需要修改原類ExportBus,在if語句中調用接口:
String exportTypeName = (String)params.get("exportType"); If ("exportAll".equals(exportTypeName)) { //全部導出的代碼 } else if ("exportChoosen".equals(exportTypeName)) { //按選擇導出的代碼 } else if ("exportOnePage".equals(exportTypeName)) { //導出本頁的代碼 } else if ("exportPageRange".equals(exportTypeName)) { //按頁導出的代碼 Exporter exporter = new ExportPageRange(); exporter.doExport(resultset); return exporter.getFileInfo(); }
加粗部分是我們不得不在原類中添加的代碼。如果不使用這個if語句而讓Exporter接口的實現類與判斷條件建立一種聯系,則問題可以得到解決。要實現這種聯系有很多方法,其中一個方法就是建立配置文件,讓配置文件中的名稱與實現類關聯起來就可以了,為此我們需要這樣設計:
然后進行這樣的配置:
<bean id="exportBus" class="com...reporter.bus.impl.ExportBusImpl"> <description>導出數據BUS</description> <property name="exportTypes"> <map> <entry key="exportAll"><!-- 全部導出 --> <bean class="com...reporter.export.ExportAll"/> </entry> <entry key="exportOnePage"><!-- 導出本頁 --> <bean class="com...reporter.export.ExportOnePage"/> </entry> <entry key="exportChosen"><!-- 按選擇導出 --> <bean class="com...reporter.export.ExportChosen"/> </entry> <entry key="exportPageRange"><!-- 按頁導出 --> <bean class="com...reporter.export.ExportPageRange"/> </entry> </map> </property> </bean>
這樣,配置文件中的entrykey就與導出程序的實現類建立了聯系,因此在ExportBus中原來的那個if語句就演變成了這樣:
String exportTypeName = (String)params.get("exportType"); Exporter exporter = exportTypes.get(exportTypeName); exporter.doExport(resultset); return exporter.getFileInfo();
加粗的部分實質性替代了原來那個if語句。這樣的設計,讓各個不同類型的導出程序得到有效解耦,然后通過接口與配置文件實現動態地裝配。這就是一種典型的可擴展點設計,當我們還有新的導出類型的功能需要擴展的時候,不需要修改原有的任何代碼,而只需添加一個Exporter接口新的實現類,再進行相應的配置,功能就可以實現。這樣的設計是可以滿足OCP原則的,而在系統中實現這種可擴展性設計的功能點,我們就稱之為“可擴展點”。
以上的設計是我們最終應當實現的設計,是結果。但要達到這樣的設計,即分析整個設計的過程,我們真的沒有修改原程序嗎?不,我們修改了。那么這怎么叫符合OCP原則呢?問題十分犀利哈。轉了那么大一圈,現在才是我要真正提出我的觀點的時候了。我認為,要改善遺留系統的可維護性,要遵守OCP原則,并不是意味著實現新需求時不能修改原有代碼。要遵從“兩頂帽子”的設計原則,先重構原有的代碼,使其具有可擴展功能,然后再添加新程序,使其滿足OCP原則,這才是可擴展設計的關鍵之所在。
具體來說,就是第一步修改代碼,第二步添加功能。第一步,修改原有代碼,在保證原有功能不變的前提下,設計出可擴展點,使其在以后添加新功能時不必修改原有代碼。在本例中就是將原有的三種導出方式從原有代碼中抽取出來,形成Exporter接口與ExportAll、ExportOnePage、ExportChosen三個實現類,以及它們的配置文件。這樣,Exporter接口就是數據導出方式的可擴展點。這時,由于沒有添加任何新功能,我們可以編寫測試代碼進行測試,或者手工測試。
第二步,就是添加新功能。由于可擴展點已經做出來了,剩下的工作其實就很簡單了:編寫實現類ExportPageRange,然后配置到系統中,整個設計符合OCP原則。功能擴展就應當這樣做,才能使我們的軟件在維護中始終能保持高質量的代碼。
(續)
相關文檔
遺留系統:IT攻城獅永遠的痛
需求變更是罪惡之源嗎?
系統重構是個什么玩意兒
我們應當改變我們的設計習慣
小步快跑是這樣玩的(上)
小步快跑是這樣玩的(下)
代碼復用應該這樣做(1)
代碼復用應該這樣做(2)
代碼復用應該這樣做(3)
做好代碼復用不簡單
軟件可以這樣功能擴展
過程擴展與放置鉤子
特別說明:希望網友們在轉載本文時,應當注明作者或出處,以示對作者的尊重,謝謝!
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
