記一次百萬行WPF專案程式碼的重構記錄

殘生發表於2022-05-13

       此前帶領小組成員主導過一個百萬行程式碼上位機專案的重構工作,分析專案中存在的問題做了些針對性的優化,整個重構工作持續了一年半之久。

主要針對以下問題:

1.產品型號太多導致程式碼工程的分支太多,維護時會產生非常多的重複性的工作。

        這是一個歷史遺留問題,公司成立之初的開發人員在開發時沒有考慮到後期其他機型的合併而留有餘地,後面增加其他機型而導致的程式碼差異,是直接通過建立工程的分支來進行維護。兩個不同機型之間可能大部分的業務邏輯都相同而只有少部分的介面和業務存在差異性,當修改一個共有的bug時,需要在所有分支上面都修改一遍。

  同時有可能因為分支程式碼的改動,而無法直接進行程式碼合併,進而導致重複工作量的增加。如果分支不多的時候倒還好,而當機型越來越多,分支越來越多,這個重複性的工作會消耗相當大的時間和精力。所以合併所有機型的程式碼,實現上位機程式碼在不同機型上的通用性勢在必行。

  一個良好的軟體架構應當是 面向介面程式設計,而不是面向實現程式設計。對於不同機型中的業務和流程在主體上是一樣的,只是其中某些細節存在差異性的分支,所以我們需要將業務程式碼提煉出主體流程和對應的介面,通過這些業務介面來實現機型差異所帶來的的業務流程上的差異和分支,而非工程上的分支。

  在軟體執行時,可以通過相應的配置來決定業務介面的具體實現是哪一個,同時由於不同機型的介面實現是分離的,修改一個機型不會影響到另一個機型的程式碼。當然,在修改主流程程式碼時候就要小心了,需要考慮這一個改動對所有機型的影響。除非是非常有把握的情況下直接改動,否則還是先抽象出介面保證原主流程程式碼不變,只修改你需要修改的實現。

 

2.配置零散,沒有統一的管理機制,不利於打包。

  軟體中的配置資料儲存的地方太零散,有儲存在資料庫的,有儲存在txt檔案的,有儲存在登錄檔的,有儲存在app.config的。經過不同開發人員的不斷迭代,積累了很多無用的配置資料,並且沒有人敢刪除。而售後有時候為了查詢修改某個配置需要在各個地方查詢,非常繁瑣。

  同時也因為公司機型太多導致很多配置資料在不同機型上存在差異性,這些差異性有可能是來自硬體差異,也有可能是來自軟體功能上的差異。而每釋出一個版本,就有可能需要同時打包多個機型的軟體包,每一個軟體包至少有1個G 的大小。而隨著機型的越來越多,一個版本不同軟體包也會越來越多,這對於打包人員來說是個不小的負擔,同時也要求打包人員需要明確的知道每一個機型配置上的差異性來保證軟體包的正確性。

  為了解決這個問題,我們花了數個月的時間,對所有機型儲存在不同位置的配置做了一個整理。我們整理出了所有機型通用的配置,統一儲存在資料庫表中,同時為每一個機型建立資料庫表用來儲存存在差異的配置資料。為了方便打包人員和售後管理檢視這些資料庫,我們開發了一個配置管理工具用來專門檢視和修改配置。

  另外,我們為了解決多個軟體包的問題,把所有硬體相關的配置整合進一個檔案中,並開發出一個版本升級軟體。在這個版本升級軟體中,售後可以選擇機型對應的硬體,升級程式可以通過所選硬體對應的配置寫入到資料庫中來實現同一個軟體包不同機型的升級工作。

  同時我們開發了通用的http介面給上層C#程式和下層C++程式使用,用於讀寫資料庫的配置資料。

  由於我們的裝置有多個PC,並且在醫院內部無法連線外網,此前軟體升級時每個PC都需要售後人員拷貝軟體包並手動呼叫升級指令碼來完成升級。而現在,我們的升級程式可以通過遠端呼叫的方式來同時完成多個PC的升級工作,做到了一鍵升級功能。

  基於第一點的軟體程式碼合併,在本次配置優化之後,打包人員每次打包僅需要一個軟體包,即可實現不同機型的一鍵升級,省時又省力。

 

3.UI和業務邏輯混雜

 

  專案以WPF為主,整體使用MVVM框架。專案中沒有使用開源的控制元件庫,其中含有非常多的高度自定義控制元件的開發,這些控制元件的UI表現程式碼和業務邏輯程式碼夾雜在一起,耦合性太高非常不利於理解業務程式碼和後期維護。

 

  WPF的MVVM框架本身最大的優勢就是為了分離業務和UI,降低耦合性,提高可重用性,所以這個專案並沒有發揮出MVVM 框架的優勢。針對這個問題,我們分離UI和業務,提煉出一個單純的UI庫,這個UI庫不包含任何的業務程式碼,除了.Net Framework的依賴庫之外,不依賴專案中的任何其他庫。

  每一個UI控制元件暴露自定義的依賴屬性,在業務邏輯呼叫時,通過繫結這些依賴屬性來改變UI的表現邏輯。這樣做的好處是完全分離了UI表現程式碼和業務邏輯程式碼,並且這個UI庫具有高重用性,可以交給其他專案使用,甚至可以直接開源出來。

  而且在後期維護時,UI程式碼的改動與業務程式碼的改動互不干擾,也有利於Bug的排查。

 

4.由於前期開發人員的層次不一,並且沒有CodeReview機制來保證程式碼質量,導致程式碼存在很多低階錯誤。

  專案中存在大量重複程式碼,明明可以提煉出一個簡單方便的方法,偏偏要在各個地方不斷的Copy相同的程式碼。

  明明只要增加一個引數就可以合併成一個方法,偏偏要寫上幾十個方法,諸如xxxx1,xxxx2....xxxx30,三十幾個方法執行同樣的功能,只有一個引數上的差異。我們的業績考核又不是看程式碼量,這種程式碼看了讓人啼笑皆非。

  沒有統一的命名規則,有些屬性首字母小寫,C#和C++風格混用,有些命名直接使用縮寫,類似“ggr”這樣縮寫,除了作者誰能看懂這是什麼意思?

  一個類,一個方法程式碼過多,幾千行一個類的檔案不在少數,一個類承擔的功能也過多也過餘複雜。類和方法都應該遵循職責單一原則,不過在實際開發過程中單一原則不太好把控,但應該合理的控制程式碼行數。

  我們在專案重構工作之前,制定了統一的命名風格,並嚴格限制了每個類每個方法的程式碼行數。一個檔案,一個類,最多不超過一千行,一個方法最多不超過六十行。

  六十行程式碼一個螢幕很難放下,這個要求其實比較低了,最合理的應該是三四十行,一個螢幕正好可以看完一個方法的所有程式碼。

  在專案重構的過程中,我們規定所有人提交的程式碼都必須提交給高階工程師CodeReview,高階工程師之間互相CodeReview。每個人的知識面都是有限的,但可以通過合作來達到無限的廣度和深度,我們應該儘量去避免犯一些低階的、顯而易見的錯誤。

 

5.軟體需要顯示處理大量的圖片,導致程式記憶體和CPU佔用過大

  由於我們的軟體需要處理顯示大量的Dicom檔案,並且對這些展示的圖片都有較為複雜的操作,諸如旋轉,放大、畫素提取、銳化等,這些功能都整合在一個ImageControl中。然而由於我們的ImageControl寫的不合理,讀取一張500K的Dicom並顯示會至少佔用2M的記憶體。如果同時讀取上百個Dicom檔案,程式記憶體可以輕輕鬆鬆突破1個G,同時由於我們系統中不止一個程式需要處理這些圖片,所以對系統的記憶體要求非常高。

  在前期,我們只能不斷的累加硬體,把記憶體擴充套件到了32G,甚至是64G,然而這只是飲鴆止渴的錯誤方式,應該從根本上解決ImageControl控制元件佔用記憶體過大的問題,同時應該優化程式顯示排列圖片的邏輯。

  在考慮到專案人員的分配情況和專案計劃,我們決定優化ImageControl這個思路暫緩,因為這個工作量會更大。我們決定先著手優化程式展示圖片的邏輯。程式只載入使用者能夠看到的Dicom,當使用者下拉進度條時,再陸續載入可見的圖片,並且將其餘不可見的ImageControl銷燬,優化之後程式佔用的記憶體銳減60%-70%,可以接受。

 

 

 

 

 

 

相關文章