[.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.重構是一門程式碼藝術。
==============================================================================================
返回目錄
<如果對你有幫助,記得點一下推薦哦,有不明白的地方或寫的不對的地方,請多交流>
==============================================================================================