Featured image of post 设计模式学习——组合模式

设计模式学习——组合模式

这篇笔记介绍了组合模式,通过 C++ 示例展示了如何创建树形结构的对象组。文章详细讲解了基类、标签、布局和窗口类的实现,并展示了组合模式在简化对象处理和解耦客户端代码中的应用。

设计模式学习——组合模式

引入

写过Qt程序的你,对于每一个Qt窗口对象初始化时需要携带的parent对象参数一定不会陌生。实际上,这暗示了一种树状结构的设计模式——组合模式。

image

组合模式的目的,就是将对象组合成树形结构以表示"部分-整体"的层次结构。在此模式中,我们定义所有树节点的基类,并让所有具体节点继承该基类。他们都通过类中的根节点parent属性互相链接。

以Qt为例,在Qt窗口程序中,有一个主窗口QMainWindow类,其下挂载了QGridLayoutQTableView等容器控件,在这些容器中又挂载了QLableQTextEdit等等具体的窗口控件。通过这样的组合,我们可以方便地管理各个窗口和容器的内容;当应用程序结束时,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;}

这里我们定义了基类成员的GetterSetter,引入了虚函数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进行内存泄露分析无误。

总结

组合模式创建了对象组的树形结构。简化树形结构中对象的处理,解耦客户端代码与复杂元素的内部结构,使得客户端可以统一处理所有类型的节点。