C++ 入門指南 4.01
單元 22 - 重構
重構 (refactoring) 是指不改變軟體 (software) 外部行為下,對程式原始碼 (source code) 進行整理、修改,不外是希望提升原始碼的可讀性及更易於維護
Readability
Maintainability
Extensibility
簡單說,只要感覺原始碼有些不對勁就該重構,尤其在一個大的開發團隊中,每個小組負責的部份可能都不同,因而常常會發生識別字 (identifier) 命名不一致或是太多類別有共通特性的情況。所以需要適時的重構原始程式碼,這也是軟體開發過程中一項重要的工作。
常見的重構技術如下
- 對資料成員 (data member) 進行封裝 (encapsulation) ,並將資料成員及成員函數 (member function) 名稱更改為具有一致性。
- 移動資料成員或成員函數,讓其出現在更適合的位置。
- 使型態 (type) 可以更具通用性,或挑出共通的資料成員、成員函數定義在父類別 (superclass) 中,使子類別 (subclass) 繼承 (inherit) 父類別。
有許多專書討論重構技術,由於本書篇幅有限,不可能塞太多東西,所以這裡僅是初步介紹而已,以下以 Encrypt 類別 (class) 為例,介紹如何替 Encrypt 類別重構。
重構通常需要搭配單元測試 (unit testing) ,確保軟體各部份的功能沒有改變。
我們利用數學公式設計一種演算法 (algorithm) ,利用這種演算法來建立密碼表,然而這個演算法並不是最理想的方式,因為這個公式建立的密碼表只有
5 × 10 = 50
依據 a 可能有 5 種數字, b 可能有 10 種數字,因此這個演算法最多就只能產生 50 種密碼表。
可是 26 個英文字母的排列會有
1 × 2 × 3 × ⋯⋯ × 25 × 26 = 4.0329 × 1026
由上可見有非常非常多種。
有沒有更理想的方式呢?有的,事實上我們可以直接攪亂字串 (string) 中字母的順序,這需要另一個攪亂順序的演算法,這在標準程式庫 (standard library) 中有,另外編碼 ToEncode() 跟 ToDecode() 兩個方法都按照 ASCII 原始數值來做計算也太不直覺了,事實上有更簡單直覺的處理方式。
以下看到重構後的 Encrypt 類別,先是標頭檔 (header file) Encrypt 類別的宣告
// 宣告 Encrypt 類別 class Encrypt { public: // 宣告建構函數 Encrypt(string s = ""); // 宣告 setter 成員函數 void set_code_array(string); // 宣告 getter 成員函數 string get_code_array(); // 宣告編碼、解碼的成員函數 string ToEncode(string); string ToDecode(string); private: // 密碼表字串 string code_array; // 字母表字串 string alphabet_array; };
完整程式碼可以參考「範例程式碼」的 encrypt_refactor.h 。
建構函數 (constructor) 新增了預設參數 (default argument)
// 宣告建構函數 Encrypt(string s = "");
注意建構函數的小括弧中增加 string 型態的參數 (parameter) s ,同時直接用指派運算子 (assignment operator) 將 s 設定為空字串 "" ,這種寫法是預設參數,也就是說重構版的建構函數需要一個參數 s ,但是這個 s 已經有預設值,因此如果建立 Encrypt 型態的物件 (object) 就可以不需要提供參數 s 。
加入參數主要目的是由參數建立密碼表,因為密碼表是字串, GUI 篇會介紹將密碼表字串儲存到純文字檔案中,以實現存檔功能,因此 Encrypt 類別的建構函數就需要直接以參數建立密碼表的功能。
底下資料成員新增了字母表字串
// 字母表字串 string alphabet_array;
這是因為重構版的編碼與解碼改用字串索引值對照的方式,因此就需要一個對照的字母表字串。
底下繼續看到重構後的建構函數
// Encrypt 的建構函數 Encrypt::Encrypt(string s) { // 建立字母表字串 alphabet_array = "abcdefghijklmnopqrstuvwxyz"; // 判斷是否由參數設定密碼表 if (s != "") { code_array = s; } else { // 初始化密碼表字串 code_array = "abcdefghijklmnopqrstuvwxyz"; // 獲取時鐘週期個數 unsigned int seed = system_clock::now().time_since_epoch().count(); // 攪亂字串中的元素順序 shuffle(code_array.begin(), code_array.end(), mt19937(seed)); } }
完整程式碼可以參考「範例程式碼」的 encrypt_refactor.cxx 。
這裡直接用字串常數建立字母表字串
// 建立字母表字串 alphabet_array = "abcdefghijklmnopqrstuvwxyz";
下面藉由判斷參數 s 是否為空字串,如果不是空字串,就將參數 s 設定為密碼表
// 判斷是否由參數設定密碼表 if (s != "") { code_array = s; }
反之參數 s 是空字串,代表需要建立新的密碼表,這裡採用的方式是單元 16 與單元 17 練習介紹的攪亂集合體型態裡面元素順序的方式
else { // 初始化密碼表字串 code_array = "abcdefghijklmnopqrstuvwxyz"; // 獲取時鐘週期個數 unsigned int seed = system_clock::now().time_since_epoch().count(); // 攪亂整數陣列中的元素順序 shuffle(code_array.begin(), code_array.end(), mt19937(seed)); }
底下繼續看到重構後的 ToEncode() 成員函數
// 進行編碼工作的成員函數 string Encrypt::ToEncode(string s) { // 由參數字串取得字元的暫存變數 char c; // 暫存編碼結果的字串 string r; // 利用迴圈走完參數字串的所有字元 for (char c: s) { // 判斷該字元是否為英文小寫字母,若是英文小寫字母就進行編碼轉換 if (islower(c)) { r += alphabet_array.at(code_array.find(c)); } else { r += c; } } // 結束回傳編碼過的字串 return r; }
編碼迴圈 (loop) 改成如下
// 利用迴圈走完參數字串的所有字元 for (char c: s) { // 判斷該字元是否為英文小寫字母,若是英文小寫字母就進行編碼轉換 if (islower(c)) { r += code_array.at(alphabet_array.find(c)); } else { r += c; } }
這裡 for 迴圈之後的小括弧改成宣告字元變數 c ,後面接冒號,冒號之後接參數字串 s
for (char c: s) {
字元變數 c 會依序取得參數字串 s 中的每一個字元,這是 C++11 新增 for 迴圈比較便捷的寫法,然後底下用 islower() 判斷該字元是否為英文小寫字母
if (islower(c)) {
islower() 在標準程式庫 cctype 中,這是個布林函數 (Boolean function) ,也就是如果參數是英文小寫字母就回傳 true ,否則回傳 false 。
如果當下處理的字元變數 c 是英文小寫字母,就進行編碼轉換
r += code_array.at(alphabet_array.find(c));
原理很簡單,先用字母表字串 alphabet_array 呼叫 find() ,以字元變數 c 當參數,這樣會回傳字元變數 c 在字母表字串中的索引值 (index) ,這個索引值也就是要轉換編碼字元在密碼表中的索引值,因此再以密碼表字串 code_array 呼叫 at() ,把取得的字母表索引值當參數就能取得編碼字元,接下來用 += 把編碼字元附加到暫存字串之後。
反之不是英文小寫字母,就直接用 += 把字元變數 c 附加到暫存字串之後
else { r += c; }
至於解碼的 ToDecode() 跟 ToEncode() 非常類似,差別在於 code_array 跟 alphabet_array 的位置對調而已。
測試執行的 encrypt_demo.cxx 換成 encrypt_refactor_demo.cxx ,主要差異只有引入得標頭檔換成 encrypt_refactor.h
// 引入 Encrypt 類別的標頭檔 #include "encrypt_refactor.h"
編譯時要將 encrypt_refactor.cxx 跟 encrypt_refactor_demo.cxx 放在一起編譯,同時加上參數 -std=c++11
GUI 篇與會使用重構版的 Encrypt 類別當作計算核心,在此之前,我們先來好好認識一下標準程式庫囉!
中英文術語對照 | |
---|---|
布林函數 | Boolean function |
指派運算子 | assignment operator |
類別 | class |
建構函數 | constructor |
資料成員 | data member |
預設參數 | default argument |
封裝 | encapsulation |
標頭檔 | header file |
識別字 | identifier |
索引值 | index |
繼承 | inherit |
迴圈 | loop |
成員函數 | member function |
物件 | object |
參數 | parameter |
重構 | refactoring |
軟體 | software |
程式原始碼 | source code |
子類別 | subclass |
父類別 | superclass |
標準程式庫 | standard library |
字串 | string |
型態 | type |
單元測試 | unit testing |
重點整理 |
---|
1. 重構是指重新整理程式碼,讓程式更易於維護。 |
2. 常見的重構技術包括整理資料成員、成員函數,挑出類別的共通特性定義父類別等等。 |
3. Encrypt 類別的重構主要分成兩大項目,其一為建立密碼表改用標準程式庫攪亂字串順序的程式,其二為編碼、解碼改用索引值對照的方式。 |
問題與討論 |
---|
1. 什麼是重構?為什麼要替開發好的程式進行重構? |
2. Encrypt 類別進行了哪些重構?為什麼要作這些重構? |
練習 |
---|
1. main() 函數的小括弧中可以宣告整數參數,如 int argc ,以及字元陣列的指標參數,如 char *argv[] ,這樣可以取得命令列參數,也就是執行程式後的依空格格開的文字會依序儲存在 argv[] 內, argc 則是數量,寫一個程式 exercise2201.cxx ,加入取得命令列參數的兩個參數,然後用迴圈印出命令列參數。 參考程式碼 |
2. 承上題,寫一個程式 exercise2202.cxx ,假設命令列參數輸入都是整數,然後利用迴圈計算命令列參數的輸入總和。 參考程式碼 |
3. 標準程式庫中 cstdlib 的 system() 可用於呼叫系統指令,這裡的指令須以字串代入,寫一個程式 exercise2203.cxx ,利用 system() 呼叫 "man ls" , man 在 Linux 系統中用來查詢指令手冊,因此 man ls 是查詢 ls 指令的用法。 參考程式碼 |
4. 承上題,標準程式庫中 unistd 的 chdir() 可用於切換工作路徑,路徑參數須以字串代入,寫一個程式 exercise2204.cxx ,先用 Linux 指令 mkdir 建立 demo 目錄,然後 chdir() 進入這個目錄,然後用 touch 建立 demo.txt 檔案,再用 ls 印出此目錄下的檔案列表。 參考程式碼 |
5. 承上題,寫一個程式 exercise2205.cxx ,繼續利用 Linux 指令 mkdir 在 demo 內建立 demo 目錄,然後用指令 cp 將 demo.txt 複製到新路徑。 參考程式碼 |
6. 承上題,寫一個程式 exercise2206.cxx ,改用 Linux 指令 mv 將 demo.txt 移動到新路徑,比較 cp 與 mv 的差異。 參考程式碼 |
7. 承上題,寫一個程式 exercise2207.cxx ,利用 Linux 指令 date 印出現在時間。 參考程式碼 |
8. 承上題,寫一個程式 exercise2208.cxx ,利用 Linux 指令 cal 印出現在的月曆。 參考程式碼 |
9. 承上題,寫一個程式 exercise2209.cxx ,利用 Linux 指令 more 顯示程式檔案內容。 參考程式碼 |
10. 承上題,寫一個程式 exercise2210.cxx ,利用 Linux 指令 pwd 顯示目前所在路徑。 參考程式碼 |
相關教學影片