第10章 結構型模式—組合模式
1. 組合模式(Composite Pattern)的定義
(1)將物件組合成樹型結構以表示“部分-整體”的層次結構。組合模式使得使用者對單個物件和組合物件的使用具有一致性。
(2)組合模式的結構和說明
①Component:抽象的元件物件,為組合中的物件宣告介面,讓客戶端可以通過這個介面來訪問和管理整個物件結構,可以在裡面為定義的功能提供預設的實現。
②Leaf:葉子節點物件,定義和實現葉子物件的行為,不再包含其他子節點物件。
③Composite:組合物件,通常會儲存子元件,定義包含子元件的那些元件的行為,並實現在元件介面中定義的與子元件有關的操作。(注意:Composite類也是繼承自Component
④Client:客戶端,通過元件介面來操作組合結構裡面的元件物件。
(3)典型的Composite組合物件的結構
(4)組合模式的思考
①組合模式的本質:統一葉子物件和組合物件。它把葉子物件和組合物件都當成Component物件,有機地統一了葉子物件和組合物件。
②組合模式的目的:讓客戶端不用區分操作的是組合物件還是葉子物件,而是以統一的方式來操作。
③組合模式中存在天然的遞迴,這裡所說的遞迴不是常說的“方法呼叫自己”的遞迴。而是指物件的遞迴組合。
2. 安全性和透明性——到底在Component介面還是Composite類中定義子元件操作方法?
(1)透明性的實現(建議使用的方式!)
①把子元件的操作定義在Component中,那麼客戶端只需要面對Component,而無須關心具體的元件型別,這種實現方法就是透明性的實現。(見前面的類圖結構)
②這種透明性是以安全性為代價的,因為Component中定義的一些方法,對於葉子物件來說沒有意義,如增加、刪除子元件物件。而客戶端不知道這些區別,因為對客戶來說是透明的,而客戶端呼叫這些方法是不安全的。
③以防止客戶端呼叫這些方法出現安全問題,一般在Component中為這些方法提供預設的實現,通常是丟擲一個異常
(2)安全性的實現
①把管理子元件的操作定義在Composite中,那麼客戶端在使用葉子物件時,因為沒有這個的操作可供呼叫,所以是安全的。
②但帶來的問題是客戶端在使用的時候,必須區分到底使用的是Composite還是Leaf的物件
(3)兩種實現方式的選擇
①對於組合模式而言,在安全性和透明性上,會更看重透明性,畢竟組合模式的功能就是要讓使用者對葉子物件和組合物件的使用具有一致性。
②對於安全性的實現,需要區分是組合物件還是葉子物件。有時需要進行強制型別轉換,但這本身就是不安全的,所以在Component中,一般會定義一個getComposite方法來判斷是葉子物件還是組合物件,這樣在使用前先判斷,再轉換才不會出現安全問題。
【程式設計實驗】Windows資源管理器
//結構型模式:組合模式
//場景:Windows資源管理
#include <iostream>
#include <string>
#include <list>
using namespace std;
//************************抽象元件類******************
class AbstractFile
{
protected:
string name; /*檔案或目錄名*/
public:
//顯示檔名或目錄名
virtual void display() {cout << name << endl;}
//判斷是檔案還是目錄
virtual bool isFolder() = 0;
//新增子目錄或檔案
virtual bool addChild(AbstractFile* file) = 0;
//刪除子目錄或檔案
virtual bool removeChild(AbstractFile* file) = 0;
//獲得子節點
virtual list<AbstractFile*>* getChildren() = 0;
};
//***********************具體元件角色*******************
class File : public AbstractFile
{
public:
File(string name){this->name = name;}
list<AbstractFile*>* getChildren()
{
//對於檔案(葉子節點),無子結點
//這裡可以丟擲異常
return NULL;
}
bool addChild(AbstractFile* file)
{
//葉子結點無子結點,可拋異常
return false;
}
bool removeChild(AbstractFile* file)
{
//葉子結點無子結點,可拋異常
return false;
}
bool isFolder() {return false;}
};
//*****************************組合元件物件**********************
class Folder : public AbstractFile
{
list<AbstractFile*> childList;
public:
Folder(string name){this->name = name;}
list<AbstractFile*>* getChildren()
{
return &childList;
}
bool addChild(AbstractFile* file)
{
childList.push_back(file);
return true;
}
bool removeChild(AbstractFile* file)
{
childList.remove(file);
return true;
}
bool isFolder(){ return true;}
};
void displayTree(AbstractFile* root,int deep = 0)
{
for(int i = 0; i< deep; i++)
{
cout << "--";
}
//顯示自身名稱
root->display();
//獲取子目錄
list<AbstractFile*>* childList = root->getChildren();
if(childList != NULL)
{
list<AbstractFile*>::iterator iter = childList->begin();
while (iter != childList->end())
{
if((*iter)->isFolder())
{
displayTree(*iter, deep + 1);
}
else
{
for(int i=0;i<=deep;i++)
cout << "--";
(*iter)->display();
}
++iter;
}
}
}
int main()
{
AbstractFile* rootFolder = new Folder("C:\\");
AbstractFile* compositeFolder = new Folder("Composite");
AbstractFile* windowsFolder = new Folder("Windows");
AbstractFile* file = new File("Test.cpp");
//組織成樹狀結構
rootFolder->addChild(compositeFolder);
rootFolder->addChild(windowsFolder);
compositeFolder->addChild(file);
//顯示根目錄及其子目錄(或檔案)
displayTree(rootFolder, 0);
delete rootFolder;
delete compositeFolder;
delete windowsFolder;
delete file;
return 0;
}
3. 父元件和環狀引用問題
3.1 父元件的引用:在子元件物件中儲存有父元件物件的引用
(1)使用場景:如刪除某個商品型別A,但如果它有子型別B,這時會涉及到子型別的處理,是連帶全部刪除,還是子型別上移一層,即成為B.setParent(A.parent);
(2)引用的定義:通常會在Component中定義對父元件的引用。組合物件和葉子物件都可以繼承這個引用。
(3)引用的維護:在Composite的實現中,當組合物件新增子元件物件時設定其父物件,刪除子元件物件時重新設定父元件的引用(可能被刪除元件的子元件物件涉及到上移一層問題)。
3.2 環狀引用
(1)概念:在物件結構中,某個物件包含的子物件,或子物件的子物件,或子物件的子物件的子物件……如此經過N層後,出現所包含的子物件中有這個物件本身,從而構成了環狀引用。比如,A包含B,B包含C,而C又包含A,就構成了環狀引用。
(2)組合模式一般是當用構建樹狀結構的,通常要避免環狀引用的出現(當然有些特殊需求,也需要環狀引用),否則很容易造引起死迴圈。
(3)檢測和處理環狀引用的方法:
①記錄下每個元件從根節點開始的路徑,在這條路徑上,某個物件出現兩次,就會構成環狀引用。
②先在Component中設定一個欄位,專門用來記錄本元件從根結點開始到Component本身的路徑。
③當Composite物件的新增子元件方法中,先檢測要新增的子元件是否出現在上面所說的路徑中,如果出現環狀引用,則丟擲異常。
(4)說明:
①上面的環路檢測方法很簡單,但沒考慮到如果刪除了某個路徑上的某個元件物件,那麼所有該元件物件的子元件物件所記錄的路徑都要更新。
②可以考慮動態計算路徑的方式,每次新增一個元件的時候,動態的遞迴尋找父元件,然後父元件再找父元件,直到根元件。這樣就能避免某個元件被刪除後,路徑發生了變化而需修改所有相關路徑記錄的情況。
4. 使用時的注意事項
(1)子元件列表(list<Component*>)應放在Component介面還是Composite類中?
①大多數情況下,一個Composite物件會持有子節點集合。因為葉子節點是沒有子元件的。
②但是也可以將子元件列表放在Component介面中定義(aps.net的容器類,就是這樣定義的),這可以簡單葉子結點的操作。但對於葉子節點來說,會導致空間的浪費,因為葉子節點本身不需要子節點,因此只有當組合結構中的葉子物件數目較少的時候,才使用這種方法。
(2)最大化Component定義
Component中的方法是兩種物件對外方法之和,換句話說,有點大雜燴的意思,元件裡面既有葉子物件需要的方法,也有組合物件需要的方法。這會造成“介面汙染”,也與類的設計原則相沖突。
(3)子元件排序
①當需要按照一定的順序來使用子元件物件時(如分析語法樹時),設計時需要把元件物件的索引考慮進去,並設計對子節點的訪問和管理介面。
②通常會結合Iterator模式來實現按照順序來訪問元件物件。
5. 組合模式的優缺點
(1)優點:
①高層模式呼叫簡單,因為統一了葉子物件和組合物件的操作。
②節點自由增加,只要找到它的父節點,就很容易擴充套件,符合開閉原則。
③可以組合成複雜的物件,從而構成一個統一的組合物件的類層次結構
(2)缺點
很難限制組合中的元件型別,需要檢測元件型別時,不能依靠編譯期的型別來約束,必須在執行期間動態檢測。
6.組合模式的使用場景
(1)通常,組合模式會組合出樹型結構來,這意味著所有可以使用物件樹來描述或操作的功能,都可以考慮使用組合模式,如UI介面設計中的容器物件、讀取XML或對語句進行語法分析、OA系統中組織結構的處理、作業系統的資源管理器等。
(2)如果想表示物件的部分——整體層次結構,把整體和部分的操作統一起來,使得層次結構實現更簡單,從外部來使用這個層次結構也容易。
(3)如果希望統一地使用組合結構中的所有物件,可以選用組合模式。
【程式設計實驗】繪製基本圖形和複合圖形物件
//結構型模式:組合模式(安全型)
//場景:繪圖(基本圖形和複合圖形)
#include <iostream>
#include <string>
#include <list>
using namespace std;
//************************抽象元件類******************
class Graphics
{
protected:
string name; /*名稱*/
public:
virtual void draw() = 0; //繪圖
};
//***********************具體元件角色*******************
//線
class Line : public Graphics
{
public:
Line(string name){this->name = name;}
void draw()
{
cout << "draw a " << name << endl;
}
};
//圓
class Circle : public Graphics
{
public:
Circle(string name){this->name = name;}
void draw()
{
cout << "draw a " << name << endl;
}
};
//矩形
class Rectangle : public Graphics
{
public:
Rectangle(string name){this->name = name;}
void draw()
{
cout << "draw a " << name << endl;
}
};
//*****************************組合元件物件**********************
class Picture : public Graphics
{
list<Graphics*> childList;
public:
Picture(string name){this->name = name;}
list<Graphics*>* getChildren()
{
return &childList;
}
bool addChild(Graphics* file)
{
childList.push_back(file);
return true;
}
bool removeChild(Graphics* file)
{
childList.remove(file);
return true;
}
void draw()
{
cout << "draw Composite Object: " << name << endl;
list<Graphics*>::iterator iter = childList.begin();
while(iter != childList.end())
{
(*iter)->draw();
++iter;
}
}
};
int main()
{
Picture* root = new Picture("Root");
Graphics* line = new Line("Line");
Graphics* circle = new Circle("Circle");
Graphics* rectangle = new Rectangle("Rectangle");
root->addChild(line);
root->addChild(circle);
root->addChild(rectangle);
root->draw();
delete line;
delete circle;
delete rectangle;
delete root;
return 0;
}