zoukankan      html  css  js  c++  java
  • 【设计模式

    一、定义与特点

    组合(Composite Pattern)模式的定义:有时又叫作整体-部分模式,是用于把一组相似的对象当作一个单一的对象。组合模式是用于整体与部分的结构,当整体与部分有相似的结构,在操作时可以被一致对待时,就可以使用组合模式。例如:

    • 文件夹和子文件夹的关系:文件夹中可以存放文件,也可以新建文件夹,子文件夹也一样。
    • 总公司子公司的关系:总公司可以设立部门,也可以设立分公司,子公司也一
    • 树枝和分树枝的关系:树枝可以长出叶子,也可以长出树枝,分树枝也一样。

    在这些关系中,虽然整体包含了部分,但无论整体或部分,都具有一致的行为。对这些简单对象与复合对象的处理,如果用组合模式来实现会很方便。

    组合模式的主要优点有:

    • 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;

    • 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”。

    其主要缺点是:

    • 设计较复杂,客户端需要花更多时间理清类之间的层次关系;

    • 不容易限制容器中的构件;

    • 不容易用继承的方法来增加构件的新功能;


    二、结构

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

    • 抽象构件(Component)角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。(总的抽象类或接口,定义一些通用的方法,比如新增、删除)

    • 树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于继承或实现抽象构件。

    • 树枝构件(Composite)角色 / 中间构件:是组合中的分支节点对象,它有子节点,用于继承和实现抽象构件。它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。


    三、具体实现

    考虑这样一个实际应用:设计一个公司的人员分布结构,结构如下图所示。

    img


    我们注意到人员结构中有两种结构,一是管理者,如老板,PM,CFO,CTO,二是职员。其中有的管理者不仅仅要管理职员,还会管理其他的管理者。这就是一个典型的整体与部分的结构。


    3.1 不使用组合模式的设计方案

    要描述这样的结构,我们很容易想到以下设计方案:

    #include <iostream>
    #include <string>
    #include <vector>
    
    using namespace std;
    
    // 职员类
    class Employee {
    public:
        // 自定义构造函数
        Employee(string position, string job) {
            this->position = position;
            this->job = job;
        }
    
        // 做自已的本职工作
        void work() {
            cout << "我是" << position << ",我正在" << job << endl;
        }
    
    private:
        // 职位
        string position;
        // 工作内容
        string job;
    };
    
    // 管理者类
    class Manager {
    public:
        // 自定义构造函数
        Manager(string position, string job) {
            this->position = position;
            this->job = job;
        }
    
        void addManager(Manager *manager) {
            managers.push_back(manager);
        }
    
        void removeManager() {
            managers.pop_back();
        }
    
        void addEmployee(Employee *employee) {
            employees.push_back(employee);
        }
    
        void removeEmployee() {
            employees.pop_back();
        }
    
        // 做自已的本职工作
        void work() {
            cout << "我是" << position << ",我正在" << job << endl;
        }
    
        // 检查下属
        void check() {
            work();
    
            // 遍历employees职员列表
            vector<Employee*>::iterator it = employees.begin();
            for(; it!=employees.end(); ++it) {
                (*it)->work();
            }
            // 遍历managers管理者列表
            vector<Manager*>::iterator it2 = managers.begin();
            for(; it2!=managers.end(); ++it2) {
                (*it2)->check();
            }
        }
    
    private:
        // 职位
        string position;
        // 工作内容
        string job;
        // 管理的管理者
        //list<Manager> *managers = new list<Manager>;
        vector<Manager*> managers;
        // 管理的职员
        //list<Employee> *employees = new list<Employee>;
        vector<Employee*> employees;
    };
    
    int main()
    {
        Manager *boss = new Manager("老板", "唱怒放的生命");
        Employee *HR = new Employee("人力资源", "聊微信");
        Manager *PM = new Manager("产品经理", "不知道干啥");
        Manager *CFO = new Manager("财务主管", "看剧");
        Manager *CTO = new Manager("技术主管", "划水");
        Employee *UI = new Employee("设计师", "画画");
        Employee *operato = new Employee("运营人员", "兼职客服");
        Employee *webProgrammer = new Employee("程序员", "学习设计模式");
        Employee *backgroundProgrammer = new Employee("后台程序员", "CRUD");
        Employee *accountant = new Employee("会计", "背九九乘法表");
        Employee *clerk = new Employee("文员", "给老板递麦克风");
        boss->addEmployee(HR);
        boss->addManager(PM);
        boss->addManager(CFO);
        PM->addEmployee(UI);
        PM->addManager(CTO);
        PM->addEmployee(operato);
        CTO->addEmployee(webProgrammer);
        CTO->addEmployee(backgroundProgrammer);
        CFO->addEmployee(accountant);
        CFO->addEmployee(clerk);
    
        boss->check();
    
        return 0;
    }
    

    运行测试方法,输出如下(为方便查看,笔者添加了缩进):

    我是老板,我正在唱怒放的生命
    	我是人力资源,我正在聊微信
    	我是产品经理,我正在不知道干啥
    		我是设计师,我正在画
    		我是运营人员,我正在兼职客服
    		我是技术主管,我正在划水
    			我是程序员,我正在学习设计模式
    			我是后台程序员,我正在CRUD
    	我是财务主管,我正在看剧
    		我是会计,我正在背九九乘法表
    		我是文员,我正在给老板递麦克风
    

    这样我们就设计出了公司的结构,但是这样的设计有两个弊端:

    • name 字段,job 字段,work 方法重复了。
    • 管理者对其管理的管理者和职员需要区别对待。

    关于第一个弊端,虽然这里为了讲解,只有两个字段和一个方法重复,实际工作中这样的整体部分结构会有相当多的重复。比如此例中还可能有工号、年龄等字段,领取工资、上下班打卡、开各种无聊的会等方法。

    大量的重复显然是很丑陋的代码,分析一下可以发现, Manager 类只比 Employee 类多—个管理人员的列表字段,多几个增加/移除人员的方法,其他的字段和方法全都是一样的。

    有读者应该会想到:我们可以将重复的字段和方法提取到个工具类中,让 Employee 和 Manager 都去调用此工具类,就可以消除重复了这样固然可行,但属于 Employee 和 Manager 类自己的东西却要通过其他类调用,并不利于程序的高内聚。

    关于第二个弊端,此方案无法解决,此方案中 Employee 和 Manager 类完全是两个不同的对象,两者的相似性被忽略了所以我们有更好的设计方案,那就是组合模式!


    3.2 使用组合模式的设计方案

    组合模式最主要的功能就是让用户可以一致对待整体和部分结构,将两者都作为一个相同的组件,所以我们先新建一个抽象的组件类:

    #include <iostream>
    #include <string>
    #include <vector>
    
    using namespace std;
    
    // 抽象的组件类
    class Component{
    public:
        Component() {}
        virtual ~Component() {}
        Component(string position, string job) {}
    
        // 做自已的本明工作
        void work() {
            cout << "我是" << position << ",我正在" << job << endl;
        }
    
        // 注意要加上函数体{},否则报错
        virtual void addComponent(Component *component) {}
        virtual void removeComponent() {}
        virtual void check() {}
    
    public:
        // 职位
        string position;
        // 工作内容
        string job;
    };
    
    // 职员类
    class Employee: public Component {
    public:
        // 自定义构造函数
        Employee(string position, string job) {
            this->position = position;
            this->job = job;
        }
    
        void addComponent(Component *component) {
            cout << "职员没有管理权限" << endl;
        }
    
        void removeComponent() {
            cout << "职员没有管理权限" << endl;
        }
    
        // 检查下属
        void check() {
            work();
        }
    };
    
    // 管理者类
    class Manager: public Component {
    public:
        Manager() {}
        ~Manager() {}
    
        // 自定义构造函数
        Manager(string position, string job) {
            this->position = position;
            this->job = job;
        }
    
        void addComponent(Component *component) {
            components.push_back(component);
        }
    
        void removeComponent() {
            components.pop_back();
        }
    
        // 检查下属
        void check() {
            work();
    
            // 遍历
            vector<Component*>::iterator it = components.begin();
            for (; it != components.end(); it++){
                (*it)->check();
            }
        }
    
    private:
        // 管理的组件
        vector<Component*> components;
    };
    
    int main()
    {
        Component *boss = new Manager("老板", "唱怒放的生命");
        Component *HR = new Employee("人力资源", "聊微信");
        Component *PM = new Manager("产品经理", "不知道干啥");
        Component *CFO = new Manager("财务主管", "看剧");
        Component *CTO = new Manager("技术主管", "划水");
        Component *UI = new Employee("设计师", "画画");
        Component *operato = new Employee("运营人员", "兼职客服");
        Component *webProgrammer = new Employee("程序员", "学习设计模式");
        Component *backgroundProgrammer = new Employee("后台程序员", "CRUD");
        Component *accountant = new Employee("会计", "背九九乘法表");
        Component *clerk = new Employee("文员", "给老板递麦克风");
        boss->addComponent(HR);
        boss->addComponent(PM);
        boss->addComponent(CFO);
        PM->addComponent(UI);
        PM->addComponent(CTO);
        PM->addComponent(operato);
        CTO->addComponent(webProgrammer);
        CTO->addComponent(backgroundProgrammer);
        CFO->addComponent(accountant);
        CFO->addComponent(clerk);
    
        boss->check();
    
        return 0;
    }
    

    运行测试方法,输出结果与之前的结果一模一样。

    可以看到,使用组合模式后,我们解决了之前的两个弊端。一是将共有的字段与方法移到了父类中,消除了重复,并且在客户端中,可以一致对待 Manager 和 Employee 类:

    • Manager 类和 Employee 类统一声明为 Component 对象。
    • 统调用 Component 对象的 addComponent 方法添加子对象即可。

    3.3 组合模式中的安全方式与透明方式

    读者可能已经注意到了, Employee 类虽然继承了父类的 addComponent 和 remove Component 方法,但是仅仅提供了一个空实现,因为 Employee 类是不支持添加和移除组件的。这样是否违背了接口隔离原则呢?

    接口隔离原则:客户端不应依赖它不需要的接口。如果一个接口在实现时,部分方法由于冗余被客户端空实现,则应该将接口拆分,让实现类只需依赖自己需要的接口方法。

    答案是肯定的,这样确实违背了接口隔离原则。这种方式在组合模式中被称作透明方式

    透明方式:在 Component 中声明所有管理子对象的方法,包括 add、 remove 等,这样继承自 Component 的子类都具备了 ad、 remove 方法。对于外界来说叶节点和枝节点是透明的,它们具备完全一致的接口。

    这种方式有它的优点:让 Manager 类和 Employee 类具备完全一致的行为接口,调用者可以—致对待它们。

    但它的缺点也显而易见:Employee 类并不支持管理子对象,不仅违背了接口隔离原则,而且客户端可以用 Employee 类调用 addComponent 和 remove Component 方法,导致程序出错,所以这种方式是不安全的。

    那么我们可不可以将 addComponent 和 remove Component 方法移到 Manager 子类中去单独实现,让 Employee 不再实现这两个方法呢?我们来尝试一下。修改为:

    #include <iostream>
    #include <string>
    #include <vector>
    
    using namespace std;
    
    // 抽象的组件类
    class Component{
    public:
        Component() {}
        virtual ~Component() {}
        Component(string position, string job) {}
    
        // 做自已的本明工作
        void work() {
            cout << "im" << position << ",doing" << job << endl;
        }
    
        // 注意要加上函数体{},否则报错
        virtual void check() {}
    
    public:
        // 职位
        string position;
        // 工作内容
        string job;
    };
    
    // 职员类
    class Employee: public Component {
    public:
        // 自定义构造函数
        Employee(string position, string job) {
            this->position = position;
            this->job = job;
        }
    
        // 检查下属
        void check() {
            work();
        }
    };
    
    // 管理者类
    class Manager: public Component {
    public:
        Manager() {}
        ~Manager() {}
    
        // 自定义构造函数
        Manager(string position, string job) {
            this->position = position;
            this->job = job;
        }
    
        void addComponent(Component *component) {
            components.push_back(component);
        }
    
        void removeComponent() {
            components.pop_back();
        }
    
        // 检查下属
        void check() {
            work();
    
            // 遍历
            vector<Component*>::iterator it = components.begin();
            for (; it != components.end(); it++){
                (*it)->check();
            }
        }
    
    private:
        // 管理的组件
        vector<Component*> components;
    };
    
    int main()
    {
        Manager *boss = new Manager("老板", "唱怒放的生命");
        Employee *HR = new Employee("人力资源", "聊微信");
        Manager *PM = new Manager("产品经理", "不知道干啥");
        Manager *CFO = new Manager("财务主管", "看剧");
        Manager *CTO = new Manager("技术主管", "划水");
        Employee *UI = new Employee("设计师", "画画");
        Employee *operato = new Employee("运营人员", "兼职客服");
        Employee *webProgrammer = new Employee("程序员", "学习设计模式");
        Employee *backgroundProgrammer = new Employee("后台程序员", "CRUD");
        Employee *accountant = new Employee("会计", "背九九乘法表");
        Employee *clerk = new Employee("文员", "给老板递麦克风");
        boss->addComponent(HR);
        boss->addComponent(PM);
        boss->addComponent(CFO);
        PM->addComponent(UI);
        PM->addComponent(CTO);
        PM->addComponent(operato);
        CTO->addComponent(webProgrammer);
        CTO->addComponent(backgroundProgrammer);
        CFO->addComponent(accountant);
        CFO->addComponent(clerk);
    
        boss->check();
    
        return 0;
    }
    

    运行程序,输出结果与之前一模一样。

    可以看到,我们在父类中去掉了 addComponent 和 removeComponent 这两个抽象方法,Manager 类单独实现了 addComponent 和 remove Component 这两个方法。这种方式在组合模式中称之为安全方式

    安全方式:在 Component中不声明add和 remove等管理子对象的方法,这样叶节点就无需实现它,只需在枝节点中实现管理子对象的方法即可。

    安全方式遵循了接口隔离原则,但由于不够透明, Manager 和 Employee 类不具有相同的接口在客户端中,我们无法将 Manager 和 Employee 统声明为 Component 类了,必须要区别对待,带来了使用上的不方便。

    安全方式和透明方式各有好处,在使用组合模式时,需要根据实际情况决定。但大多数使用组合模式的场景都是采用的透明方式,虽然它有点不安全,但是客户端无需做任何判断来区分是叶子结点还是枝节点,用起来是真香。


    参考:

    知乎 - 如何学习设计模式? 热门回答

    组合模式(详解版)

    菜鸟教程 - 设计模式篇


  • 相关阅读:
    队列

    有序数组
    集合:一条规则决定性能
    基础数据结构:数组
    空间复杂度
    插入排序
    重新认识Javascript的一些误区总结
    Knockout: 使用knockout validation插件进行校验, 给未通过校验的输入框添加红色边框突出显示.
    Knockout: 使用CSS绑定和event的blur失去焦点事件, 给未通过校验的输入框添加红色边框突出显示.
  • 原文地址:https://www.cnblogs.com/linuxAndMcu/p/14395937.html
Copyright © 2011-2022 走看看