大神方案|如何重寫一個萬行程式碼的類檔案

北京劉先生發表於2022-05-20

標題中牛皮吹的有點大,總結成一句話是:用一種極簡的方案,處理了一個相對複雜的問題。

前言:

什麼是設計模式?同事調侃道:設計模式就是把一個類,拆成一百個類。

調侃歸調侃,能夠將一個類拆成一百個類,而且類的功能劃分合理,類之間彼此獨立,又能相互配合,需要有足夠的技術沉澱。

經典的設計模式,為我們指明瞭解決問題的思路和方向,結合新的框架及語言特性,可以得到更好的解決方案。

 

現狀:

一個類有近萬行程式碼(9160行),一個方法有800多行,一個實體類包含492個屬性,一個賦值方法中包含234個if...else...判斷,而且這種近萬行的類檔案不止一個,相信大多數人面對這種程式碼時,要麼選擇視而不見,要麼會十分頭疼,而我很幸運的接手了這樣一個工作。

 

背景:

據說這是一個核心的業務模組,起初有2000多行程式碼,每次需求迭代,就加進去一部分程式碼,3年時間,增至目前近一萬行的程式碼。

可以想象,這樣的程式碼是沒有任何架構可言的,完全是意識流式的寫法,想到哪裡就寫到哪裡,有新需求就加新程式碼,複製貼上,修修改改,常年累月,形成了這樣一個龐然大物。

 

基本原則:

程式碼不僅僅要滿足執行的要求,好的程式碼更要便於閱讀,邏輯清晰,易擴充套件,易維護,符合高內聚,低耦合的需求。以此為指導思想,開展重寫工作。

如何制定一個統一的處理規範,將一個萬行大類拆分為多個小類,並要這些小類相互配合,而又彼此隔離,是這次重寫工作的重點。

 

(為節省閱讀時間,直接說方案)

方案說明: 

整體上,解決方案分為兩部分:抽象層和實現層。

抽象層:定義介面,並負責介面實現類的初始化,呼叫執行等功能,

實現層:實現介面,建立具體的資料查詢類,計算類。

 

資料傳遞:貫穿整個計算過程的資料,通過引數在計算類中進行傳遞;計算的中間資料,以注入的方式([Autowired]),在計算類之間傳遞。

 

整個方案可以說做到了儘可能的簡潔,抽象層不關心具體實現,實現類之間完全解耦,每個類只需要關心自己的實現邏輯,類的呼叫順序及依賴關係放到抽象層。

 

業務場景:

養殖行業的成本費用分攤計算 

業務需求:

將當期發生的成本費用,按照一定比例,分攤到具體的成本物件以及其對應的成本專案上。 

核心邏輯: 

整個計算邏輯包括30多個計算步驟,需要從20多個資料來源(分佈在多個資料庫的的20多個關聯查詢)中查詢原始資料,經過層層計算,得到最終結果。

這30多個計算步驟,有的以原始資料作為輸入資料,有的以前置計算結果作為輸入資料。

思考過程:

首先想到的是用設計模式:職責鏈模式

每個計算步驟作為職責鏈中的一個節點,節點依次執行,完成整個計算過程。

進一步研究發現,使用職責鏈模式有兩個問題:

問題一:職責鏈模式的每個節點,依賴下一個節點,每個節點需要明確知道其下一節點,增加了節點間程式碼耦合。

問題二:職責鏈模式的節點實現自同一個介面,對於需要以其他節點的計算結果作為輸入資料的節點並不友好。(有同學可能會想到用一個大類,將所有節點需要的資料都放到這個類中,節點從中篩選自己需要的資料,雖然能解決問題,但並不優雅)

如何解決:

第一個問題,使用職責鏈模式,是為了隔離每個步驟的關注點,降低程式碼耦合,至於執行順序是由職責鏈自己控制還是由外部控制,並不重要,為此,將控制職責鏈執行順序的職責放到職責鏈外部,是更合理的方式,最簡單的方法是:使用List承載節點實現類,順序執行即可。

第二個問題,資料傳遞的問題,節點要想訪問某個資料,傳參是最簡單的方式,另一種方式是使用“注入”。也就是下面程式碼中的特性[Autowired],用過spring的同學應該不會陌生,這裡借鑑spring的思想,自定義了一個資料注入的特性。大致思路是:前置計算器的計算結果存入一個全域性資料容器中,需要使用該資料的計算器類,定義屬性,標記[Autowired]特性,在計算器初始化時,會根據屬性的型別,在全域性資料容器中查詢資料,自動對屬性賦值,計算器不需要關心資料來源,只需要使用資料,完成計算邏輯即可。

    /// <summary>
    /// 飼料費用計算
    /// </summary>
    internal class MaterialCostCaculator : ICostCaculator
    {
        [Autowired]
        public MaterialRequisitionDataContext DataContext { get; set; }

        public List<KeyValuePair<string, decimal>> Caculate(FeedPigsContext feedDaysContext, CaculateItem item)
        {
            var result = new List<KeyValuePair<string, decimal>>();
        }
    }

 

詳細說明:

CostCaculateScheduler

CostCaculateScheduler是整個計算功能的唯一入口,該類只有一個方法 Caculate(),作用是:接收引數,初始化並呼叫 “資料查詢類”和“計算器實現類”(具體功能下放到了CaculatorCollection和CostDataContextScheduler),完成整個計算工作。

後期計劃加入註冊“資料查詢類”和“計算器實現類”的方法,以便呼叫方自定義使用哪些計算器。

CaculatorCollection

CaculatorCollection是計算器的集合類,可以理解為問題一的解決方案中提到的List,提取這個類是為了使結構更清晰。

CostDataContextScheduler

CostDataContextScheduler是一個資料查詢類的排程器,可以在計算器執行前,完成對資料的查詢,同時也充當問題二的解決方案中“全域性資料容器”的角色。

 

ICostCaculator:是計算器介面。

ICostDataHandler:是通用資料的查詢介面,其查詢結果會存入“全域性資料容器”。

ICostDataContext:是一個標誌介面,是為了保持資料查詢介面的外觀的一致性。

對於每個計算環節來說,只需要定義具體的計算實現類,並完成註冊即可。

最左側的FeedDaysXXX相關的類,是用於查詢成本物件,及計算分攤比例的,基本邏輯一致。

類圖如下:

  

心得體會:

將一件簡單的事情做複雜,很容易,而將一個複雜的業務做簡單,卻很考驗能力。

經典的設計模式,為我們指明瞭解決問題的思路和方向,結合新的框架及語言特性,可以得到更好的解決方案。

 

費了好大力氣,說了很多,但感覺好像還沒說明白...

 

相關文章