[.net 面向對象編程基礎] (14) 重構
通過面向對象三大特性:封裝、繼承、多態的學習,可以說我們已經掌握了面向對象的核心。接下來的學習就是如何讓我們的代碼更優雅、更高效、更易讀、更易維護。當然了,這也是從一個普通程序員到一個高級程序員的必由之路。就看病一樣,普通醫生只能治標,高級醫生不但看好病,還能除病根。
1.什么時重構?
重構(Refactoring)就是在不改變軟件現有功能的基礎上,通過調整程序代碼改善軟件的質量、性能,使其程序的設計模式和架構更趨合理,提高軟件的擴展性和維護性。
目的:是提高其可理解性,降低其修改成本。
通俗的說法就是,程序的功能和結果沒有任何的變化。重構只是對程序內部結構進行調整,讓代碼更加容易理解,然后更容易維護。也就是代碼的優化。
通過上述定義,可以看出,重構並不是.net的本身的特性,而是軟件設計范疇。
2.重構的目的
A.改進軟件的設計
在實際工作中,為了趕進度或是為了短期利益,再或者是沒有完全摸清軟件整體架構的情況下,對代碼進行改動。而這些改動的積累很容易使軟件偏離它原先的設計初衷,使軟件變很很難維護或無法維護。
而重構可以幫助重新組織代碼,重新清晰的體現結構和進一步改進設計。
B.提高代碼的質量和可維護性
容易理解的代碼很容易維護和做進一步開發。即使寫這些代碼的程序員本身而言,容易更解的代碼也能幫助他容易的修改。
代碼也是文檔,首先是寫給人看的,其次才是計算機。
C.幫助盡早的發現錯誤
重構是一個復習和反饋的過程,在另一個時段重新審視自己或別人的代碼,可以更容易發現問題和加深對代碼的理解.
重構是一個良好的開發習慣。
D.可以提高開發速度
重構對設計和代碼的改進,都可以有效提高開發速度。
在一個有缺陷的設計和混亂的代碼基礎上開發,即使表面是進度較快,但本質是延后對設計缺陷的發現和對錯誤的修改。也就延后了開發風險,最終要在開發后期付出更多的代價。
一句話,出來混,遲早是要還的!!
3.重構的時機
重構的時候,即何時需要重構,何時不需要
A.首先,以下幾種情況需要重構:
過大的類和過長的方法
過長的方法由於包含的邏輯過於復雜,錯誤機率將直線上升,而可讀性則直線下降,類的健壯性很容易被打破。當看到一個過長的方 法時,需要想辦法將其划分為多個小方法,以便於分而治之。
牽一發而需要動全身的修改
當你發現修改一個小功能,或增加一個小功能時,就引發一次代碼地震,也許是你的設計抽象度不夠理想,功能代碼太過分散所引起的。
類之間需要過多的通訊
A類需要調用B類的過多方法訪問B的內部數據,在關系上這兩個類顯得有點狎昵,可能這兩個類本應該在一起,而不應該分家。
過度耦合的信息鏈
如果你在代碼中看到需要獲取一個信息,需要一個類的方法調用另一個類的方法,層層掛接,就象輸油管一樣節節相連。這往往是因為銜接層太多造成的,需要查看就否有可移除的中間層,或是否可以提供更直接的調用方法。
各自為政的功能模塊
如果你發現有兩個類或兩個方法雖然命名不同但卻擁有相似或相同的功能,你會發現往往是因為開發團隊成員協調不夠造成的。筆者曾經寫了一個頗好用的字符串處理類,但因為沒有及時通告團隊其他人員,后來發現項目中居然有三個字符串處理類。革命資源是珍貴的,我們不應各立山頭干革命。
不完美的設計
每個系統都或多或少存在不完美的設計,剛開始可能注意不到,到后來才會慢慢凸顯出來,此時唯有勇於更改才是最好的出路。
缺少必要的注釋
雖然許多軟件工程的書籍常提醒程序員需要防止過多注釋,但這個擔心好象並沒有什么必要。往往程序員更感興趣的是功能實現而非代碼注釋,因為前者更能帶來成就感,所以代碼注釋 往往不是過多而是過少,過於簡單。人的記憶曲線下降的坡度是陡得嚇人的,當過了一段時間后再回頭補注釋時,很容易發生"提筆忘字,愈言且止"的情形。
曾在網上看到過微軟的代碼注釋,其詳盡程度讓人嘆為觀止,也從中體悟到了微軟成功的一個經驗。
(以上關於重構的內容來自網上小伙伴的分析,還是比較全面的,摘錄過來分享之)
B.還有幾種情況是不適用重構的:
代碼混亂,錯誤百出,這種情況,不是重構而是需要重寫了
大型多模塊軟件,需要逐步重構,不是一下子完成
重構需要太長的時間,這種情況下不建議重構。
項目即將進入交付階段,隱定性勝過其它。
3.如何進行重構
前面講了太多的理論知識,下面來點硬貨,說說重構的方法。
3.1使用VS.NET 自身的功能實現快速重構
VS.net本身關於重構的功能,可能很多人很少用到,作為一個重構的輔助功能,雖說不能完全實現重構,但是可以幫助我們快速優化代碼。
3.1.1重構類型
<1>. 重命名
<2>.提取方法
<3>. 封裝字段
<4>. 提取接口
<5>. 將局部變量提升為參數
<6>. 移除參數
<7>. 重新排列參數
VS.NET中提供了這么七種重構的類型。我們在代碼編輯窗口中,點擊鼠標右鍵,可以看到如下圖所示:
下面,我們逐一說明
<1>重命名
我們在代碼重構過程中,會有不按規范命名的情況發生或者我們想讓一段代碼產生一個副本。
A. 提供了一種重命名代碼符號(如字段、局部變量、方法、命名空間、屬性和類型)標識符的簡單方法.
B. “重命名”功能除了可用來更改標識符的聲明和調用以外,還可用來更改注釋中和字符串中的名稱.
如下圖所示,選中一個名稱后,輸入新名稱,VS.NET會提示你更改那些名字。
<2>.提取方法
A 可以通過從現有成員的代碼塊中提取選定的代碼來創建新方法.
B. 創建的新方法中包含選定的代碼,而現有成員中的選定代碼被替換為對新方法的調用.
C. 代碼段轉換為其自己的方法,使您可以快速而准確地重新組織代碼,以獲得更好的重用和可靠性.
• 優點
A. 通過強調離散的可重用方法鼓勵最佳的編碼做法。
B. 鼓勵通過較好的組織獲得自記錄代碼。當使用描述性名稱時,高級別方法可以像讀取一系列注釋一樣進行讀取。
C. 鼓勵創建細化方法,以簡化重載。
D. 減少代碼重復.
如下圖,我們選中一個方法中的代碼片段,點重構中的 “提取方法”彈出下下對話框,我們重命名一個新的方法名
確定后,如下所示:
生成一個靜態的方法。在一個方法實現中代碼片段太長的時候,我們可以很方便的進行方法提取了。
<3>. 封裝字段
A. 可以從現有字段快速創建屬性,然后使用對新屬性的引用無縫更新代碼.
B. 當某個字段為public(C# 參考)時,其他對象可以直接訪問該字段並對其進行修改,而不會被擁有該字段的對象檢測到。通過使用屬性(C# 編程指南)封裝該字段,可以禁止對字段的直接訪問。
C. 僅當將光標與字段聲明置於同一行時,才可以執行“封裝字段”操作。
• 實例
大部分開發者都習慣把類級的變量(字段)暴露給外界。由於每一個對象都屬於面向對象編程,所以開發者應該允許通過屬性或方法來存取變量。這種情況可以使用重構菜單下的"封裝字段"選項來進行處理。
為此,選擇你想包裝在一個屬性中的類級變量並且選擇"封裝字段"選項。這將打開一個如下圖所示的對話框:
你需要輸入該屬性的名字並且決定是否你想從類外或類內部更新到該變量的參考。就象"重命名"對話框一樣,你可以在應用之前先預覽一下所作的改變。
如下圖所示,假如我們要在動物這個類中,加一個屬性,我們使用封裝字段,
如果選擇“外部”確定后,代碼如下:
可以看到,為我們自動增加了一個外部屬性
<4>• 提取接口
A. 使用來自現有類、結構或接口的成員創建新接口的簡單方法.
B. 當幾個客戶端使用類、結構或接口中成員的同一子集時,或者當多個類、結構或接口具有通用的成員子集時,在接口中嵌入成員子集將很有用.
C. 僅當將光標定位於包含要提取成員的類、結構或接口中時,才可以訪問此功能。當光標處於此位置時,調用“提取接口”重構操作.
如下圖所示,我們在類名稱點擊右鍵 重構,選擇提取接口,在彈出窗口中,輸入接口名稱,選擇類的公有成員,則為它們創建了一個接口文件,非常實用。
<5>• 將局部變量提升為參數
A. 提供一種簡單的方法,以在正確更新調用站點的同時將變量從局部使用移動至方法、索引器或構造函數參數.
B. 調用“將局部變量提升為參數”操作時,變量將被添加到成員參數列表的結尾處.
C. 對已修改成員的所有調用都將使用新參數(將替代最初賦給該變量的表達式)立即進行更新,並保留代碼,以使其像變量提升之前那樣正常工作.
D. 將常數值賦值給提升的變量時,此重構操作效果最好。必須聲明並初始化該變量,而不能僅聲明或僅賦值.
• 實例
原代碼:
private static void NewMethod2() { string s = ""; }
選中s,轉換后
private static void NewMethod2(string s) { }
<6>• 移除參數
A. 從方法、索引器或委托中移除參數的簡單方法.
B. 在調用成員的任何位置,都會將參數移除以反映新聲明.
• 實例
原代碼
protected void Page_Load(EventArgs e, object sender) { int i = 0; NewMethod2("1","2"); } private static void NewMethod2(string s1, string s2) { string s = s1 + s2; }
移除后的代碼
protected void Page_Load(EventArgs e, object sender) { int i = 0; NewMethod2(); } private static void NewMethod2() { string s = s1 + s2; }
<7>• 重新排列參數
A. 對方法、索引器和委托的參數順序進行更改的簡單方法.
B. 可以通過方法聲明或方法調用來重新排列參數。要將光標置於方法聲明或委托聲明中,而不是置於正文中。
• 實例
原代碼:
private static void NewMethod2(string s1,string s2) { }
重新排列后
private static void NewMethod2(string s2,string s1) { }
4.重構實例
我們通過一個實例來看看重構帶來的好處,還是我們前一節的關於動物叫的例子,有一個基類 動物(Animal)有成員屬性名字(Name)
方法叫聲(Shout)和叫的次數的虛方法(getShoutCount),它有N個派生類,我們先看重構前的代碼如下:
1 /// <summary> 2 /// 動物類(父類) 3 /// </summary> 4 class Animal 5 { 6 /// <summary> 7 /// 名字 8 /// 說明:類和子類可訪問 9 /// </summary> 10 protected string name; 11 12 13 /// <summary> 14 /// 構造函數 15 /// </summary> 16 /// <param name="name"></param> 17 public Animal(string name) 18 { 19 this.name = name; 20 } 21 22 private int shoutNum = 3; 23 public int ShoutNum 24 { 25 get { return shoutNum; } 26 set { shoutNum = value; } 27 } 28 29 /// <summary> 30 /// 名字(虛屬性) 31 /// </summary> 32 public virtual string MyName 33 { 34 get { return this.name; } 35 36 } 37 38 /// <summary> 39 /// 叫(虛方法) 40 /// </summary> 41 public virtual void Shout() 42 { 43 Console.WriteLine("我會叫!"); 44 } 45 46 } 47 48 /// <summary> 49 /// 狗(子類) 50 /// </summary> 51 class Dog : Animal 52 { 53 string myName; 54 public Dog(string name) 55 : base(name) 56 { 57 myName = name; 58 } 59 60 /// <summary> 61 /// 名字(重寫父類屬性) 62 /// </summary> 63 public override string MyName 64 { 65 get { return "我是:狗狗,我叫:" + this.name; } 66 } 67 68 /// <summary> 69 /// 叫(重寫父類方法) 70 /// </summary> 71 public override void Shout() 72 { 73 string result = ""; 74 for (int i = 0; i < ShoutNum; i++) 75 result += "汪!"; 76 Console.WriteLine(result); 77 } 78 } 79 /// <summary> 80 /// 貓(子類) 81 /// </summary> 82 class Cat : Animal 83 { 84 string myName; 85 public Cat(string name) 86 : base(name) 87 { 88 myName = name; 89 } 90 /// <summary> 91 /// 名字(重寫父類屬性) 92 /// </summary> 93 public override string MyName 94 { 95 get { return "我是:貓咪,我叫:" + this.name; } 96 97 } 98 99 /// <summary> 100 /// 叫(重寫父類方法) 101 /// </summary> 102 public override void Shout() 103 { 104 string result = ""; 105 for (int i = 0; i < ShoutNum; i++) 106 result += "喵!"; 107 Console.WriteLine(result); 108 } 109 } 110 111 /// <summary> 112 /// 羊(子類) 113 /// </summary> 114 class Sheep : Animal 115 { 116 string myName; 117 public Sheep(string name) 118 : base(name) 119 { 120 myName = name; 121 } 122 /// <summary> 123 /// 名字(重寫父類屬性) 124 /// </summary> 125 public override string MyName 126 { 127 get { return "我是:羊羊,我叫:" + this.name; } 128 129 } 130 131 /// <summary> 132 /// 叫(重寫父類方法) 133 /// </summary> 134 public override void Shout() 135 { 136 string result = ""; 137 for (int i = 0; i < ShoutNum; i++) 138 result += "咩!"; 139 Console.WriteLine(result); 140 } 141 }
我們可以看到,雖然這段代碼實現了繼承和多態,封裝的特性,代碼還是比較簡潔的,但是有一點就是這個叫的方法,每個子類中都要寫一次循環。假如又來了豬啊,牛啊,這些動物,是不是代碼量也不少啊。我們能不能只寫一次循環呢,答案是肯定的,看我們重構后的代碼:
1 /// <summary> 2 /// 動物類(父類) 3 /// </summary> 4 class Animal 5 { 6 /// <summary> 7 /// 名字 8 /// 說明:類和子類可訪問 9 /// </summary> 10 protected string name; 11 12 /// <summary> 13 /// 構造函數 14 /// </summary> 15 /// <param name="name"></param> 16 public Animal(string name) 17 { 18 this.name = name; 19 } 20 21 private int shoutNum = 3; 22 public int ShoutNum 23 { 24 get { return shoutNum; } 25 set { shoutNum = value; } 26 } 27 28 /// <summary> 29 /// 名字(虛屬性) 30 /// </summary> 31 public virtual string MyName 32 { 33 get { return this.name; } 34 35 } 36 37 /// <summary> 38 /// 叫聲,這個方法去掉虛方法,把循環寫在這里 39 /// </summary> 40 public void Shout() 41 { 42 string result = ""; 43 for (int i = 0; i < ShoutNum; i++) 44 result += getShoutSound()+"!"; 45 46 Console.WriteLine(MyName); 47 Console.WriteLine(result); 48 } 49 /// <summary> 50 /// 創建一個叫聲的虛方法,子類重寫 51 /// </summary> 52 /// <returns></returns> 53 public virtual string getShoutSound() 54 { 55 return ""; 56 } 57 } 58 59 /// <summary> 60 /// 狗(子類) 61 /// </summary> 62 class Dog : Animal 63 { 64 string myName; 65 public Dog(string name): base(name) 66 { 67 myName = name; 68 } 69 /// <summary> 70 /// 名字(重寫父類屬性) 71 /// </summary> 72 public override string MyName 73 { 74 get { return "我是:狗狗,我叫:" + this.name; } 75 } 76 /// <summary> 77 /// 叫(重寫父類方法) 78 /// </summary> 79 public override string getShoutSound() 80 { 81 return "汪!"; 82 } 83 } 84 /// <summary> 85 /// 貓(子類) 86 /// </summary> 87 class Cat : Animal 88 { 89 string myName; 90 public Cat(string name): base(name) 91 { 92 myName = name; 93 } 94 /// <summary> 95 /// 名字(重寫父類屬性) 96 /// </summary> 97 public override string MyName 98 { 99 get { return "我是:貓咪,我叫:" + this.name; } 100 } 101 /// <summary> 102 /// 叫(重寫父類方法) 103 /// </summary> 104 public override string getShoutSound() 105 { 106 return "喵!"; 107 } 108 } 109 110 /// <summary> 111 /// 羊(子類) 112 /// </summary> 113 class Sheep : Animal 114 { 115 string myName; 116 public Sheep(string name): base(name) 117 { 118 myName = name; 119 } 120 /// <summary> 121 /// 名字(重寫父類屬性) 122 /// </summary> 123 public override string MyName 124 { 125 get { return "我是:羊羊,我叫:" + this.name; } 126 } 127 /// <summary> 128 /// 叫(重寫父類方法) 129 /// </summary> 130 public override string getShoutSound() 131 { 132 return "咩!"; 133 } 134 }
這樣重構,是不是代碼量就少很多了,結構也更加清晰了。。
調用一:
//調用 Animal sheep = new Sheep("美羊羊"); sheep.Shout(); Console.ReadLine();
結果如下:
//調用結果 //我是:羊羊,我叫:美羊羊 //咩!咩!咩!
調用二:
//調用 Animal dog= new Dog("旺財"); dog.Shout(); Console.ReadLine();
結果如下:
//調用結果 //我是:狗狗,我叫:旺財 //汪!汪!汪!
總結:重構是一門復雜的學問,本節內容只是重構的皮毛而已,有一些書籍用幾千頁的篇幅來介紹中重構。能否熟練使用重構,寫出優雅高效的代碼是區分一個程序員優秀的標准之一,重構也是學習設計模的基礎,這需要我們不斷的練習和思考才能做好。
要點:
A.重構(Refactoring)就是在不改變軟件現有功能的基礎上,通過調整程序代碼改善軟件的質量、性能,使其程序的設計模式和架構更趨合理,提高軟件的擴展性和維護性。
B.重構不是.NET面向對象本身的特性,而屬於一種軟件設計范疇。
C.重構提高了代碼的可讀性,可維護性;也使得代碼結構更加清晰。
D.能否有效的重構代碼,是一個程序員優秀與否的標准之一。也是學習設計模式和軟件架構的基礎。
E.重構是一門代碼藝術。
==============================================================================================
返回目錄
<如果對你有幫助,記得點一下推薦哦,有不明白的地方或寫的不對的地方,請多交流>
==============================================================================================