说明:此文章两个示例中,公司结构的示例思路来自于《大话设计模式》,内容以及代码经过了一定修改,尊重且维护作者版权所有,特此声明。

一 引言

在生活中常常会见到一些具有层级关系的结构,例如学生时代的【大学-学院-专业】之间的关系就是这样,同样还有例如【总公司-分公司/部门】、【书包-书】,软件开发中也是啊,【文件夹-文件】、【容器-组件】

但是其实可以发现其共性,都是大范围包括小范围这样的形式,例如每一个学院下面都有不同的专业,例如计算机学院下的软件工程专业,我们可以将其称之为 “整体-部分” 的模式

如果我们按照最简单能想到的方式,或许就是多层继承,例如 XXX大学,下面计算机学院等多个学院去继承 XXX 大学,而软件工程,计算机科学与技术等专业又去继承了计算机学院

年轻人,这好吗,这不好。

这种方式其实是按照组织的规模大小来进行划分的,但我们从实际出发,除了其规模,我们更倾向于展示其组成结构,例如计算机学院下有多个专业,同时对其进行一定的维护业务,例如更新遍历等

而使用今天要讲的组合模式就可以实现我们的想法

二 分公司不就是一部门吗

这个案例的需求是这样的,一家在全国都有分销机构的大公司,想要在全国的分公司中一起使用同一套办公管理系统,例如在北京由总部,全国几大城市有分公司,还有几个省会有办事处。要求总公司的人力资源部没财务部等办公管理功能在所有分公司和办事处都需要有,该如何办?

(一) 分析

首先捋一下总公司,分公司,办事处,以及各自所属几个部门的关系

根据图可以看出,北京公司总部是最高级别的,其拥有两个最直接的部门,即:人力资源部和财务部,而分公司其实和这几个部门是属于同一级的,而人力资源部,财务部这两个管理功能又可以复用于分公司。办事处这个级别在下面一层,也是如此。

分析完这个图,其实如何去做已经有了一个简单的思路了,简单的平行管理肯定是不合适的,所以我们将其做成一个树状结构,这样维护管理起来也会非常方便

北京总公司就好比这个根节点,其下属分公司也就是这棵树的分支,像办事处就是更小的分支,无论是总公司还是分公司,亦或办事处的相关职能部门(如财务部)都没有分支了,所以也就是叶子节点

直接讲解例子或许会有一些不明朗各个公司,部门之间的关系,我们先通过演示最简单的组合模式来铺垫一下,下一个点,再来实现上面的公司例子

(二) 组合模式

(1) 什么是组合模式

定义:组合模式有时又叫作“整体-部分”模式,它是一种将对象组合成树状的层次结构的模式,用来表示“整体-部分”的关系,使用户对单个对象和组合对象具有一致的访问性

(2) 结构图

(3) 简单代码

Component 为组合中的树枝以及树叶对象声明公共接口

public abstract class Component {
    protected String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    // 添加部件
    public abstract void add(Component component);

    // 删除部件
    public abstract void remove(Component component);

    // 遍历所有子部件内容
    public abstract void traverseChild();
}

Leaf 即表示叶节点,起没有子节点,所有 add 和 remove 方法均没有具体业务,只是抛一个异常,只单纯实现遍历方法

public class Leaf extends Component{
    @Override
    public void add(Component component) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void remove(Component component) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void traverseChild() {
        System.out.println("执行:" + super.getName());
    }
}

Composite 代表树枝,用来存储子部件,主要作用来存储和管理子部件

public class Composite extends Component {

    // 用来保存组合的部件
    List<Component> list = new ArrayList<Component>();

    @Override
    public void add(Component component) {
        list.add(component);
    }

    @Override
    public void remove(Component component) {
        list.remove(component);
    }

    @Override
    public void traverseChild() {
        System.out.println("执行:" + name);
        for (Component c : list) {
            c.traverseChild();
        }
    }
}

测试一下

public class Test {
    public static void main(String[] args) {
        // 设置根节点
        Composite root = new Composite();
        root.name = "根节点";

        // 添加叶子节点
        Leaf leafA = new Leaf();
        leafA.setName("叶子节点 A");
        Leaf leafB = new Leaf();
        leafB.setName("叶子节点 B");
        // 将 A 和 B 添加到 root 下
        root.add(leafA);
        root.add(leafB);
        // 遍历
        root.traverseChild();
    }
}

结果:

执行:根节点
执行:叶子节点 A
执行:叶子节点 B

(4) 透明方式和安全方式

在上面的代码中,我们在抽象的 Component 中定义了 add 和 remove 方法

// 添加部件
public abstract void add(Component component);
// 删除部件
public abstract void remove(Component component);

这也就意味着,即使在叶子节点 Leaf 类中也需要实现 add 以及 remove 方法,(这里我们做了抛异常处理,还可以选择空实现)其实这种方式就叫做透明方式,也就是在 Component 中声明所有用来管理子对象的方法,其中包括 add 以及 remove 方法,这样但凡继承或者实现这个类的子类都具备了 这些方法,这样的行为好处在于,使得叶节点,枝节点这一件具有了完全一致的行为接口,缺点就是,叶节点中的空实现等并无意义

而安全方式就是不在 Component 中声明 add 以及 remove 方法,而是在 Composite 声明所有用来管理子类对象的方法,这样就不会有刚才的问题了,其缺点是叶节点,枝节点不再具有相同的结构,客户端调用需要增加一些判断

(5) 优缺点

优点:

  • 类似例子中职能部门这种基本对象以及分公司办事处等组合对象之间可以组合成更复杂的组合对象,而组合对象又可以被组合
  • 客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,客户端调用方便
  • 组合体中加入新内容,不需要修改源代码,满足开闭原则

缺点:

  • 设计较复杂,类与类之间的层次关系需要捋清楚

(三) 公司示例代码实现

下面我们再结合上面具体的例子来应用一下组合模式(透明方式)

公司的抽象类,相当于上面的 Component

/**
 * 公司抽象类
 */
public abstract class Company {
    protected String name;

    public Company(String name) {
        this.name = name;
    }

    // 增加
    public abstract void add(Company company);

    // 删除
    public abstract void remove(Company company);

    // 显示
    public abstract void display(int depth);

    // 履行职责
    public abstract void lineOfDuty();
}

具体公司类,相当于 Composite ,为了在控制台输出时能看出其层级结构,我根据当前节点的深度,增加了一下等于符号 MyStringUtil.repeatString("=", depth) 就是自己封装了一个方法,根据 depth 的值来多次输出一个字符串

/**
 * 具体公司类,树枝节点
 */
public class ConcreteCompany extends Company {
    // 用来存储其下厨枝节点和叶节点
    private List<Company> children = new ArrayList<>();

    public ConcreteCompany(String name) {
        super(name);
    }

    @Override
    public void add(Company company) {
        children.add(company);
    }

    @Override
    public void remove(Company company) {
        children.remove(company);
    }

    @Override
    public void display(int depth) {
        // 显示其枝节点名称,并对其下级进行遍历
        System.out.println(MyStringUtil.repeatString("=", depth) + name);
        for (Company c : children) {
            c.display(depth + 4);
        }
    }

    @Override
    public void lineOfDuty() {
        // 遍历每一个孩子的节点
        for (Company c : children) {
            c.lineOfDuty();
        }
    }
}

上面用到的工具类就这样

public class MyStringUtil {
    public static String repeatString(String repeatStr, int repeatNum) {
        StringBuilder stringBuilder = new StringBuilder();
        while (repeatNum-- > 0) {
            stringBuilder.append(repeatStr);
        }
        return stringBuilder.toString();
    }
}

下面是两个具体的部门,也就是叶子节点

/**
 * 人力资源部
 */
public class HRDepartment extends Company {


    public HRDepartment(String name) {
        super(name);
    }

    @Override
    public void add(Company company) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void remove(Company company) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void display(int depth) {

        // 显示其枝节点名称,并对其下级进行遍历
        System.out.println(MyStringUtil.repeatString("=", depth) + name);
    }

    @Override
    public void lineOfDuty() {
        System.out.println("【" + name + "】员工培训管理");
    }
}
/**
 * 财务部
 */
public class FinanceDepartment extends Company {


    public FinanceDepartment(String name) {
        super(name);
    }

    @Override
    public void add(Company company) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void remove(Company company) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void display(int depth) {


        // 显示其枝节点名称,并对其下级进行遍历
        System.out.println(MyStringUtil.repeatString("=", depth) + name);
    }


    @Override
    public void lineOfDuty() {
        System.out.println("【" + name + "】公司财务收支管理");
    }
}

测试类,我们分别创建了 root、comp、comp1 等多个 ConcreteCompany,通过 add 方法先给每部分都添加了这几个职能部门,在通过像 root.add(comp); 这样的代码表示了其层级

public class Test {
    public static void main(String[] args) {
        // 创建根节点
        ConcreteCompany root = new ConcreteCompany("北京总公司");
        root.add(new HRDepartment("总公司人力资源部"));
        root.add(new FinanceDepartment("总公司财务部"));

        ConcreteCompany comp = new ConcreteCompany("上海华东分公司");
        comp.add(new HRDepartment("华东分公司人力资源部"));
        comp.add(new FinanceDepartment("华东分公司财务部"));

        root.add(comp);
        
        ConcreteCompany comp = new ConcreteCompany("南京办事处");
        comp1.add(new HRDepartment("南京办事处人力资源部"));
        comp1.add(new FinanceDepartment("南京办事处财务部"));
        comp.add(comp1);

        System.out.println("结构图:");
        root.display(1);

        System.out.println("职责:");
        root.lineOfDuty();
    }
}

运行结果:

结构图:
=北京总公司
=====总公司人力资源部
=====总公司财务部
=====上海华东分公司
=========华东分公司人力资源部
=========华东分公司财务部
=========南京办事处
=============南京办事处人力资源部
=============南京办事处财务部
职责:
【总公司人力资源部】员工培训管理
【总公司财务部】公司财务收支管理
【华东分公司人力资源部】员工培训管理
【华东分公司财务部】公司财务收支管理
【南京办事处人力资源部】员工培训管理
【南京办事处财务部】公司财务收支管理

二境志
191 声望26 粉丝