C++设计模式——组合模式(Composite Pattern)

C++设计模式——组合模式

微信公众号:幼儿园的学霸

目录

文章目录

  • C++设计模式——组合模式
  • 目录
  • 定义
    • 定义
    • 结构
  • 代码示例
  • 总结
    • 使用场景及注意事项
    • 优缺点
  • 参考资料

定义

定义

Compose objects into tree structures to represent part-whole hierarchies.Composite lets clients treat individual objects and compositions of objects uniformly.
将对象组合成树形结构以表示 “部分-整体” 的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

组合模式 一般用来描述整体部分的关系,它将对象组织到树形结构中,最顶层的节点称为 根节点,根节点下面可以包含 树枝节点 和 叶子节点,树枝节点下面又可以包含 树枝节点 和 叶子节点。如下图所示:
C++设计模式——组合模式(Composite Pattern)_第1张图片
由上图可以看出,其实 根节点树枝节点 本质上是同一种数据类型(蓝色圆圈),可以作为容器使用;而 叶子节点树枝节点 在语义上不属于同一种类型,但是在 组合模式 中,会把 树枝节点叶子节点 认为是同一种数据类型(用同一接口定义),让它们具备一致行为。这样,在 组合模式 中,整个树形结构中的对象都是同一种类型,带来的一个好处就是客户无需辨别 树枝节点 还是 叶子节点,而是可以直接进行操作,给客户使用带来极大的便利

组合模式的设计动机:如何将容器对象和叶子对象进行递归组合,使得客户在使用的过程中无须进行区分,可以对他们进行一致的处理。

组合模的核心:借助同一接口,使叶子节点和树枝节点的操作具备一致性。

组合模式的本质:统一叶子对象和组合对象/树枝对象

结构

组合模式包含以下主要角色:

  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性。它的主要作用是为树枝节点和树叶节点声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树节点完成。
  • 树叶节点(Leaf):是组合中的叶子节点对象,其下再无子节点,用于实现抽象根节点中 声明的公共接口,它是系统层次遍历的最小单位。
  • 树枝节点(Composite):是组合中的分支节点对象,它有子节点。它实现了抽象根节点中声明的接口,它的主要作用是存储和管理子节点,组合树枝节点和叶子节点形成一个树形结构,通常包含 Add()、Remove()、GetChild()等方法。

组合模式在代码具体实现上,有透明组合模式和安全组模式两种实现。

透明组合模式
透明组合模式把组合(树枝节点、树叶节点)使用的方法全部放到抽象根节点(Component)中,让不同层次的节点(树枝节点、树叶节点)的结构都具备相同的行为。其UML类图如下:

在透明组合模式中,由于抽象根节点声明了所有子类中的全部方法,所以产生的好处是客户端无须分辨是树叶节点(Leaf)和树枝节点(Composite),它们具备完全一致的接口,对客户端来说是透明的;但其缺点是树叶节点会继承得到一些它所不需要的方法(管理子类操作的方法,如树叶节点本来没有 Add()、Remove() 及 GetChild() 方法,但是由于被继承了下来,因此需要进行实现,实现为空方法或者抛异常),这与设计模式中接口隔离原则相违背。

接口隔离原则:1)客户端不应该依赖它不需要的接口;2)类间的依赖关系应该建立在最小的接口上

安全组合模式
抽象根节点(Component)只规定各个层次的最基础的一致行为,而把组合(树枝节点)本身的方法(如管理子类对象的添加、删除等)放到自身当中。其UML类图如下:

在该方式中由于将树枝节点特有的行为放到自身当中,树叶节点没有对子对象的管理方法,带来的好处是接口定义职责清晰,符合设计模式的接口隔离原则和单一职责原则,避免了上一种方式的安全性问题;但是由于树枝节点和树叶节点有不同的接口,使得客户端调用时需要区分树枝节点(Composite)和树叶节点(Leaf),这样才能正确处理各个层次的操作,客户端无法依赖抽象(Component),失去了透明性,违背了设计模式的依赖倒置原则。

单一职责原则:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责
依赖倒置原则:程序要依赖于抽象接口,不要依赖于具体实现。或,要对抽象(抽象类)进行编程,不要对实现(具体的实现类)进行编程

代码示例

场景:北京总公司,上海分公司,北京财务部,上海财务部的公司结构。
以北京总部,上海分公司、广州分公司、成都分公司为例。 透明组合模式的代码如下:

#include 

//
//组合模式:透明组合模式
//

// 抽象的部件类描述将来所有部件共有的行为
class Component {

public:
    Component(const std::string &name)
            : m_strCompname(name) {

    }

    virtual ~Component() = default;//基类:虚析构函数

    virtual void Operation() = 0;

    virtual void Add(const std::shared_ptr pComponent) = 0;

    virtual void Remove(const std::shared_ptr pComponent) = 0;

    //为了讲解方便,此处用了裸指针(Raw Pointer),实际项目建议修改为智能指针
    virtual std::vector> *GetChild() = 0;

    virtual const std::string &GetName() const {
        return m_strCompname;
    }

    //打印节点及子节点
    //共有行为及实现,该函数放在基类中即可
    virtual void Print(int level = 0) {

        std::cout << std::string(4*level,'-') <GetChild();
        if (childs)
            for (auto child:*childs)
                child->Print(level + 1);


    };

protected:
    std::string m_strCompname;

};

//树叶节点
class Leaf : public Component {
public:
    Leaf(const std::string &name) : Component(name) {

    }

    void Operation() override {
        std::cout << "I'm " << m_strCompname << std::endl;
    }
    
    // 叶节点没有Add功能,但这样做能使接口具备一致性,这就是透明方式,
    // 如果不加入Add和Remove方法,那就是安全方式。
    void Add(const std::shared_ptr pComponent) override {}

    void Remove(const std::shared_ptr pComponent) override {}

    std::vector> *GetChild() override { return nullptr; }

};

//树枝节点
class Composite : public Component {
public:
    Composite(const std::string &name) : Component(name) {

    }

    ~Composite() {}

    void Operation() override {
        std::cout << "I'm " << m_strCompname << std::endl;
    }

    void Add(std::shared_ptr pComponent) override {
        m_vecComp.emplace_back(pComponent);
    }

    void Remove(std::shared_ptr pComponent) override {
        for (auto it = m_vecComp.begin(); it != m_vecComp.end(); ++it) {
            if ((*it)->GetName() == pComponent->GetName()) {
                m_vecComp.erase(it);
                break;
            }
        }
    }

    std::vector> *GetChild() override {
        if (m_vecComp.size())
            return &m_vecComp;
        return nullptr;

    }

private:
    std::vector> m_vecComp;
};

int main(int argc, char *argv[]) {
    std::shared_ptr pNode = std::make_shared("Beijing Head Office");//北京总部
    std::shared_ptr pNodeHr = std::make_shared("Beijing Department1");
    std::shared_ptr pSubNodeSh = std::make_shared("Shanghai Branch");
    std::shared_ptr pSubNodeCd = std::make_shared("Chengdu Branch");
    std::shared_ptr pSubNodeGz = std::make_shared("Guangzhou Branch");
    pNode->Add(pNodeHr);//叶子节点
    pNode->Add(pSubNodeSh);//树枝节点 上海分部
    pNode->Add(pSubNodeCd);//树枝节点 成都分部
    pNode->Add(pSubNodeGz);//树枝节点 广州分部
    pNode->Print();
    std::cout< pSubNodeShHr = std::make_shared("Shanghai Department1");
    std::shared_ptr pSubNodeShCg = std::make_shared("Shanghai Purchasing Department");
    std::shared_ptr pSubNodeShXs = std::make_shared("Shanghai Sales department");
    std::shared_ptr pSubNodeShZb = std::make_shared("Shanghai Quality supervision Department");
    pSubNodeSh->Add(pSubNodeShHr);
    pSubNodeSh->Add(pSubNodeShCg);
    pSubNodeSh->Add(pSubNodeShXs);
    pSubNodeSh->Add(pSubNodeShZb);
    pNode->Print();
    std::cout<Remove(pSubNodeShZb);

    pNode->Print();
    std::cout<

总结

使用场景及注意事项

  • 场景
    系统对象层次具备整体和部分,呈树形结构,且要求具备统一行为(如树形菜单,操作系统目录结构,公司组织架构等);
    希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象时。

  • 注意事项
    有时候系统需要遍历一个树枝结构的子构件很多次,这时候可以考虑把遍历子构件的结构存储在父构件里面作为缓存(类似我上面写的Print打印节点函数)。
    客户端尽量不要直接调用树叶类中的方法,而是借用其父类(Graphics)的多态性完成调用,这样可以增加代码的复用性。

优缺点

  • 优点:
    组合模式使得客户端代码可以一致地处理对象和对象容器,无需关系处理的单个对象,还是组合的对象容器。
    将”客户代码与复杂的对象容器结构“解耦。
    可以更容易地往组合对象中加入新的构件。
  • 缺点: 使得设计更加复杂。客户端需要花更多时间理清类之间的层次关系。(这个是几乎所有设计模式所面临的问题)。

参考资料

1.设计模式之组合模式
2.组合模式



下面的是我的公众号二维码图片,欢迎关注。
图注:幼儿园的学霸

你可能感兴趣的:(C++,c++,设计模式)