C++ 實習測試: 檔案內容統計 - Strategy vs. Template Method |
時間: 120分鐘 (11:20 上傳時間截止, 不管你做到什麼地方, 請一定要上傳) |
目標程式:
這個程式基本的功能就是讀取一個文字檔案,分析裡面不是標點符號的文字滿足某些特性的數量,例如:「開頭是字母 i 的英文單字有幾個」、「長度是 7 的英文單字有幾個」、「while 出現幾次」等等 程式輸出如下 Analyzing the file Document.cpp ... # of words start with 'i' is 41 # of words of length 7 is 10 # of "while" is 9 這個程式其實很簡單, 底下先給你主要的程式碼 考試要求一請你開啟一個新的方案 lexAnalysis, 讓 Visual Studio 幫你建立方案的目錄, 然後請把下面的程式碼拷貝到不同的檔案裡, 測試執行, 應該可以得到前兩列(黑字)的結果 #include <iostream> #include <fstream> #include <string> #include <vector> #include <string.h> // strtok using namespace std; class Document { public: Document(string filename); int wordsStartWith(char ch); private: string m_filename; }; Document::Document(string filename): m_filename(filename) { } int Document::wordsStartWith(char ch) { int wordCount=0; string line; char *ptr; ifstream inf(m_filename.c_str()); while (getline(inf, line)) { vector<char> buf(line.c_str(), line.c_str()+line.size()+1); ptr = strtok(&buf[0]," ,.-()[]<>#;{}=+*/&:\"'\\"); while (ptr != NULL) { if (ptr[0] == ch) wordCount++; ptr = strtok(NULL, " ,.-()[]<>#;{}=+*/&:\"'\\"); } } return wordCount; } int main() { Document doc("../lexAnalysis/Document.cpp"); cout << "Analyzing the file Document.cpp ..." << endl; cout << " # of words start with 'i' is " << doc.wordsStartWith('i') << endl; // cout << " # of words of length 7 is " << doc.wordsOfLength(7) << endl; // cout << " # of \"while\" is " << doc.wordsEqual("while") << endl; return 0; } 上面 main() 裡面開啟的檔案名稱你可以自己修改 考試要求二請在 Document 類別裡增加兩個介面函式,使得上面 main() 裡面註解掉的兩列可以順利執行 int wordsOfLength(int length); int wordsEqual(char *target); 函式實作請直接修改 int Document::wordsStartWith(char ch) 函式 需要改的地方應該很少! |
上面這個程式裡的三個函式很明顯有很大的部份是重複的,想像一下,如果像這樣的功能一直不斷地增加,程式裡面就有一大堆重複的程式碼,這一定是不好的設計,萬一需要修改一點點,例如增加一個 標點符號 '%' 作為英文單字的分隔字元,麻煩就大了! 每一個運用 strtok 工具的地方都要修改, 接下來到了比較重要的部份了... 考試要求三 - template method pattern請在方案中加入一個新的專案 lexAnalysis1 前面寫的三個功能中,應該可以發現就只有測試的那一小段程式是不同的,我們可以把它改成 if (passesTest(ptr, param)) wordCount++; 其他的部份就是一模一樣的了,我們可以運用實習裡做過的 Template Method 設計樣板來修改, 把才的 int Document::wordsStartWith(char ch) 函式改成父類別裡面的 template method 如下: int Document::countWords(void *param) { int wordCount=0; string line; char *ptr; ifstream inf(m_filename.c_str()); while (getline(inf, line)) { vector<char> buf(line.c_str(), line.c_str()+line.size()+1); ptr = strtok(&buf[0]," ,.-()[]<>#;{}=+*/&:\"'\\"); while (ptr != NULL) { if (passesTest(ptr, param)) wordCount++; ptr = strtok(NULL, " ,.-()[]<>#;{}=+*/&:\"'\\"); } } return wordCount; } 把這個 bool passesTest(char *word, void *param) 宣告為 Document 類別私有的純粹虛擬函式,第二個參數設計成 void * 型態是因為三種不同的測試所需要的參數型態是不一樣的,但是父類別裡面的虛擬函式的參數只能有一種,只好定義為 void * 這種任意的記憶體位址型態 (這當然不是好的設計,但是這是C裡面最基本的泛用型態) 然後我們可以繼承 Document 類別成為 DocWordsStartWith 類別,覆蓋 passesTest(char *word, void *param) 成為 bool DocWordsStartWith::passesTest(char *word, void *param) { return word[0] == *(char*)param; } 另外因為現在每個分析的類別是不一樣的了, main() 函式裡需要比較大的修改 int main() { cout << "Analyzing the file lexAnalysis1/Document.cpp ..." << endl; DocWordsStartWith doc1("../lexAnalysis1/Document.cpp"); char ch = 'i'; cout << " # of words start with 'i' is " << doc1.countWords(&ch) << endl; DocWordsOfLength doc2("../lexAnalysis1/Document.cpp"); int len = 7; cout << " # of words of length 7 is " << doc2.countWords(&len) << endl; DocWordsEqual doc3("../lexAnalysis1/Document.cpp"); char *target="while"; cout << " # of \"while\" is " << doc3.countWords(target) << endl; system("pause"); return 0; } 請在專案 lexAnalysis1 中完成上面的兩個類別 Document 和 DocWordsStartWith(提醒你一下,如果你要由專案 lexAnalysis 中拷貝 Document.h Document.cpp 還有 main.cpp 到新的專案 lexAnalysis1裡的話,需要在作業系統裡面直接拷貝檔案,然後在方案總管裡面加入現有項目),請測試執行,然後再設計另外兩個衍生類別 DocWordsOfLength 以及 DocWordsEqual,請測試執行 這個設計的類別圖如下 |
上面這個設計的缺點:除了需要使用 void * 設計參數之外,上面這種用 template method 設計的解法中,你應該可以想像如果功能一直不斷地增加,main() 裡面增加的程式碼似乎有點多,尤其是對於同一個檔案來說需要替每一個測試產生一個新的檔案物件,這很奇怪,因為檔案是相同的,直覺上應該一個物件就夠了 考試要求四 - strategy 設計樣板 & functor請在方案中加入一個新的專案 lexAnalysis2 讓我們更改一下設計,前面使用 template method 的設計裡,測試的程式碼是 if (passesTest(ptr, param)) wordCount++; passesTest(char*, void*) 只是一個函式,想要這個函式有不同的表現又要重用 Document::countWords() 似乎就需要繼承了! 可是這有個盲點,passesTest() 其實可以是一個代表不同測試的物件,我們只需要定義一個抽象的 Test 介面,在裡面定義純粹虛擬函式 bool operator()(char *) 就可以使 Document::countWords(Test &test) 接收任何有實作 Test 介面的物件如下: int Document::countWords(Test &test) { int wordCount=0; string line; char *ptr; ifstream inf(m_filename.c_str()); while (getline(inf, line)) { vector<char> buf(line.c_str(), line.c_str()+line.size()+1); ptr = strtok(&buf[0]," ,.-()[]<>#;{}=+*/&:\"'\\"); while (ptr != NULL) { if (test(ptr)) wordCount++; ptr = strtok(NULL, " ,.-()[]<>#;{}=+*/&:\"'\\"); } } return wordCount; } 其中 test(ptr) 是呼叫 function call operator。因為 test 是一個物件,所以這個物件初始化的時候可以紀錄需要的參數,不同的測試也可以用不同型態的參數,定義 Test 抽象類別的時候並部會把衍生類別的建構元的參數定死。 有了這樣的設計以後,main() 函式可以寫成 int main() { Document doc("../lexAnalysis2/Document.cpp"); cout << "Analyzing the file lexAnalysis2/Document.cpp ..." << endl; TestWordsStartWith test1('i'); cout << " # of words start with 'i' is " << doc.countWords(test1) << endl; TestWordsOfLength test2(7); cout << " # of words of length 7 is " << doc.countWords(test2) << endl; TestWordsEqual test3("while"); cout << " # of \"while\" is " << doc.countWords(test3) << endl; system("pause"); return 0; } 上面這種設計裡可以看到我們一次解決所有的問題了,請完成
這是一個 Strategy 設計樣板, 這個設計的類別圖如下,設計的精髓在於使用抽象類別 (界面) 來隔離客戶端 (Document 類別) 以及真正的測試實作類別 (TestWordsStartWith, TestWordsOfLength, TestWordsEqual, ...),後續在增加新的測試時,Document 類別完全不需要修改而可以順利擁有新的功能,在語言層次上運用 C++ 動態多型的機制來讓 Document 類別在設計時以完全一樣的概念來呼叫不同衍生類別物件提供的虛擬函式;由高階設計層面上來看,抽象類別 Test 把界面固定下來,使得 Document 類別可以用一致的概念來操作不同的子類別物件。如果 main 函式裡希望不要看到那多的新類別,可以考慮再加上 Factory Method 設計樣板。 完成後請測試 |
將所完成的 project (只需保留 .cpp, .h, .sln 以及 .vcxproj 檔案即可; 刪除掉 .suo, .sdf, .filters, .users, debug\ 資料匣, 以及 ipch\ 資料匣下的所有內容) 以 zip/rar/7zip 程式將整個資料匣壓縮起來, 在「考試作業」繳交區選擇 Labtest4上傳 |
尾聲 |
回
C++ 物件導向程式設計課程 首頁 製作日期: 06/12/2016 by 丁培毅 (Pei-yih Ting) E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615 海洋大學 電機資訊學院 資訊工程學系 Lagoon |