對於樹形結構,當容器對象(如文件夾)的某一個方法被調用時,將遍歷整個樹形結構,尋找也包含這個方法的成員對象(可以是容器對象,也可以是葉子對象,如子文件夾和文件)並調用執行。(遞歸調用)
由於容器對象和葉子對象在功能上的區別,在使用這些對象的客戶端代碼中必須有區別地對待容器對象和葉子對象,而實際上大多數情況下客戶端希望一致地處理它們,因爲對於這些對象的區別對待將會使得程序非常複雜。
組合模式描述瞭如何將容器對象和葉子對象進行遞歸組合,使得用戶在使用時無須對它們進行區分,可以一致地對待容器對象和葉子對象,這就是組合模式的模式動機。
一、模式定義
組合模式(Composite Pattern):組合多個對象形成樹形結構以表示“整體-部分”的結構層次。組合模式對單個對象(即葉子對象)和組合對象(即容器對象)的使用具有一致性。
組合模式又可以稱爲“整體-部分”(Part-Whole)模式,屬於對象的結構模式,它將對象組織到樹結構中,可以用來描述整體與部分的關係。
組合模式的分類
1) 安全組合模式:將管理子元素的方法定義在Composite類中。
2) 透明組合模式:將管理子元素的方法定義在Component接口中,這樣Leaf類就需要對這些方法空實現。
適用性
• 需要表示一個對象整體或部分層次,在具有整體和部分的層次結構中,希望通過一種方式忽略整體與部分的差異,可以一致地對待它們。
• 讓客戶能夠忽略不同對象層次的變化,客戶端可以針對抽象構件編程,無須關心對象層次結構的細節。
• 對象的結構是動態的並且複雜程度不一樣,但客戶需要一致地處理它們。
模式優點:
• 可以清楚地定義分層次的複雜對象,表示對象的全部或部分層次,使得增加新構件也更容易。
• 客戶端調用簡單,客戶端可以一致的使用組合結構或其中單個對象。
• 定義了包含葉子對象和容器對象的類層次結構,葉子對象可以被組合成更復雜的容器對象,而這個容器對象又可以被組合,這樣不斷遞歸下去,可以形成複雜的樹形結構。
• 更容易在組合體內加入對象構件,客戶端不必因爲加入了新的對象構件而更改原有代碼。
缺點
• 使設計變得更加抽象,對象的業務規則如果很複雜,則實現組合模式具有很大挑戰性,而且不是所有的方法都與葉子對象子類都有關聯。
• 增加新構件時可能會產生一些問題,很難對容器中的構件類型進行限制。
二、ULM圖
下圖是透明組合模式的ulm圖
組合模式包含如下角色:
• Component: 抽象構件
• Leaf: 葉子構件
• Composite: 容器構件
• Client: 客戶類
三、實例
3.1 公司:
一個集團公司,它有一個母公司,下設很多家子公司。不管是母公司還是子公司,都有各自直屬的財務部、人力資源部、銷售部等。對於母公司來說,不論是子公司,還是直屬的財務部、人力資源部,都是它的部門。整個公司的部門拓撲圖就是一個樹形結構。
下面給出組合模式的UML圖。從圖中可以看到,FinanceDepartment、HRDepartment兩個類作爲葉結點,因此沒有定義添加函數。而ConcreteCompany類可以作爲中間結點,所以可以有添加函數。那麼怎麼添加呢?這個類中定義了一個鏈表,用來放添加的元素。
實現代碼如下:
#include <iostream>
#include <list>
#include <memory>
#include <utility>
#include <cstddef>
using namespace std;
class Company
{
public:
Company(string name) { m_name = name; }
virtual ~Company() {}
virtual void Add(std::unique_ptr<Company>) {}//這裏默認實現爲空函數,leaf節點中不用實現爲空了
virtual void Show(int depth) {} // 這裏默認實現爲空函數,leaf節點中不用實現爲空了
protected:
string m_name;
};
//具體公司
class ConcreteCompany : public Company
{
public:
ConcreteCompany(string name) : Company(name) {}
virtual ~ConcreteCompany()
{
for (auto& company : m_listCompany)
{
company.reset(nullptr);
}
}
void Add(std::unique_ptr<Company> pCom) { m_listCompany.push_back(std::move(pCom)); } //位於樹的中間,可以增加子樹
void Show(int depth)
{
for (int i = 0; i < depth; i++)
cout << "-";
cout << m_name << endl;
auto iter = m_listCompany.begin();
for (; iter != m_listCompany.end(); iter++) //顯示下層結點
(*iter)->Show(depth + 2);
}
private:
list<std::unique_ptr<Company>> m_listCompany;
};
//具體的部門,財務部
class FinanceDepartment : public Company
{
public:
FinanceDepartment(string name) :Company(name) {}
virtual ~FinanceDepartment() {}
virtual void Show(int depth) //只需顯示,無限添加函數,因爲已是葉結點
{
for (int i = 0; i < depth; i++)
cout << "-";
cout << m_name << endl;
}
};
//具體的部門,人力資源部
class HRDepartment :public Company
{
public:
HRDepartment(string name) :Company(name) {}
virtual ~HRDepartment() {}
virtual void Show(int depth) //只需顯示,無限添加函數,因爲已是葉結點
{
for (int i = 0; i < depth; i++)
cout << "-";
cout << m_name << endl;
}
};
int main()
{
auto root = std::make_unique<ConcreteCompany>("總公司");
auto leaf1 = std::make_unique < FinanceDepartment>("總公司財務部");
auto leaf2 = std::make_unique < HRDepartment>("總公司人力資源部");
root->Add(std::move(leaf1));
root->Add(std::move(leaf2));
//分公司
auto mid1 = std::make_unique < ConcreteCompany>("杭州分公司");
auto leaf3 = std::make_unique < FinanceDepartment>("杭州分公司財務部");
auto leaf4 = std::make_unique < HRDepartment>("杭州分公司人力資源部");
mid1->Add(std::move(leaf3));
mid1->Add(std::move(leaf4));
root->Add(std::move(mid1));
//分公司
auto mid2 = std::make_unique < ConcreteCompany>("上海分公司");
auto leaf5 = std::make_unique < FinanceDepartment>("上海分公司財務部");
auto leaf6 = std::make_unique < HRDepartment>("上海分公司人力資源部");
mid2->Add(std::move(leaf5));
mid2->Add(std::move(leaf6));
root->Add(std::move(mid2));
root->Show(0);
return 0;
}
運行結果如下:
3.2 水果盤
在水果盤(Plate)中有一些水果,如蘋果(Apple)、香蕉(Banana)、梨子(Pear),當然大水果盤中還可以有小水果盤,現需要對盤中的水果進行遍歷(吃),當然如果對一個水果盤執行“吃”方法,實際上就是吃其中的水果。使用組合模式模擬該場景 。
代碼實現如下:
#include <iostream>
#include <list>
#include <memory>
#include <utility>
#include <cstddef>
//抽象構建類MyElement
class MyElement
{
public:
virtual void eat() = 0;
};
//葉子構件類Apple
class Apple : public MyElement
{
public:
void eat() {
std::cout << "吃蘋果!" << std::endl;
}
};
//葉子構件類Banana
class Banana : public MyElement {
public:
void eat() {
std::cout << "吃香蕉!" << std::endl;
}
};
//葉子構件類Pear
class Pear : public MyElement
{
public:
void eat() {
std::cout << "吃梨子!" << std::endl;
}
};
//容器構建類Plate
class Plate : public MyElement {
public:
void add(std::unique_ptr<MyElement> element)
{
ls.push_back(std::move(element));
}
void remove(std::unique_ptr<MyElement> element)
{
ls.remove(std::move(element));
}
void eat()
{
for (auto& i : ls)
{
i->eat();
}
}
~Plate()
{
for (auto& i : ls)
{
i.reset(nullptr);
}
}
private:
std::list<std::unique_ptr<MyElement>> ls;
};
//client
int main(void)
{
auto obj1 = std::make_unique<Apple>();
auto obj2 = std::make_unique < Pear>();
auto plate1 = std::make_unique < Plate>();
plate1->add(std::move(obj1));
plate1->add(std::move(obj2));
auto obj3 = std::make_unique < Banana>();
auto obj4 = std::make_unique < Banana>();
auto plate2 = std::make_unique < Plate>();
plate2->add(std::move(obj3));
plate2->add(std::move(obj4));
auto obj5 = std::make_unique < Apple>();
auto plate3 = std::make_unique < Plate>();
plate3->add(std::move(plate1));
plate3->add(std::move(plate2));
plate3->add(std::move(obj5));
plate3->eat();
return 0;
}
運行結果如下:
其他應用場景:
(1) XML文檔解析
(2) 操作系統中的目錄結構是一個樹形結構,因此在對文件和文件夾進行操作時可以應用組合模式,例如殺毒軟件在查毒或殺毒時,既可以針對一個具體文件,也可以針對一個目錄。如果是對目錄查毒或殺毒,將遞歸處理目錄中的每一個子目錄和文件。