天天看點

從把3000行代碼重構成15行代碼談起

  從把3000行代碼重構成15行代碼談起  [複制連結]

發表于 2015-1-12 10:01 | 來自  51CTO網頁

[隻看他] 樓主

從把3000行代碼重構成15行代碼談起

如果你認為這是一個标題黨,那麼我真誠的懇請你耐心的把文章的第一部分讀完,然後再下結論。如果你認為能夠戳中您的G點,那麼請随手點個贊。

把三千行代碼重構為15行

那年我剛畢業,進了現在這個公司。公司是搞資料中心環境監控的,裡面充斥着嵌入式、精密空調、總線、RFID的概念,我一個都不懂。還好,公司之前用Delphi寫的老用戶端因為太慢,然後就搞了個Webform的替代,恰好我對Asp.Net還算了解,我對業務的不了解并不妨礙我稱成為這個公司的一個程式員。小公司也有小公司的好,人少,進去很快負責代碼開發。我當然也就搞這個資料中心智能管理系統啦。

這個系統非常的龐大,尤其牛逼的是支援用戶端組态,然後動态生成網頁,資料還能通過Socket實時監控(那時我還真就不懂網絡程式設計)。這個對于當時的我來說,真真是高、大、上呐!!當時跟着了解整個系統大半個月才算能夠調試,寫一些簡單的頁面。

在維護系統的過程中,時不時要擴充一些功能,也就接觸了下面這個類:

從把3000行代碼重構成15行代碼談起

圖檔有壓縮,點選這裡檢視清晰的原圖。

看到沒有,就是當年最最流行的三層架構的産物,對于剛出茅廬的毛頭小子來說,這是多麼專業的檔案頭注釋,還有反射也就算了,這構造函數還能靜态的,還能私有的?那時剛接觸這麼高大上的代碼的我,瞬間給跪了!

但是,類寫多了,我就感覺越來越别扭,就是下面這段代碼:

從把3000行代碼重構成15行代碼談起

圖檔有壓縮,點選這裡檢視清晰的原圖。

每增加一個表,除了要改接口、要改DAL、要改BLL之外,還得在這個工廠類添加一個方法,真真是累到手抽筋,即使有當時公司了的G工給我推薦的神器——動軟代碼生成器,這粘貼複制的幾遍,也是讓我感覺到異常繁瑣,有時候打鍵盤稍微累了點,還把複制出來代碼改錯了,你妹的,難道這就是程式員該幹的事情,不,絕對不是!我想起了一句至理名言:當你覺得代碼重複出現在程式中的時候,就應該重構了。是的,在這句話的指導下,我開始了折騰,決定挑戰這個高大上的代碼,事實證明,思想的力量是無窮的。

那麼,怎麼修改呢,仔細觀察之後,發現其中className的生成跟傳回的類型非常類似,隻是一個是類名,一個是字元串,這兩者之間應該能夠關聯起來。于是google了一下(當時GFW還沒猖獗起來哈),隐隐約約就找到了“反射”這兩個字,深入了解之後,确定可以完成。

接下來,就是傳回的類型了,傳回的類型并不固定,但是似乎很有規律……這個似乎好像在哪裡見過,對了,模闆,C++課程上有講過的,于是再次google,了解到了C#中使用了泛型代替了C++中的模闆。在學習完泛型和反射之後,并參考了網上的一些文章,我搗鼓出了下面的代碼:

從把3000行代碼重構成15行代碼談起

圖檔有壓縮,點選這裡檢視清晰的原圖。

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

看着原來滾十幾螢幕的代碼,變成了十多行的代碼,真是爽到了骨子裡去了,太幹淨了!唯一讓我擔憂的是,我進公司的時候,幫忙整理公司申請軟體著作權都是需要代碼量的,根據代碼多少行來評估軟體的大小,萬一老闆知道了我非但沒有幫公司增加代碼量,還減少了,會不會立即把我開掉?我沒敢給我們老闆展示我優秀的成果,所幸,這段代碼非但沒有出過任何問題,還避免了以前同僚老是在新增一個類之後,把代碼複制過來,但是沒有正确修改的問題,大大提高了效率。雖然,我沒敢大事宣布我的勞動成果,但是這次成功的修改,則徹底讓我走上了代碼重構的不歸路。

看到這裡,大家應該知道這個案例是否真實的了吧。我相信,從08年開始的碼農們,看到這種類似的代碼絕對不比我少。那麼,我想告訴你們的是什麼呢?

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

少用代碼生成器我們來分析一下,為什麼我之前的前輩會寫出上面的代碼。我歸結起來有以下幾點:

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

至今為止,還是很多人使用代碼生成器,那麼我們應該怎麼對待這個問題呢。我認為,代碼生成器确實可以減少你不少工作,但是少用,那些重複性的工作,除了部分确實是沒有辦法的,其他大部分都是可以通過架構解決的,舉例來說,像三層架構,真正需要用到代碼生成器的,也就是Model類而已,其他的完全可以在架構中完成。是以你要竭盡全力的思考怎麼在架構中來減少你的重複性工作,而不是依賴于代碼生成器。

另外,如果你還是在用相關的代碼生成工具,請重新定義“動軟代碼生成器”的代碼模闆,自己寫一個模闆;或者使用CodeSmith來完全制定自己的代碼生成,因為動軟給的代碼模闆真心亂,比如下面這段代碼:

01

for

(intn =

; n < rowsCount; n++)

02

{

03

model = newDBAccess.Model.eventweek();

04

if

(dt.Rows[n][

"GroupNo"

].ToString()!=

""

)

05

{

06

model.GroupNo=

int

.Parse(dt.Rows[n][

"GroupNo"

].ToString());

07

}

08

if

(dt.Rows[n][

"Week0"

].ToString()!=

""

)

09

{

10

model.Week0=

int

.Parse(dt.Rows[n][

"Week0"

].ToString());

11

}

12

if

(dt.Rows[n][

"Week1"

].ToString()!=

""

)

13

{

14

model.Week1=

int

.Parse(dt.Rows[n][

"Week1"

].ToString());

15

}

16

}

首先,你就不能用 var row=dt.Rows[n] 替代嗎?其次,直接用int.Parse效率多低?再次,dt.Rows[n]["Week0"]為NULL怎麼辦?

不要重複發明輪子我們再來看看其他的一些代碼:

01

publicList<string> GetDevices(string dev){

02

List<string> devs=newList<string>();

03

04

intstart=

;

05

for

(inti=

;i<dev.Length;i++){

06

if

(dev==

'^'

){

07

devs.Add(dev.SubString(start,i));

08

start=i+

1

;

09

}

10

}

11

12

returndevs;

13

}

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

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

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

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

這裡我再舉一個我自己的例子。在我現有的程式中,我發現我需要越來越多的線程來執行一些簡單的任務,比如在每天檢測一下硬碟是否達到90%了,每天9點要控制一下空調的開啟而在網上6點的時候把空調關掉。線程使用越來越多,我越是覺得浪費,因為這些現場僅僅隻需完成一次或者有限的幾次,大部分時間都是沒有意義的,那麼怎麼辦呢?我決定自己寫一個任務類,來完成相關的事情。說幹就幹,我很快把這個類寫出來了。

01

publicabstractclassMissionBase : IMission

02

{

03

privateDateTime _nextExecuteTime;

04

protectedvirtualDateTime[] ExecuteTimePoints { get; privateset; }

05

protectedvirtualint IntervalSeconds { get; privateset; }

06

protectedIEngine Engine { get; privateset; }

07

08

publicboolIsCanceled{get{……}}

09

publicboolIsExecuting{get{……}}

10

publicboolIsTimeToExecute{get{……}}

11

12

publicabstractboolEnable { get; }

13

publicabstract string Name { get; }

14

15

protectedMissionBase(IEngine engine)

16

{

17

ExecuteTimePoints =

null

;

//預設采用間隔的方式

18

IntervalSeconds =

60

*

60

;

//預設的間隔為1個小時

19

20

Engine = engine;

21

}

22

23

/// 任務的執行方法

24

publicvoidDone()

25

{

26

if

(Interlocked.CompareExchange(ref _isExecuting,

1

,

) ==

1

)

return

;

27

28

try

29

{

30

……

31

}

32

finally

33

{

34

Interlocked.CompareExchange(ref _isExecuting,

,

1

);

35

}

36

}

37

38

///實際方法的執行

39

protectedabstractvoidDoneReal();

40

}

但是,實際上這個任務方法,并不好用,要寫的代碼不少,而且可靠性還沒有保障。當然,我可以繼續完善這個類,但是我決定搜尋一下是否還有其他的方法。直到有一天,我再次閱讀《CLR Via C#》,看到線程這一章,講到了System.Threading.Timer以及ThreadPool類時,我就知道了,使用Timer類完全可以解決我的這個用盡量少的線程完成定時任務的問題。

因為從原理上來說,Timer類無論你聲明了多少個,其實就隻有一個線程在執行。當你到了執行時間時,這個管理線程會用ThreadPool來執行Timer中的函數,因為使用的ThreadPool,執行完成之後,線程就馬上回收了,這個其實就完全實作了我所需要的功能。

等你無法重構的時候再考慮重寫我帶過很多優秀的程式員,也與很多優秀的程式員共事過。有一大部分的程式員在看到一套系統不是那麼滿意,或者存在某些明顯的問題,就總是忍不住要把整套系統按自己覺得可以優化的方向來重寫,結果,重寫結構往往并不令人滿意。系統中确實存在很多不合理的地方,但是有不少的這種代碼,恰恰是為了解決一些特定場景下的問題的。也就是說,所有的規範以及程式設計的原則,其實也是有條件限制的,他可能在大部分的時候是正确的,能夠指導你完成你的任務,但是,并不是在所有地方都是适用的。比如資料庫範式,但實際中我們的設計往往會考慮備援,這是違背範式的,但是為什麼還有那麼多人趨之若鹜呢?因為我們可能需要用空間換時間。

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

  • 需要花更大的精力來完成一些看似簡單的BUG

    你要知道,有一部分看似錯誤或者非常不優美的代碼,其實恰恰是為了解決一些非常刁鑽的問題的。

  • 再也無法相容老的系統了

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

  • 過度設計,導緻重寫計劃遲遲無法完成

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

  • 無法有效利用現有系統已經完成并測試的代碼

    如果你确實有必要進行重寫,我還是建議你把代碼盡可能的重構。因為重構之後的系統,能夠讓你更輕易的重寫,又最大限度了保留以前可用的業務代碼。

我舉個例子,說明如何通過重構更好的利用現有代碼的。

我有一個非常龐大的系統,其中有一塊功能是用于資料采集、存儲、告警管理以及電話、短信等告警通知。大緻的結構如下:

1

classMainEngine:IEngine{

2

publicMainEngine(ConfigSettings config){

3

4

}

5

6

publicvoidStart();

7

publicvoidStop();

8

}

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

1

classConfigSettings{

2

publicboolNewFuncEnable{get;privateset;}

3

publicConfigSettings(){

4

NewFuncEnable=xx;

//從配置檔案讀取

5

}

6

}

接着修改主程式:

01

classMainEngine:IEngine{

02

privateNewFuncClass newCls=newNewFuncClass();

03

publicMainEngine(ConfigSettings config){

04

}

05

06

publicvoidStart(){

07

if

(config.NewFuncEnable)

08

newCls.Start();

09

}

10

publicvoidStop(){

11

if

(config.NewFuncEnable)

12

newCls.Stop();

13

}

14

}

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

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

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

01

privatevoidRegisterTaskHandlerBundles()

02

{

03

var bundles = xxx.BLL.Caches.ServiceBundleCache.Instance.GetBundles(

"TaskHandlerBundle"

);

04

if

(bundles !=

null

&& bundles.Count >

)

05

{

06

var asmCache = newDictionary<string, Assembly>();

07

foreach (var bundle in bundles)

08

{

09

try

10

{

11

if

(!asmCache.ContainsKey(bundle.Category)) asmCache.Add(bundle.Category, Assembly.Load(bundle.AssemblyName));

12

var handler = (ITaskHandler)asmCache[bundle.Category].CreateInstance(bundle.ClassName,

false

, BindingFlags.Default,

null

,

13

newobject[] {

this

, bundle },

null

,

null

);

14

_taskHandlerBundles.Add(bundle, handler);

15

}

16

catch

(Exception e)

17

{

18

NLogHelper.Instance.Error(

"加載bundle[Name:{0},Assembly:{1}:Class:{2}]異常:{3}"

, bundle.Name, bundle.AssemblyName, bundle.ClassName, e.Message);

19

}

20

}

21

}

22

}

修改MainEngine代碼

01

classMainEngine:IEngine{

02

privateNewFuncClass newCls=newNewFuncClass();

03

publicMainEngine(ConfigSettings config){

04

RegisterTaskHandlerBundles();

05

}

06

07

publicvoidStart(){

08

_taskHandlerBundles.Start();

09

}

10

publicvoidStop(){

11

_taskHandlerBundles.Stop();

12

}

13

}

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

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

重構的目标之一,就是把架構和業務完全分離。

有志于深入了解的同學,可以了解下反射、Ioc和插件話程式設計等。

學會單元測試,培養你的重構意識可能上面說了這麼多,還是有很多人并不了解重構。沒關系,在這裡我教你們一個快速入門的辦法,就是單元測試。什麼是單元測試,請自行google。單元測試有什麼要求?就是要求你要把每個方法都弄成盡量可以測試的。盡量讓你的方法變成是可測試的,就是培養你重構意識的利器。在你要求把方法變成可測試的過程,你就會發現你必須得不斷的修改你的方法,讓它的職責盡量單一,讓它盡量的與上下文無關,讓它盡可能通過方法參數的輸入輸出就能完成相關的功能,讓依賴的類都盡量改為接口而不是執行個體。最終,你就會發覺,這就是重構!而且是在不知不覺中,你重構的功力就會大大提升,你程式設計的水準也會大大提升!

看到這裡,有經驗的程式員就會問,你這是在鼓勵我使用TDD嗎?不,不是的。TDD(Test-Driven Development)鼓勵的是測試驅動開發,未開發之前先編寫單元測試用例代碼,測試代碼确定需要編寫什麼産品代碼。這是一種比較先進的開發方法,但是在程式設計的實踐過程中,我認為它過于繁瑣,很多中小企業很難實施,更别提我們個人開發者。我這裡提倡你用單元測試培養你的重構意識,可以說是一種後驅動,用于提高你的重構能力和重構願望,你完全可以把我的這個方法稱為“TDR(Test-Driven Refactoring)——測試驅動重構”。當然,在開發之前如果你有意識的讓方法可測試,那麼你寫出來的函數将會是比較高品質的代碼。當你的函數都是一個個可重用性高的函數之時,你将會發現,寫代碼其實就像堆積木一樣,可以把一個大型的需求分解成無數細小的功能,很快的把需求實作。

以下是一個超大方法中的一段代碼,如果你懂得怎樣讓這段代碼程式設計一個可測試的方法,那麼,恭喜你,你入門了。

從把3000行代碼重構成15行代碼談起

圖檔有壓縮,點選這裡檢視清晰的原圖。

所謂重構如果你有耐心看到這裡,你應該知道,我并非一個标題黨,而這篇文章也許稱為“如何在程式設計中應用重構的思想”更為貼切,但是我不想用這麼嚴肅的标題。

很多程式設計初學者,或者有多年程式設計經驗的人都覺得閱讀别人的代碼非常困難,重構更是無從談起,他們要麼對這些代碼望洋興歎,要麼就是推翻從來。但是,如果我們有重構的意識,以及在程式設計的過程中熟悉一些代碼調整和優化的小技巧,你自然而然就會培養出重構的能力。

重構,其實很簡單:

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

如果你堅持這麼去做了,一段時間之後感覺自然就出來了。

重構的目的,是讓你的代碼更為精簡、穩定、能夠重用,是最大程度的讓功能和業務分離。在重構的過程中,還要注意下dex源碼加密保護,通過以上全文的介紹,相信你的閱讀代碼的能力、寫出優秀代碼的能力以及系統架構能力都會穩步提升。你成為一個優秀的程式員将指日可待。 本帖最近評分記錄

  • rongwei84n 無憂币 +8 精品文章 2015-1-12 19:05