這裡延續 作業 3a, 但是並沒有打算增加程式的功能, (當然如果你覺得你前面繳交的作業功能不全的, 還是可以新增), 我們要修改作業 3a 中相同功能程式碼重複多次的問題, 我們希望用上課時談到實作 double dispatch 功能的 Vistor 設計樣式來配合 Composite 設計樣式工作, 以便得到一個比較好的程式架構。這個作業要寫的程式不多, 但是你應該由作業 3a 的程式開始修改, 為了不把一個已經測試完成的程式改壞掉, 你還是要花相當的時間, 耐心地修改相關的程式, 同時也應該要設計好單元測試的程式碼, 一邊修改一邊測試, 由於程式的外觀不會有明顯改變, 所以你也特別必須用心體會, 才能夠了解這些修改的真正意義, 以後遇見類似的問題才能夠以比較好的物件導向設計來應付。
做了這個作業以後, 應該會有進一層的體會, 設計程式不只著重在完成一些指定的功能而已, 不見得只是硬湊出一些功能而已, 是可以很有巧思的, 就像是蓋房子, 可以只蓋出四四方方、平平淡淡、足以遮風避雨的東西, 也可以設計成美觀有創意有價值、可重複欣賞與應用的真正工藝品。
範例執行程式 FileDirectory_v1.exe, 資料檔案 prog3b.in, 下載後可以直接透過鍵盤交談式執行,也可以用
FileDirectory_v1 < prog3b.in
直接執行資料檔案中的命令, 程式結束後會將所建立的資料匣及檔案架構存檔, 下次執行時會自動讀入, 不需要重新由鍵盤輸入。由使用界面和功能上你應該看不到和上一次作業有什麼差別, 這也是這個測試資料檔案的主要目標: 保證具有和上一次作業完全一樣的功能。
例如在計算檔案/資料匣大小的時候, 你需要完成下列這三件事情:
int Folder::getSize() { vector<Entry *>::iterator iter; int sum=0; for (iter=m_entries.begin(); iter<m_entries.end(); iter++) sum += (*iter)->getSize(); return sum; }這段程式假設每一個子資料匣和檔案都會計算它的大小, 然後以多型指標 (*iter) 來委託容器中每一個物件計算它自己的大小。
int File::getSize() { return m_content.size(); }
在 Composite 設計樣式所描述的異質的樹狀資料架構中, 我們要加入一些功能時, 會依照不同的類別而有不同的作法, 我們把這些功能直接加進去時會使得 Composite 設計樣式不但要管理異質的樹狀資料架構, 還要同心協力來完成指定的功能, 於是 Composite 設計樣式中的各個成員變得越來越多也越來越複雜。
用 Visitor 設計樣式和 Composite 設計樣式合作時, 最主要就是希望讓 Composite 設計樣式專心管理異質的樹狀資料架構, 其它的演算法和不斷增加的新功能讓 Visitor 設計樣式來處理, 增加功能時 Composite 設計樣式的各個類別根本不需要修改。
用一個比喻來說, Visitor 和 Composite 設計樣式的合作, 就好像是一個專業的廣告公司 (Visitor) 和它的客戶 (Composite), 如果每一個客戶都要負責自己公司的廣告設計, 用很多的人力卻不一定能把工作做好, 每一個客戶為自己做廣告設計, 也會使得自己公司內人員的業務變得比較複雜, 比較容易出錯, 這時候如果出錯的話其實不是個別人員的問題, 常常是公司管理以及組織架構的問題。 這個時候其實需要一個專業的廣告公司, 在這個廣告公司裡當然比較會是任務導向的, 針對每一種客戶都有一個專門的設計小組 (例如 Visitor 類別中所實作的 visit(File *) 界面就是一個為了處理 File 這個類別的工作小組, visit(Folder *) 界面就是一個為了處理 Folder 這個類別的工作小組), Acceptor 所定義的 accept(Visitor &) 界面最主要的功能就是當專業的廣告公司人員來到客戶公司內部時, 必須有適當的接待人員來將公司的需求和廣告公司的設計人員溝通, 以這個客戶的屬性來挑選廣告公司內專門的設計小組來為其服務。
Visitor 設計樣式的骨幹如下圖中紅色部份所示:
class Acceptor { public: virtual void accept(Visitor &visitor)=0; };要求 Composite 設計樣式中的 Entry 類別繼承它, 也就是要求每一個底層的類別包括 File 和 Folder 都要實作這個界面函式, 不過這個界面函式不論在 File 類別內或是在 Folder 類別內的實作都一樣也都很簡單, 如下所示:
void File::accept(Visitor &visitor) { visitor.visit(this); } void Folder::accept(Visitor &visitor) { visitor.visit(this); }這兩個函式看起來內容完全一樣, 但是其實因為 this 指標所指到物件的型態不一樣, 使它可以完成適當的功能: 它最主要的目的是根據不同物件的類別來決定用 Visitor 類別物件中相對應的 visit() 成員函式來處理實際的功能。
class Visitor { public: virtual void visit(File *file)=0; virtual void visit(Folder *folder)=0; };由於所要拜訪的 Composite 設計樣式中只有兩種實際的類別, 所以只需要定義兩個不同的純虛擬函式。
class SizeVisitor : public Visitor { public: SizeVisitor():m_totalSize(0) {} void visit(File *file); void visit(Folder *folder); int getTotalSize(); private: int m_totalSize; }; void SizeVisitor::visit(File *file) { m_totalSize += file->getSize(); } void SizeVisitor::visit(Folder *folder) { vector<Entry *>::iterator iter; for (iter=folder->begin(); iter!=folder->end(); iter++) (*iter)->accept(*this); } int SizeVisitor::getTotalSize() { return m_totalSize; }其中需要使用 Folder 類別特別定義的 begin() 和 end() 兩個界面,
vector<Entry *>::iterator Folder::begin() { return m_entries.begin(); } vector<Entry *>::iterator Folder::end() { return m_entries.end(); }假設 entry 為你打算計算大小的物件指標, 使用這個 SizeVisitor 的方法如下:
SizeVisitor sv; entry->accept(sv); cout << "The size is " << sv.getSize() << " bytes." << endl;
在物件導向的軟體發展流程中調整軟體的架構我們叫做軟體重構 (refactoring), 這是常常需要做的, 有的時候是為了程式的效率, 有的時候是為了和其它程式發展者建立一致的溝通界面, 有的時候是為了程式的彈性和擴充性: 我們在設計軟體的時候很難設計出一個萬能的架構, 在新的需求出現的時候常常需要更改舊的程式架構來應付。
回
C++ 物件導向程式設計課程
首頁
製作日期: 05/01/2024 by 丁培毅 (Pei-yih Ting) E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615 海洋大學 電機資訊學院 資訊工程學系 Lagoon |