處理遺留系統,幾乎是每個程序員都不可能繞過的一件麻煩事兒。因為時間壓力,技能不足以及功能復雜等諸多原因,常常使得遺留系統的代碼變得糟糕混亂,可讀性與維護性差,無法保證功能的可測試性,糾纏不清的代碼讓類、方法之間緊緊耦合在一起。如果遺留系統能夠正常工作,那么我們還可以置之不理,即使代碼接近腐爛的邊緣,我們還可以得過且過。倘若我們需要維護遺留系統,或者需要為它添加新的功能,又或者需要將新的系統與遺留系統進行集成,就必須正視遺留系統帶來的問題了。
處理遺留系統,首先需要分析和了解遺留系統,尤其這個遺留系統并非你開發時,更需如此。我們可以考慮雙管齊下的辦法。一是從業務邏輯方面去了解。相比新系統而言,遺留系統的唯一好處就是它往往是可以運行可以使用的。因此,最好的辦法是直接運行遺留系統,通過實際操作了解系統的各個功能點、業務流程。這樣的直觀感受可以最快地幫助你了解該系統:它能夠做什么?它能達成什么目標?它的范圍是什么?它存在什么問題?其二則需要從系統架構出發,了解遺留系統的邏輯結構和物理分布。可以閱讀架構文檔和源代碼,如果能夠咨詢遺留系統的設計者或開發人員,就更好。盡快地描繪出遺留系統的輪廓圖,可以幫助你從技術的宏觀角度剖析遺留系統的結構與組成。再結合你對該系統業務的理解,快速地掌握遺留系統。如果需要閱讀源代碼,最好能夠從主程序入口開始,找到一些主要的模塊,了解其大體的設計方式與編碼習慣。由于之前對系統架構已有了解,閱讀代碼時,不應在一開始就去理解代碼實現的細節,而應結合架構文檔,比對代碼實現是否與文檔的描述一致,并充分利用自己的技術與經驗,找到閱讀代碼的終南捷徑。例如,如果我們知道該系統采用了MVC架構,就可以很容易地根據Url找到對應的Controller對象,并在該對象中尋找業務功能實現的脈絡。又例如我們知道系統采用了SSH框架,而我們又非常熟悉SSH框架,就可以基本忽略系統基礎設施的部分,直接了解系統的業務實現。如果是Swing系統,而且在界面中混雜了大量的業務邏輯和界面邏輯,則需要找到系統實現的特點,譬如系統的業務都是通過菜單項進行操作,就可以在界面中找到相關的菜單對象,然后根據這些對象的Action操作,一步一步跟蹤。甚至可以利用調試的方法,設置斷點,來摸清楚系統的運行機制與執行順序。
分析遺留系統需要有的放矢,根據目標快速鎖定范圍。例如,如果我們是因為系統性能出現問題,而要去分析遺留系統,就無需過于關心業務邏輯,而應從性能分析入手。考慮數據庫的訪問,IO操作,緩存機制,資源的使用等諸多方面。這就需要借助一些經驗和技術了,當然也可以考慮使用一些工具,用以診斷性能瓶頸。我們曾經在一個項目中,發現遺留系統的性能問題非常嚴重。根據分析,我們發現系統對字符串的處理存在問題,大量使用了String類型的對象完成字符的拼接。而在進行數據庫查詢時,很多代碼是直接性地一次將相關表的數據“拉”到內存的集合對象中,然后利用過濾器在集合對象中進行篩選。而在對數據進行更改時,又沒有很好地利用業務特性,完成一次提交,導致產生多次數據庫訪問。在系統的某些公共模塊中,重復多次加載了Spring的配置文件。還有某些占用了較大資源的對象,對于系統用戶而言應該是同一個對象,但卻沒有設計為單例。因為我們抱著改善遺留系統性能的目的,所以在分析遺留系統時,就應該事先確定可能導致性能損耗的地方,而不是全方位地去了解整個系統。
在維護遺留系統時,需要根據不同的場景做出不同的決策。簡言之,我們需要排定優先級。如果時間緊迫,則解決問題是第一要務。盡快通過出錯情況和記錄日志辨別出錯原因,定位出錯代碼,并解決之,而不是去考慮設計的優雅,代碼的重用。我在一次解決報表顯示的問題時,采用的就是這種做法。雖然與錯誤相關的代碼相當丑陋,但由于時間緊迫,解決Bug才是最要緊的,所以我基本上沒有去修改或改善既有代碼的結構,僅僅解決了問題就結束了對遺留系統的處理。這個問題的處理過程在我的另一篇博客《 精益求精,抑或得過且過 》中詳細論述。是的,我在此時選擇了得過且過的做法,主要原因就在于優先級列表中,問題的解決才是最高優先級。在這次處理過程中,我部分地利用了copy & paste的做法,并通過引入新方法的方式來解決bug,而不是直接修改出現問題的方法。這是因為該方法的引用點存在多處,由于遺留系統并沒有單元測試的保護,我不能草率地修改,否則可能導致一個bug的修復,結果引入更多無法預測的Bug。
這樣的處理方式其實是我不甘心的選擇。在時間允許的情況下,我會考慮對相關代碼進行一些小的重構,例如提取方法或提取類等。雖然這些重構不能改變遺留系統的本質,但至少可以提高代碼的可讀性,并能在一定程度下去除代碼重復。
這一實例實際引出了單元測試的必要性。如果我們的開發都能夠在單元測試的呵護下進行,即使當它隨著時間的推移,慢慢變成了丑陋的遺留代碼,因為有單元測試的保護,在對它進行手術時,成功的幾率卻會變得更大。因此,認為編寫單元測試是浪費時間的觀點,事實上是一種短視的做法。缺乏單元測試,就是一種技術債(
technical debt
)。現在欠下的,將來總是要還的。我們不能忽略軟件的維護成本。事實上,在軟件成本中,維護成本所占的比例遠遠大于開發成本。Kent Beck在《
實現模式(Implementation Patterns)
》一書中認為:“維護的代價很大,這是因為理解現有代碼需要花費時間,而且容易出錯。知道了需要修改什么以后,做出改動就變得輕而易舉了。掌握現在的代碼做了哪些事情是最需要花費人力物力的部分,改動之后,還要進行測試和部署。”
當然,在處理遺留系統時,仍然可以進行單元測試。但它需要的技能更高,付出的精力更多。若要完成對遺留系統的單元測試,首要必須解決的就是依賴。解除依賴是面向對象系統開發中似乎亙古不變的話題。無論你翻閱哪一本面向對象著作,都會提到這個問題。依賴的起源其實在于對象的協作。根據 單一職責原則 ,一個類顯然不能承擔太多的職責。如果一個系統的所有功能都是一個對象完成的,那么這個對象就成了邪惡的“上帝”對象。它的粒度太大,因此難以重用,不夠靈活,細節糾纏,無法維護。這樣的設計是我們必須避免的。既然一個類不能承擔太多職責,系統要完成客戶要求的所有功能,就必須讓許多對象互相協作,就像人類社會的人際交往一般,于是,就產生了對象之間的依賴。凡事有利必有弊,這種細粒度的對象協作,雖然保證了對象的重用性、靈活性,卻也因為協作帶來的依賴,導致對象之間存在一定的耦合度,變得難以替換。由于一個對象依賴另一個對象,就使得我們在單元測試時,無法獨立地測試我們的目標對象。
依賴雖然不可避免,但如果能夠保證對象之間的良好協作,就能夠最大程度保證系統的松耦合。對象協作一般可以分為類協作、接口協作與回調協作(回調協作通常可以看做是接口協作的一種特殊方式,關于這三種協作的特點與方式,我會在以后的文章中談到)。依賴最弱的是接口協作,這也是我們努力達到的目標,它符合“面向接口設計”的基本原則。同時,也能夠保證對象的封裝性,通過將實現細節隱藏起來,從而降低對象之間的依賴程度。
知易行難,要對付遺留系統中的依賴問題,通常需要付出百倍的努力。Michael Feathers在其大作《
修改代碼的藝術(Working Effectively with Legacy Code)
》中,給出了很多好的解除遺留代碼依賴的實踐。例如他提出的接縫類型,隱藏依賴,以及影響結構圖。解除依賴需要合理地利用封裝與抽象,包括對方法參數的封裝與抽象。對于遺留系統而言,Feathers提出的影響結構圖尤其有用。當你需要修改某個方法時,影響結構圖可以幫助你分析該方法的依賴傳播途徑,避免因為修改對其他代碼造成影響。具體做法可以參考該書。當然,現在有很多IDE工具事實上還支持生成依賴圖,它可以在一定程度上幫助你尋找依賴的方向與結構。不過,手工方式的分析尤其是通過手繪影響結構圖仍然不可缺少,它更能幫助你深入地理解遺留系統。為遺留系統繪制包圖(Package Diagram)同樣非常重要,它既能幫助你理解遺留系統的結構,又能為我們找到系統中可能存在的雙向依賴和循環依賴,找到分解不合理的包,這就為我們的系統重構奠定了良好的基礎。
如果遺留系統非常復雜,以至于無法重構,同時我們又需要在遺留系統的基礎上增加新的特性和功能,好的做法就是做好新、舊功能之間的隔離。暫時可以不去考慮遺留系統的原有結構和代碼(大多數情況,這些遺留系統其實是可以正常工作的),而只需要為新增的功能做好單元測試,并以好的設計原則與編碼規范來要求新功能的實現。同時,我們還可以編寫一些自動化的回歸測試用例(例如使用Cucumber結合Watir為Web系統編寫回歸測試),保證新增的功能不會影響到原有系統。如果新增功能的實現需要調用原有系統中的某些類或方法,而這些類和方法卻又難以分解,則可以考慮參照原有實現編寫新的類或方法,新的類一定要做到合理的封裝與抽象,保證它的高內聚與松耦合;也可以在新的類中不去真正實現,而是通過運用 Adapter模式 來重用舊有的類。
只要不是更改開發平臺,通常情況下,我們不會考慮重寫遺留系統。如果需要重構遺留系統,就必須采取“分而治之,小步前進”的策略。可以首先選擇實現較為容易,或者獨立性較好的模塊進行重構。將遺留系統逐步提取為一些可重用的模塊與類,就可以形成“星星之火,可以燎原”的態勢。其中,對于原有類或模塊的調用方,由于在重構時可能會更改接口,因此可以考慮引入 Facade模式 或Adapter模式,或者對接口進行另一層的包裝,或者對接口進行適配,使系統慢慢被替換,最后演化為一個結構合理的良好系統。在重構時,甚至可以考慮將一些獨立性很好的功能,提取為單獨進程下的應用或服務,通過Web Service、Socket或Restful的方式進行調用,既保證了重用,又能夠使遺留系統變得更簡單,成功瘦身。
我們還需謹記的一點是,雖然模塊乃至服務的重構對系統的改造更加重要,但我們卻不能忽視代碼的質量。小到方法的提取,以及變量的命名都非常重要。也許它不會給系統帶來根本的改變,但它卻能夠改善代碼的可閱讀性,進而提高系統的可維護性。所謂”聚沙成塔“,無論是系統還是模塊,其實都是這些類和方法以及變量組成的。
對遺留系統的處理不可能“一蹴而就”,必須遵循循序漸進的做法,逐步改善。處理遺留系統影響深遠,成本也非常高,所以我們也不能因為腦袋一發熱,就開始氣勢洶涌地“提槍上陣”,錯將風車當做了魔鬼,結果撞得頭破血流。必須對整個遺留系統進行審慎的分析,并結合具體情況考慮這項工程的復雜度、成本與預算,了解團隊的重構與設計能力。處理遺留系統的前路漫漫,我們固然需要上下而求索的決心與勇氣,卻也不能在茫然失措中迷失了前進的方向。遺留系統的處理,必須慎之,慎之,再慎之!
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

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