设计模式学习——组合模式
引入
写过Qt程序的你,对于每一个Qt窗口对象初始化时需要携带的parent对象参数一定不会陌生。实际上,这暗示了一种树状结构的设计模式——组合模式。
组合模式的目的,就是将对象组合成树形结构以表示"部分-整体"的层次结构。在此模式中,我们定义所有树节点的基类,并让所有具体节点继承该基类。他们都通过类中的根节点parent
属性互相链接。
以Qt为例,在Qt窗口程序中,有一个主窗口QMainWindow
类,其下挂载了QGridLayout
、QTableView
等容器控件,在这些容器中又挂载了QLable
、QTextEdit
等等具体的窗口控件。通过这样的组合,我们可以方便地管理各个窗口和容器的内容;当应用程序结束时,QMainWindow
会从根至叶地析构所有的子节点,也避免了复杂的内存管理。
TUI应用的实现
仿照Qt的组合模式,我们来写一套我们自己的TUI(控制台UI)实现方案。
首先,定义一个树节点的基类GObject
:
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
| class GObject {
public:
explicit GObject(GObject* parent);
virtual ~GObject();
void setParent(GObject* node);
GObject* getParent();
void setName(const std::string & name);
const std::string & getName();
virtual bool hasChild();
virtual void add(GObject* node);
virtual void remove(GObject* node);
virtual void render() = 0;
protected:
std::string _name;
GObject* _parent = nullptr;
};
GObject::GObject(GObject* parent) {
setParent(parent);
}
GObject::~GObject() = default;
void GObject::setParent(GObject *node) {_parent = node;}
GObject * GObject::getParent() {return _parent;}
void GObject::setName(const std::string &name) {_name = name;}
const std::string & GObject::getName() {return _name;}
bool GObject::hasChild() {return false;}
|
这里我们定义了基类成员的Getter
和Setter
,引入了虚函数add(GObject* node)
、remove(GObject* node)
等作为可增加子节点的容器的接口;还有纯虚函数render()
用来渲染具体的元素。
接下来我们定义可显示的元素GLable
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| class GLable : public GObject {
public:
explicit GLable(GObject* parent);
explicit GLable(GObject* parent, std::string text);
~GLable() override = default;
[[nodiscard]] const std::string & text() const;
void text(const std::string &text);
void render() override;
private:
std::string _text;
};
GLable::GLable(GObject *parent) : GObject(parent){}
GLable::GLable(GObject *parent, std::string text) : GObject(parent), _text(std::move(text)){}
const std::string & GLable::text() const { return _text;}
void GLable::text(const std::string &text) { _text = text;}
void GLable::render() {
std::cout << _text;
}
|
在GLable
中,我们不重写子节点相关的函数,意味着GLable
将是一个没有子节点的叶节点。我们为GLable
定义成员_text
用来储存标签中显示的文字。
接下来我们定义两种水平、垂直两种布局。
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
| class GHLayout : public GObject {
public:
explicit GHLayout(GObject* parent);
~GHLayout() override;
bool hasChild() override;
std::list<GObject*> getChildren();
void add(GObject *node) override;
void remove(GObject *node) override;
void render() override;
private:
std::list<GObject*> _children;
};
GHLayout::GHLayout(GObject *parent) : GObject(parent){}
GHLayout::~GHLayout() {
for(const auto child : _children) {
delete child;
}
_children.clear();
}
bool GHLayout::hasChild() {return !_children.empty();}
std::list<GObject *> GHLayout::getChildren() { return _children;}
void GHLayout::add(GObject *node) { _children.push_back(node);}
void GHLayout::remove(GObject *node) {
node->setParent(nullptr);
_children.remove(node);
}
void GHLayout::render() {
for(const auto child : _children) {
child->render();
}
}
|
在布局类中,我们加入std::list
私有成员变量用来储存子节点。此时组合模式的好处体现了出来——我们可以在render()
方法中递归地调用子节点的render()
方法来输出所有的内容,还可以在析构函数中递归地析构所有的子节点。
对于垂直布局,由于它们的区别只有渲染时的回车与否,我们可以使用适配器模式完成。
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
| class GVLayout : public GObject {
public:
explicit GVLayout(GObject* parent);
~GVLayout() override;
bool hasChild() override;
std::list<GObject*> getChildren();
void add(GObject *node) override;
void remove(GObject *node) override;
void render() override;
private:
GHLayout* _adapter;
};
GVLayout::GVLayout(GObject *parent) : GObject(parent), _adapter(new GHLayout(parent)){}
GVLayout::~GVLayout() { delete _adapter;}
bool GVLayout::hasChild() { return _adapter->hasChild();}
std::list<GObject *> GVLayout::getChildren() { return _adapter->getChildren();}
void GVLayout::add(GObject *node) { _adapter->add(node);}
void GVLayout::remove(GObject *node) { _adapter->remove(node);}
void GVLayout::render() {
for(const auto child : _adapter->getChildren()) {
child->render();
std::cout << std::endl;
}
}
|
我们还定义一个窗口类,用来表示TUI中的一个窗口。其和两个布局的思路基本相同。
最后,我们在主程序中创建并使用上述节点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| int main() {
auto mainwindow = new GWindow(nullptr);
mainwindow->setName("Software");
auto layout1 = new GHLayout(mainwindow);
auto lable1 = new GLable(layout1, "Hello ");
auto lable2 = new GLable(layout1, "C++ Composite Pattern!");
layout1->add(lable1);
layout1->add(lable2);
auto layout2 = new GVLayout(mainwindow);
auto lable3 = new GLable(layout2, "MoonFeather");
auto lable4 = new GLable(layout2, "2024.8.25");
layout2->add(lable3);
layout2->add(lable4);
mainwindow->add(layout1);
mainwindow->add(layout2);
mainwindow->render();
delete mainwindow;
return 0;
}
|
得到结果:
1
2
3
4
5
6
| |----- Software ---------- - x|
|Hello C++ Composite Pattern!
|MoonFeather
2024.8.25
|-----------------------------|
|
使用Dr.Memory进行内存泄露分析无误。
总结
组合模式创建了对象组的树形结构。简化树形结构中对象的处理,解耦客户端代码与复杂元素的内部结构,使得客户端可以统一处理所有类型的节点。