C++ 入門指南 4.01

單元 22 - 重構

-unit22-

重構 (refactoring) 是指不改變軟體 (software) 外部行為下,對程式原始碼 (source code) 進行整理、修改,不外是希望提升原始碼的可讀性及更易於維護

Source code → Refactoring
Readability
Maintainability
Extensibility

簡單說,只要感覺原始碼有些不對勁就該重構,尤其在一個大的開發團隊中,每個小組負責的部份可能都不同,因而常常會發生識別字 (identifier) 命名不一致或是太多類別有共通特性的情況。所以需要適時的重構原始程式碼,這也是軟體開發過程中一項重要的工作。

常見的重構技術如下

有許多專書討論重構技術,由於本書篇幅有限,不可能塞太多東西,所以這裡僅是初步介紹而已,以下以 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_arrayalphabet_array 的位置對調而已。

測試執行的 encrypt_demo.cxx 換成 encrypt_refactor_demo.cxx ,主要差異只有引入得標頭檔換成 encrypt_refactor.h

// 引入 Encrypt 類別的標頭檔
#include "encrypt_refactor.h"

編譯時要將 encrypt_refactor.cxxencrypt_refactor_demo.cxx 放在一起編譯,同時加上參數 -std=c++11

-encrypt_refactor-

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. 標準程式庫中 cstdlibsystem() 可用於呼叫系統指令,這裡的指令須以字串代入,寫一個程式 exercise2203.cxx ,利用 system() 呼叫 "man ls"man 在 Linux 系統中用來查詢指令手冊,因此 man ls 是查詢 ls 指令的用法。 參考程式碼
4. 承上題,標準程式庫中 unistdchdir() 可用於切換工作路徑,路徑參數須以字串代入,寫一個程式 exercise2204.cxx ,先用 Linux 指令 mkdir 建立 demo 目錄,然後 chdir() 進入這個目錄,然後用 touch 建立 demo.txt 檔案,再用 ls 印出此目錄下的檔案列表。 參考程式碼
5. 承上題,寫一個程式 exercise2205.cxx ,繼續利用 Linux 指令 mkdirdemo 內建立 demo 目錄,然後用指令 cpdemo.txt 複製到新路徑。 參考程式碼
6. 承上題,寫一個程式 exercise2206.cxx ,改用 Linux 指令 mvdemo.txt 移動到新路徑,比較 cpmv 的差異。 參考程式碼
7. 承上題,寫一個程式 exercise2207.cxx ,利用 Linux 指令 date 印出現在時間。 參考程式碼
8. 承上題,寫一個程式 exercise2208.cxx ,利用 Linux 指令 cal 印出現在的月曆。 參考程式碼
9. 承上題,寫一個程式 exercise2209.cxx ,利用 Linux 指令 more 顯示程式檔案內容。 參考程式碼
10. 承上題,寫一個程式 exercise2210.cxx ,利用 Linux 指令 pwd 顯示目前所在路徑。 參考程式碼

相關教學影片

上一頁 單元 21 - 前置處理
回 C++ 入門指南 4.01 目錄
下一頁 單元 23 - 認識標準程式庫
回 C++ 教材
回程式語言教材首頁