把3000行程式碼重構成15行,這樣做!

資料和雲發表於2020-02-04

把三千行程式碼重構為 15 行


那年我剛畢業,進了現在這個公司。公司是搞資料中心環境監控的,裡面充斥著嵌入式、精密空調、匯流排、RFID 的概念,我一個都不懂。還好,公司之前用 Delphi 寫的老客戶端因為太慢,然後就搞了個 Webform 的替代,恰好我對 Asp.Net 還算了解,我對業務的不瞭解並不妨礙我稱成為這個公司的一個程式設計師。
小公司也有小公司的好,人少,進去很快負責程式碼開發。我當然也就搞這個資料中心智慧管理系統啦。
這個系統非常的龐大,尤其牛逼的是支援客戶端組態,然後動態生成網頁,資料還能通過 Socket 實時監控 (那時我還真就不懂網路程式設計)。這個對於當時的我來說,真真是高、大、上吶!!當時跟著瞭解整個系統大半個月才算能夠除錯,寫一些簡單的頁面。
在維護系統的過程中,時不時要擴充套件一些功能,也就接觸了下面這個類:

看到沒有,就是當年最最流行的三層架構的產物,對於剛出茅廬的毛頭小子來說,這是多麼專業的檔案頭註釋,還有反射也就算了,這建構函式還能靜態的,還能私有的?那時剛接觸這麼高大上的程式碼的我,瞬間給跪了!
但是,類寫多了,我就感覺越來越彆扭,就是下面這段程式碼:

每增加一個表,除了要改介面、要改 DAL、要改 BLL 之外,還得在這個工廠類新增一個方法,真真是累到手抽筋,即使有當時公司了的 G 工給我推薦的神器 —— 動軟程式碼生成器,這貼上複製的幾遍,也是讓我感覺到異常繁瑣,有時候打鍵盤稍微累了點,還把複製出來程式碼改錯了,你妹的,難道這就是程式設計師該乾的事情,不,絕對不是!我想起了一句至理名言: 當你覺得程式碼重複出現在程式中的時候,就應該重構了。是的,在這句話的指導下,我開始了折騰,決定挑戰這個高大上的程式碼,事實證明,思想的力量是無窮的。
那麼,怎麼修改呢,仔細觀察之後,發現其中 className 的生成跟返回的型別非常類似,只是一個是類名,一個是字串,這兩者之間應該能夠關聯起來。於是 google 了一下 (當時 GFW 還沒猖獗起來哈),隱隱約約就找到了 “ 反射” 這兩個字,深入瞭解之後,確定可以完成。
接下來,就是返回的型別了,返回的型別並不固定,但是似乎很有規律…… 這個似乎好像在哪裡見過,對了, 模板,C++ 課程上有講過的,於是再次 google,瞭解到了 C# 中使用了泛型代替了 C++ 中的模板。在學習完泛型和反射之後,並參考了網上的一些文章,我搗鼓出了下面的程式碼:


沒錯,就是它了,三層架構年代最流行的工廠類……

看著原來滾十幾螢幕的程式碼,變成了十多行的程式碼,真是爽到了骨子裡去了,太乾淨了!唯一讓我擔憂的是,我進公司的時候,幫忙整理公司申請軟體著作權都是需要程式碼量的,根據程式碼多少行來評估軟體的大小,萬一老闆知道了我非但沒有幫公司增加程式碼量,還減少了,會不會立即把我開掉?我沒敢給我們老闆展示我優秀的成果。
所幸,這段程式碼非但沒有出過任何問題,還避免了以前同事老是在新增一個類之後,把程式碼複製過來,但是沒有正確修改的問題,大大提高了效率。雖然,我沒敢大事宣佈我的勞動成果,但是這次成功的修改,則徹底讓我走上了程式碼重構的不歸路。
看到這裡,大家應該知道這個案例是否真實的了吧。我相信,從 08 年開始的碼農們,看到這種類似的程式碼絕對不比我少。那麼,我想告訴你們的是什麼呢?

  • 要在程式設計過程中多思考
  • 程式設計的思想很重要,請多看點經典的書
  • 從小處著眼,慢慢重構,尤其在應對一個大型的系統
  • 當重複出現的時候,你應該考慮重構了
  • 貼上複製的程式碼越少,你的系統越穩定


少用程式碼生成器


我們來分析一下,為什麼我之前的前輩會寫出上面的程式碼。我歸結起來有以下幾點:

  • 因為使用了動軟程式碼生成器,生成程式碼方便,就沒多想了。
  • 三層架構的概念倒是瞭解了,但是沒有去深入思考就拿來應用
  • 遇到重複的程式碼,沒有重構的概念,這是思想的問題 —— 思想比你的能力重要


至今為止,還是很多人使用程式碼生成器,那麼我們應該怎麼對待這個問題呢。我認為,程式碼生成器確實可以減少你不少工作,但是少用,那些重複性的工作,除了部分確實是沒有辦法的,其他大部分都是可以通過框架解決的,舉例來說,像三層架構,真正需要用到程式碼生成器的,也就是 Model 類而已,其他的完全可以在框架中完成。
因此 你要竭盡全力的思考怎麼在框架中來減少你的重複性工作,而不是依賴於程式碼生成器。
另外,如果你還是在用相關的程式碼生成工具,請 重新定義 “動軟程式碼生成器” 的程式碼模板,自己寫一個模板;或者 使用 CodeSmith 來完全制定自己的程式碼生成,因為動軟給的程式碼模板真心亂,比如下面這段程式碼:
for ( int n =  0; n < rowsCount; n++)
{
    model =  new DBAccess.Model.eventweek();
     if(dt.Rows[n][ "GroupNo"].ToString()!= "")
    {
        model.GroupNo= int.Parse(dt.Rows[n][ "GroupNo"].ToString());
    }
     if(dt.Rows[n][ "Week0"].ToString()!= "")
    {
        model.Week0= int.Parse(dt.Rows[n][ "Week0"].ToString());
    }
     if(dt.Rows[n][ "Week1"].ToString()!= "")
    {
        model.Week1= int.Parse(dt.Rows[n][ "Week1"].ToString());
    }
}

首先,你就不能用 var row=dt.Rows [n] 替代嗎?其次,直接用 int.Parse 如果丟擲了異常效能得有多低?再次,這段程式碼要是有點修改,我不是要每個 dt.Rows [n] 得改一遍?

不要重複發明輪子

我們再來看看其他的一些程式碼:

public List< stringGetDevices( string dev){
    List< string> devs= new List< string>();

     int start= 0;
     for( int i= 0;i<dev.Length;i++){
         if(dev[i]== '^'){
            devs.Add(dev.SubString(start,i));
            start=i+ 1;
        }
    }

     return devs;
}

有沒有很眼熟,沒錯,這就是對 String.Split () 函式的簡單實現。我的前輩應該是從 c++ 程式設計師轉過來的,習慣了各種功能自己實現一遍,但是他忽略了 C# 的很多東西。我們不去評判這段程式碼的優劣,而實際上他在很長一段時間都執行得很好。我們來看看使用這一段程式碼有什麼不好的地方:

  • 重複發明輪子。花費了額外的時間,函式的健壯性和很差
  • 可讀性差。其實是一個很簡單的功能,但是用上了這麼一段函式,起初我還以為有什麼特別的功能。


那麼,我們應該怎樣去避免重複發明輪子呢?我從個人的經歷來提出以下幾點,希望能夠對各位有所幫助:

  • 瞭解你所學的程式語言的特性。你可以看一本基礎的入門書籍,把所有的特性瀏覽一遍,或者上 MSDN,把相關的內容過一遍。
  • 在你決定動手發明一個輪子之前,先搜尋一下現成的解決方案。你還可以到 CodeProject、GitHub 之類的網站搜尋一下。在知乎上有很多人都在批評這麼一種現象,老是問一些重複性的問題,然後又職責知乎沒落了,沒有人回答他的問題,實際上相關問題已經有了很詳細的解答,那提問之前,不能首先去搜一下是否有現成的答案,反而指責沒有回答他的問題呢?
  • 你有一定的基礎之後,還應該去讀一下相關的經典書籍,深入瞭解其中的原理。比如,你覺得你有一定的基礎了,我建議你去把《CLR Via C#》多讀幾遍,你瞭解原理越多,你越是能夠利用這程式語言的特性,從而來實現原本那些你認為要靠自己寫程式碼的功能。


這裡我再舉一個我自己的例子。在我現有的程式中,我發現我需要越來越多的執行緒來執行一些簡單的任務,比如在每天檢測一下硬碟是否達到 90% 了,每天 9 點要控制一下空調的開啟而在網上 6 點的時候把空調關掉。

執行緒使用越來越多,我越是覺得浪費,因為這些現場僅僅只需完成一次或者有限的幾次,大部分時間都是沒有意義的,那麼怎麼辦呢?我決定自己寫一個任務類,來完成相關的事情。說幹就幹,我很快把這個類寫出來了。

public  abstract  class  MissionBase :  IMission
{
     private DateTime _nextExecuteTime;
     protected  virtual DateTime[] ExecuteTimePoints {  getprivate  set; }
     protected  virtual  int IntervalSeconds {  getprivate  set; }
     protected IEngine Engine {  getprivate  set; }

     public  bool IsCanceled{ get{……}}
     public  bool IsExecuting{ get{……}}
     public  bool IsTimeToExecute{ get{……}}

     public  abstract  bool Enable {  get; }
     public  abstract  string Name {  get; }

     protected  MissionBase( IEngine engine)
    
{
        ExecuteTimePoints =  null; //預設採用間隔的方式
        IntervalSeconds =  60 *  60; //預設的間隔為1個小時

        Engine = engine;
    }

     /// 任務的執行方法
     public  void  Done()
    
{
         if (Interlocked.CompareExchange( ref _isExecuting,  10) ==  1return;

         try
        {
            ……
        }
         finally
        {
            Interlocked.CompareExchange( ref _isExecuting,  01);
        }
    }
    
     ///實際方法的執行
     protected  abstract  void  DoneReal();
}

但是,實際上這個任務方法,並不好用,要寫的程式碼不少,而且可靠性還沒有保障。當然,我可以繼續完善這個類,但是我決定搜尋一下是否還有其他的方法。
直到有一天,我再次閱讀《CLR Via C#》,看到執行緒這一章,講到了 System.Threading.Timer 以及 ThreadPool 類時,我就知道了, 使用 Timer 類完全可以解決我的這個用盡量少的執行緒完成定時任務的問題。
因為從原理上來說,Timer 類無論你宣告瞭多少個,其實就只有一個執行緒在執行。當你到了執行時間時,這個管理執行緒會用 ThreadPool 來執行 Timer 中的函式,因為使用的 ThreadPool,執行完成之後,執行緒就馬上回收了,這個其實就完全實現了我所需要的功能。

等你無法重構的時候再考慮重寫


我帶過很多優秀的程式設計師,也與很多優秀的程式設計師共事過。有一大部分的程式設計師在看到一套系統不是那麼滿意,或者存在某些明顯的問題,就總是忍不住要把整套系統按自己覺得可以優化的方向來重寫,結果,重寫結構往往並不令人滿意。

系統中確實存在很多不合理的地方,但是有不少的這種程式碼,恰恰是為了解決一些特定場景下的問題的。也就是說,所有的規範以及程式設計的原則,其實也是有條件限制的,他可能在大部分的時候是正確的,能夠指導你完成你的任務,但是,並不是在所有地方都是適用的。比如資料庫正規化,但實際中我們的設計往往會考慮冗餘,這是違背正規化的,但是為什麼還有那麼多人趨之若鶩呢?因為我們可能需要用空間換時間。

如果我們一開始就考慮重寫,那麼你可能會陷入以下的困境:

  • 需要花更大的精力來完成一些看似簡單的 BUG
    你要知道,有一部分看似錯誤或者非常不優美的程式碼,其實恰恰是為了解決一些非常刁鑽的問題的。

  • 再也無法相容老的系統了
    你急於把原有系統重寫,卻往往忽略了對原有系統的相容,那麼你新的系統的推進則會十分緩慢。
    而老系統的維護,又會陷入及其尷尬的情況。

  • 過度設計,導致重寫計劃遲遲無法完成
    有重寫衝動的程式設計師往往是在架構設計上有一些讀到的見解,他們善於利用所學的各種設計模式和架構技巧來建立系統,但是越是想盡可能的利用設計模式,越是陷入過度設計的困局,導致重寫的計劃遲遲都無法完成。

  • 無法有效利用現有系統已經完成並測試的程式碼
    如果你確實有必要進行重寫,我還是建議你把程式碼儘可能的重構。
    因為重構之後的系統,能夠讓你更輕易的重寫,又最大限度了保留以前可用的 業務程式碼

我舉個例子,說明如何通過重構更好的利用現有程式碼的。
我有一個非常龐大的系統,其中有一塊功能是用於資料採集、儲存、告警管理以及電話、簡訊等告警通知。大致的結構如下:

class  MainEngine:IEngine{
     public  MainEngine (ConfigSettings config){
        
    }

     public  void  Start ();
     public  void  Stop ();
}

需要增加新的業務功能時,程式設計師寫的程式碼往往是這樣的:首先時修改配置類

class  ConfigSettings{
     public  bool NewFuncEnable{ get; private  set;}
     public  ConfigSettings(){
        NewFuncEnable=xx; //從配置檔案讀取
    }
}

接著修改主程式:

class  MainEngine:IEngine{
     private NewFuncClass newCls= new NewFuncClass();
     public  MainEngine (ConfigSettings config){
    }

     public  void  Start (){
         if(config.NewFuncEnable)
            newCls.Start();
    }
     public  void  Stop (){
         if(config.NewFuncEnable)
            newCls.Stop();
    }
}

在修改的過程中,往往是根據配置檔案來判斷新功能是否啟用。上面程式碼會造成什麼問題呢:

  • 主程式程式碼和擴充套件功能耦合性太強,每增加一個功能都要修改主程式程式碼,這裡非常非常容易出錯。尤其是新的人進度開發組,很容易就忘主程式中增加了一些致命性的程式碼。比如上述的擴充套件功能,可能是在特定的專案中才會有這個擴充套件功能,但是,寫程式碼的人忘記增加是否啟用的配置選項了,導致所有的專案都應用了這個功能,而這個功能需要特定的表,這樣就悲劇了。即使是你增加了配置,也是非常的不美觀,因為在通用的版本中使用了這個配置,往往會讓定製專案以外的人員感到困惑。
  • 增加擴充套件功能的人還需對整個 MainEngine 程式碼有一定的熟悉,否則,他根本就不知道在 Start 方法和 Stop 方法進行 newClas 的對應方法的呼叫
  • 如果你打算對這段程式碼進行重寫,那麼,你會感到非常的困難,因為你分不清楚 newCls 這個新例項的作用,要麼你花大精力去把所有程式碼理清楚,要麼直接就把這段新增的業務程式碼去掉了。

那麼我們如何對這段程式碼進行重構呢。首先,我們把新功能註冊的程式碼抽取出來,通過反射來實現新的功能的註冊。

private  void  RegisterTaskHandlerBundles()
    
{
         var bundles = xxx.BLL.Caches.ServiceBundleCache.Instance.GetBundles( "TaskHandlerBundle");
         if (bundles !=  null && bundles.Count >  0)
        {
             var asmCache =  new Dictionary< string, Assembly>();
             foreach ( var bundle  in bundles)
            {
                 try
                {
                     if (!asmCache.ContainsKey(bundle.Category)) asmCache.Add(bundle.Category, Assembly.Load(bundle.AssemblyName));
                     var handler = (ITaskHandler)asmCache[bundle.Category].CreateInstance(bundle.ClassName,  false, BindingFlags.Default,  null,
                         new  object[] {  this, bundle },  nullnull);
                    _taskHandlerBundles.Add(bundle, handler);
                }
                 catch (Exception e)
                {
                    NLogHelper.Instance.Error( "載入bundle[Name:{0},Assembly:{1}:Class:{2}]異常:{3}", bundle.Name, bundle.AssemblyName, bundle.ClassName, e.Message);
                }
            }
        }
    }

修改 MainEngine 程式碼

class  MainEngine:IEngine{
     private NewFuncClass newCls= new NewFuncClass();
     public  MainEngine (ConfigSettings config){
        RegisterTaskHandlerBundles();
    }

     public  void  Start (){
        _taskHandlerBundles.Start();
    }
     public  void  Stop (){
        _taskHandlerBundles.Stop();
    }
}

OK,現在我們再來看看怎麼實現原來的新增功能:你只需按規範新建一個類,繼承 ITaskHandler 介面,並實現介面的方法。最後在 XTGL_ServiceBundle 表中新增一條記錄即可。我們再來看看這麼做有什麼好處:

  • 新增的類只需按規範寫即可,完全對 MainEngine 程式碼沒有任何影響。你甚至可以把這個 MainEngine 程式碼寫在一個新建的 Dll 中。
  • 新增功能的這個業務類跟原來的程式碼解耦,非常方便進行新功能的業務測試,而無需考慮原有框架的影響
  • 新增功能的業務類與架構完全分離,我們在重寫程式碼中只要保證介面的穩定性,無論我們怎麼把系統架構重寫,我們可以馬上就重用上原有的業務功能程式碼。


重構的目標之一,就是把框架和業務完全分離。
有志於深入瞭解的同學,可以瞭解下反射、Ioc 和外掛話程式設計等。

學會單元測試,培養你的重構意識



可能上面說了這麼多,還是有很多人並不理解重構。沒關係,在這裡我教你們 一個快速入門的辦法,就是單元測試。什麼是單元測試,請自行 google。單元測試有什麼要求?
就是要求你要把每個方法都弄成儘量可以測試的。 儘量讓你的方法變成 是可測試的,就是培養你重構意識的利器 在你要求把方法變成可測試的過程,你就會發現你必須得不斷的修改你的方法,讓它的職責儘量單一,讓它儘量的與上下文無關,讓它儘可能通過方法引數的輸入輸出就能完成相關的功能,讓依賴的類都儘量改為介面而不是例項。
最終,你就會發覺,這就是重構!而且是在不知不覺中,你重構的功力就會大大提升,你程式設計的水平也會大大提升!
看到這裡,有經驗的程式設計師就會問,你這是在鼓勵我使用 TDD 嗎?不,不是的。TDD (Test-Driven Development) 鼓勵的是測試驅動開發,未開發之前先編寫單元測試用例程式碼,測試程式碼確定需要編寫什麼產品程式碼。
這是一種比較先進的開發方法,但是在程式設計的實踐過程中,我認為它過於繁瑣,很多中小企業很難實施,更別提我們個人開發者。我這裡提倡你用單元測試培養你的重構意識,可以說是一種後驅動,用於提高你的重構能力和重構願望,你完全可以把我的這個方法稱為 “ TDR (Test-Driven Refactoring)—— 測試驅動重構”。
當然,在開發之前如果你有意識的讓方法可測試,那麼你寫出來的函式將會是比較高質量的程式碼。當你的函式都是一個個可重用性高的函式之時,你將會發現,寫程式碼其實就像堆積木一樣,可以把一個大型的需求分解成無數細小的功能,很快的把需求實現。
以下是一個超大方法中的一段程式碼,如果你懂得怎樣讓這段程式碼程式設計一個可測試的方法,那麼,恭喜你,你入門了。


所謂重構


這篇文章也許稱為 “ 如何在程式設計中應用重構的思想” 更為貼切,但是我不想用這麼嚴肅的標題。
很多程式設計初學者,或者有多年程式設計經驗的人都覺得閱讀別人的程式碼非常困難,重構更是無從談起,他們要麼對這些程式碼望洋興嘆,要麼就是推翻從來。但是,如果我們有重構的意識,以及在程式設計的過程中熟悉一些程式碼調整和優化的小技巧,你自然而然就會培養出重構的能力。
重構,其實很簡單:

  • 把基礎打牢固
  • 多看點優秀的程式碼
  • 避免複製貼上,如果看見重複程式碼時應該有意識要消滅它
  • 減少對程式碼生成器的依賴
  • 在處理現有程式碼時儘量用重構代替重寫,在重寫之前一定要先重構
  • 儘量讓所有的方法都是可測試的


如果你堅持這麼去做了,一段時間之後感覺自然就出來了。
重構的目的,是讓你的程式碼更為精簡、穩定、能夠重用,是最大程度的讓功能和業務分離。 在重構的過程中,你的閱讀程式碼的能力、寫出優秀程式碼的能力以及系統架構能力都會穩步提升。你成為一個優秀的程式設計師將指日可待。
作者:馬非碼

來源: https://www.cnblogs.com/marvin/p/4133973.html#!comments

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31556440/viewspace-2674448/,如需轉載,請註明出處,否則將追究法律責任。

相關文章