组合模式 - 草帽大船团


配套视频课程已更新完毕,大家可通过以下两种方式观看视频讲解:

关注公众号:爱编程的大丙,或者进入大丙课堂学习。


1. 好大一棵树

路飞在德雷斯罗萨打败多弗朗明哥之后,一些被路飞解救的海贼团自愿加入路飞麾下,自此组成了草帽大船团,旗下有7为船长,分别是:

  1. 俊美海贼团75人
  2. 巴托俱乐部56人
  3. 八宝水军1000人
  4. 艾迪欧海贼团4人
  5. 咚塔塔海贼团200人
  6. 巨兵海贼团5人
  7. 约塔玛利亚大船团4300人

小弟数量总计5640人。

对于草帽大船团的结构组成,很像一棵树:路飞是这棵树的根节点,旗下的七个船长是路飞的子节点。在这七个船长的旗下可能还有若干个船长。。。

像草帽大船团这样,能将多个对象组成一个树状结构,用以描述部分—整体的层次关系,使得用户对单个对象和组合对象的使用具有一致性,这样的结构性设计模式叫做组合模式。

现实生活中能够和组合模式对应的场景也有很多,下面举例说明:

  1. Linux 的树状目录结构
  2. 国家的行政区划分(省级、地级、县级、乡级)
  3. 解放军编制(军、师、旅、团、营、连、排、班)
  4. 公司的组织结构(树状)

2. 大决战

在海贼中,大家都预测路飞的对手应该是同为四皇的黑胡子,黑胡子手下也有很多海贼船,双方一旦开战,必定死伤无数,最后的赢家就可以得到罗杰所留下的大秘宝ONE PIECE,并成为新的海贼王。

为了让路飞成为海贼王,我决定使用组合模式为路飞写一个管理草帽大船团的程序,其对应的主要操作是这样的:扩充船员战斗减员显示各船队信息加入战斗等。

2.1 团队管理

对于组合模式来说,操作这个集合中的任意一个节点的方式都是相同的,所以必须要先定义出单个节点的抽象,在这个抽象类中定义出节点的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 抽象节点
class AbstractTeam
{
public:
AbstractTeam(string name) :m_name(name) {}
// 设置父节点
void setParent(AbstractTeam* node)
{
m_parent = node;
}
AbstractTeam* getParent()
{
return m_parent;
}
string getName()
{
return m_name;
}
virtual bool hasChild()
{
return false;
}
virtual void add(AbstractTeam* node) {}
virtual void remove(AbstractTeam* node) {}
virtual void fight() = 0;
virtual void display() = 0;
virtual ~AbstractTeam() {}
protected:
string m_name;
AbstractTeam* m_parent = nullptr;
};

草帽大船团中有若干个番队,这个抽象类对应的就是以船为单位的一个团队(一艘船就是一个节点),它内部定义了如下方法:

  1. 设置和获得当前船队的名字
    • 设置名字:构造函数
    • 获得名字:getName()
  2. 设置和得到当前船队节点的父节点
    • 设置父节点:setParent(AbstractTeam* node)
    • 得到父节点:getParent()
  3. 给当前番队添加一个子船队节点:add(AbstractTeam* node)
  4. 跟当前番队删除一个子船队节点:remove(AbstractTeam* node)
  5. 当前番队和敌人战斗:fight()
  6. 显示当前番队的信息:display()

2.2 叶子节点

草帽大船团是一种组合模式,也就是一种树状结构,在最末端的节点就没有子节点了,这种节点可以将其称之为叶子节点。叶子节点也是一个船队,所以它肯定是需要继承抽象节点类的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 叶子节点的小队
class LeafTeam : public AbstractTeam
{
public:
using AbstractTeam::AbstractTeam;
void fight() override
{
cout << m_parent->getName() + m_name + "与黑胡子的船员进行近距离肉搏战..." << endl;
}
void display() override
{
cout << "我是" << m_parent->getName() << "下属的" << m_name << endl;
}
~LeafTeam()
{
cout << "我是" << m_parent->getName() << "下属的" << m_name
<< ", 战斗已经结束, 拜拜..." << endl;
}
};

叶子节点对应的番队由于没有子节点,所以在其对应的类中就不需要重写父类的add(AbstractTeam* node)remove(AbstractTeam* node)方法了,这也是基类中为什么不把这两个虚函数指定为纯虚函数的原因。

2.3 管理者节点

所谓的管理者节点其实就是非叶子节点。这种节点还拥有子节点,它的实现肯定是需要继承抽象节点类的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 管理者节点
class ManagerTeam : public AbstractTeam
{
public:
using AbstractTeam::AbstractTeam;
void fight() override
{
cout << m_name + "和黑胡子的恶魔果实能力者战斗!!!" << endl;
}
void add(AbstractTeam* node) override
{
node->setParent(this);
m_children.push_back(node);
}
void remove(AbstractTeam* node) override
{
node->setParent(nullptr);
m_children.remove(node);
}
bool hasChild()
{
return true;
}
list<AbstractTeam*> getChildren()
{
return m_children;
}
void display()
{
string info = string();
for (const auto item : m_children)
{
if (item == m_children.back())
{
info += item->getName();
}
else
{
// 优先级: + > +=
info += item->getName() + ", ";
}
}
cout << m_name + "的船队是【" << info << "】" << endl;
}
~ManagerTeam()
{
cout << "我是【" << m_name << "】战斗结束, 拜拜..." << endl;
}
private:
list<AbstractTeam*> m_children;
};

在管理者节点类的内部有一个容器list,容器内存储的就是它的子节点对象:

  • 通过add(AbstractTeam* node)把当前番队的子节点存储到list
  • 通过remove(AbstractTeam* node)把某一个子节点从当前番队的list中删除
  • 通过display()来遍历这个list容器中的节点

2.4 战斗

最后把测试程序写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 内存释放
void gameover(AbstractTeam* root)
{
if (root == nullptr)
{
return;
}
if (root && root->hasChild())
{
ManagerTeam* team = dynamic_cast<ManagerTeam*>(root);
list<AbstractTeam*> children = team->getChildren();
for (const auto item : children)
{
gameover(item);
}
}
delete root;
}

// 和黑胡子战斗
void fighting()
{
vector<string> nameList = {
"俊美海贼团", "巴托俱乐部", "八宝水军", "艾迪欧海贼团",
"咚塔塔海贼团", "巨兵海贼团", "约塔玛利亚大船团"
};
// 根节点
ManagerTeam* root = new ManagerTeam("草帽海贼团");
for (int i = 0; i < nameList.size(); ++i)
{
ManagerTeam* child = new ManagerTeam(nameList.at(i));
root->add(child);
if (i == nameList.size() - 1)
{
// 给最后一个番队添加子船队
for (int j = 0; j < 9; ++j)
{
LeafTeam* leaf = new LeafTeam("第" + to_string(j + 1) + "番队");
child->add(leaf);
leaf->fight();
leaf->display();
}
child->fight();
child->display();
}
}
root->fight();
root->display();

cout << "====================================" << endl;
gameover(root);
}

int main()
{
fighting();
return 0;
}

输出的结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
约塔玛利亚大船团第1番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第1番队
约塔玛利亚大船团第2番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第2番队
约塔玛利亚大船团第3番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第3番队
约塔玛利亚大船团第4番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第4番队
约塔玛利亚大船团第5番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第5番队
约塔玛利亚大船团第6番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第6番队
约塔玛利亚大船团第7番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第7番队
约塔玛利亚大船团第8番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第8番队
约塔玛利亚大船团第9番队与黑胡子的船员进行近距离肉搏战...
我是约塔玛利亚大船团下属的第9番队
约塔玛利亚大船团和黑胡子的恶魔果实能力者战斗!!!
约塔玛利亚大船团的船队是【第1番队, 第2番队, 第3番队, 第4番队, 第5番队, 第6番队, 第7番队, 第8番队, 第9番队】
草帽海贼团和黑胡子的恶魔果实能力者战斗!!!
草帽海贼团的船队是【俊美海贼团, 巴托俱乐部, 八宝水军, 艾迪欧海贼团, 咚塔塔海贼团, 巨兵海贼团, 约塔玛利亚大船团】
====================================
我是【俊美海贼团】战斗结束, 拜拜...
我是【巴托俱乐部】战斗结束, 拜拜...
我是【八宝水军】战斗结束, 拜拜...
我是【艾迪欧海贼团】战斗结束, 拜拜...
我是【咚塔塔海贼团】战斗结束, 拜拜...
我是【巨兵海贼团】战斗结束, 拜拜...
我是约塔玛利亚大船团下属的第1番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第2番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第3番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第4番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第5番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第6番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第7番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第8番队, 战斗已经结束, 拜拜...
我是约塔玛利亚大船团下属的第9番队, 战斗已经结束, 拜拜...
我是【约塔玛利亚大船团】战斗结束, 拜拜...
我是【草帽海贼团】战斗结束, 拜拜...

由于草帽大船团对应的设计模式是组合模式,它对应的是一个树模型,并且每个节点的操作方式都形同,所以在释放节点的时候就可以使用递归了,gameover()函数就是一个递归函数。

3. 结构图

学完了组合模式,根据上面的例子把对应的UML类图画一下(学会之后就得先画类图,再写程序了

为了能够更加清楚地描述出设计模式中的组合关系(不是UML中的组合关系),在AbstractTeamManagerTeam之间画了两条线:

  • 继承关系:对节点的操作使用的是抽象类中提供的接口,以保证操作的一致性
  • 聚合关系:ManagerTeam类型的节点还可以有子节点,父节点和子节点的之间的关系需要具体问题具体分析
    • 子节点跟随父节点一起销毁,二者就是组合关系(UML中的组合关系)
    • 子节点不跟随父节点一起销毁,二者就是聚合关系
    • 上面的程序中,在父节点的析构函数中没有销毁它管理的子节点,所以在上图中标记的是聚合关系