关注「Java视界」公众号,获取更多技术干货

组合模式Composite——树形结构不再头疼

组合模式可以在需要针对“树形结构”进行操作的应用中使用,例如扫描文件夹、渲染网站导航结构等等。

一、什么是组合模式?

组合模式将一组相似的对象看做一个对象处理,并根据一个树状结构来组合对象,提供一个统一的方法去访问相应的对象,使得用户对单个对象和组合对象的使用具有一致性。

很抽象对吧,那就到了举例子的时候:

  • 公司组织关系树

公司组织关系可能分为部门与人,其中人属于部门,有的人有下属,有的人没有下属。如果我们统一将部门、人抽象为组织节点,就可以方便的统计某个部门下有多少人、财务数据等等,而不用关心当前节点是部门还是人。

  • 操作系统的文件夹与文件

操作系统的文件夹与文件也是典型的树状结构,为了方便递归出文件夹内文件数量或者文件总大小,我们最好设计的时候就将文件夹与文件抽象为文件,这样每个节点都拥有相同的方法添加、删除、查找子元素,而不需要关心当前节点是文件夹或是文件。

这下就明白了吧,就是常见的树形结构。

组合模式有两种实现:安全模式和透明模式

  • Component抽象构件角色
    定义参加组合对象的共有方法和属性,可以定义一些默认的行为或属性。

  • Leaf叶子构件
    Leaf叶子构件叶子对象,其下再也没有其他的分支,也就是遍历的最小单位。

  • Composite树枝构件
    树枝对象,它的作用是组合树枝节点和叶子节点形成一个树形结构。组合模式的重点就在树枝构件。

二、安全模式实现

public abstract class Component {
    //个体和整体都具有
    public void operation(){
        //编写业务逻辑
    }
}
public class Composite extends Component {
    //构件容器
    private List<Component> componentArrayList = new ArrayList<Component>();
    //增加一个叶子构件或树枝构件
    public void add(Component component){
        this.componentArrayList.add(component);
    }
    //删除一个叶子构件或树枝构件
    public void remove(Component component){
        this.componentArrayList.remove(component);
    }
    //获得分支下的所有叶子构件和树枝构件
    public List<Component> getChildren(){
        return this.componentArrayList;
    }
}
public class Leaf extends Component {
    /*
     * 可以覆写父类方法
     * public void operation(){
     *
     * }
     */
}
public class Client {
    public static void main(String[] args) {
        //创建一个根节点
        Composite root = new Composite();
        root.operation();
        //创建一个树枝构件
        Composite branch = new Composite();
        //创建一个叶子节点
        Leaf leaf = new Leaf();
        //建立整体
        root.add(branch);
        branch.add(leaf);
    }

    //通过递归遍历树
    public static void showTree(Composite root){
        for(Component c:root.getChildren()){
            if(c instanceof Leaf){ //叶子节点
                c.operation();
            }else{ //树枝节点
                showTree((Composite)c);
            }
        }
    }
}

三、透明模式实现

public abstract class Component {
    //个体和整体都具有
    public void operation(){
        //编写业务逻辑
    }
    //增加一个叶子构件或树枝构件
    public abstract void add(Component component);
    //删除一个叶子构件或树枝构件
    public abstract void remove(Component component);
    //获得分支下的所有叶子构件和树枝构件
    public abstract List<Component> getChildren();
}
public class Composite extends Component {
    //构件容器
    private ArrayList<Component> componentArrayList = new ArrayList<Component>();
    //增加一个叶子构件或树枝构件
    public void add(Component component){
        this.componentArrayList.add(component);
    }
    //删除一个叶子构件或树枝构件
    public void remove(Component component){
        this.componentArrayList.remove(component);
    }
    //获得分支下的所有叶子构件和树枝构件
    public List<Component> getChildren(){
        return this.componentArrayList;
    }
}
public class Leaf extends Component {

    public void add(Component component){
        //空实现
    }

    public void remove(Component component){
        //空实现
    }

    public List<Component> getChildren(){
        //空实现
        return null;
    }
}
public class Client {
    public static void main(String[] args) {
        //创建一个根节点
        Composite root = new Composite();
        root.operation();
        //创建一个树枝构件
        Composite branch = new Composite();
        //创建一个叶子节点
        Leaf leaf = new Leaf();
        //建立整体
        root.add(branch);
        branch.add(leaf);
    }

    //通过递归遍历树
    public static void showTree(Component root){
        for(Component c:root.getChildren()){
            if(c instanceof Leaf){ //叶子节点
                c.operation();
            }else{ //树枝节点
                showTree(c);
            }
        }
    }
}

四、安全模式和透明模式的区别

上面的实现代码可以看到:

  1. Composite和Client是一样的
  2. 安全模式在抽象组件中只定义一些默认的行为或属性,它是把树枝节点和树叶节点彻底分开
  3. 透明模式是把用来组合使用的方法放到抽象类中,不管叶子对象还是树枝对象都有相同的结构,通过判断确认是叶子节点还是树枝节点,如果处理不当,这个会在运行期出现问题,不是很建议的方式
  4. 安全模式与依赖倒置原则冲突
  5. 透明模式的好处就是它基本遵循了依赖倒转原则,方便系统进行扩展
  6. 安全模式在遍历树形结构的的时候需要进行强制类型转换;在透明模式下,遍历整个树形结构是比较容易的,不用进行强制类型转换。

产生上面的区别的根本原因是透明模式的Composite定义了更多的方法,叶子节点和非叶子节点都去实现,这样叶子节点和非叶子节点的区分度就不高了,但是兼容性更强。

 五、安全模式下代码实例

现有下面的文件夹树形关系:

使用组合的安全模式实现下:

@ToString
public abstract class FolderMode {
    private String name;
    private String path;
    private String size;

    public FolderMode(String name, String path, String size) {
        this.name = name;
        this.path = path;
        this.size = size;
    }
}
/**
 * 非叶子文件夹
 */
public class FolderNonLeaf extends FolderMode {
    private List<FolderMode> list=new ArrayList<>();

    public FolderNonLeaf(String name, String path, String size) {
        super(name, path, size);
    }

    public void addFolder(FolderMode folder){
        this.list.add(folder);
    }

    public List<FolderMode> getList(){
        return this.list;
    }
}
public class FolderLeaf extends FolderMode {
    public FolderLeaf(String name, String path, String size) {
        super(name, path, size);
    }
}
public class Client {
    public static void main(String[] args) {
        FolderNonLeaf folderNonLeaf = getFolders();
        showTree(folderNonLeaf);
    }

    private static FolderNonLeaf getFolders() {
        // 第一层根目录
        FolderNonLeaf root = new FolderNonLeaf("文件夹1", "1", "1");
        // 文件夹1的两个子文件夹
        FolderNonLeaf second_A = new FolderNonLeaf("文件夹1-1", "1/1-1", "6");
        FolderNonLeaf second_B = new FolderNonLeaf("文件夹1-2", "1/1-2", "6");
        // second_A的三个子文件夹
        FolderNonLeaf third_second_A_A = new FolderNonLeaf("文件夹1-1-1", "1/1-1/1-1-1", "40");
        FolderNonLeaf third_second_A_B = new FolderNonLeaf("文件夹1-1-2", "1/1-1/1-1-2", "40");
        FolderNonLeaf third_second_A_C = new FolderNonLeaf("文件夹1-1-3", "1/1-1/1-1-3", "30");
        // second_B的两个子文件夹
        FolderNonLeaf third_second_B_A = new FolderNonLeaf("文件夹1-2-1", "1/1-1/1-2-1", "30");
        FolderNonLeaf third_second_B_B = new FolderNonLeaf("文件夹1-2-2", "1/1-1/1-2-2", "30");
        // third_second_A_A子文件夹
        FolderNonLeaf fourth_third_second_A_A_A = new FolderNonLeaf("文件夹1-1-1-1", "1/1-1/1-1-1/1-1-1-1", "60");

        // 开始组装树状族谱
        // 第二层
        root.addFolder(second_A);
        root.addFolder(second_B);
        // 第三层
        second_A.addFolder(third_second_A_A);
        second_A.addFolder(third_second_A_B);
        second_A.addFolder(third_second_A_C);
        second_B.addFolder(third_second_B_A);
        second_B.addFolder(third_second_B_B);
        // 第四层
        third_second_A_A.addFolder(fourth_third_second_A_A_A);
        return root;
    }

    //通过递归遍历树
    private static void showTree(FolderNonLeaf root) {
        System.out.println(root.toString());
        for(FolderMode folder:root.getList()){
            // 叶子节点
            if(folder instanceof FolderLeaf){
                System.out.println(folder.toString());
            }else{
                showTree((FolderNonLeaf) folder);
            }
        }
    }
}
FolderMode(name=文件夹1, path=1, size=1)
FolderMode(name=文件夹1-1, path=1/1-1, size=6)
FolderMode(name=文件夹1-1-1, path=1/1-1/1-1-1, size=40)
FolderMode(name=文件夹1-1-1-1, path=1/1-1/1-1-1/1-1-1-1, size=60)
FolderMode(name=文件夹1-1-2, path=1/1-1/1-1-2, size=40)
FolderMode(name=文件夹1-1-3, path=1/1-1/1-1-3, size=30)
FolderMode(name=文件夹1-2, path=1/1-2, size=6)
FolderMode(name=文件夹1-2-1, path=1/1-1/1-2-1, size=30)
FolderMode(name=文件夹1-2-2, path=1/1-1/1-2-2, size=30)

这里把叶子结点和非叶子节点的共同部分抽出来放在 FolderMode 中。

六、适用场景

优点:

  • 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让高层模块忽略了层次的差异,方便对整个层次结构进行控制。
  • 高层模块可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了高层模块的代码。
  • 在组合模式中增加了新的枝干构件和叶子构件都很方便,无须对现有类库进行任何修改,符合 "开闭原则"。
  • 组合模式为树状结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和树干对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。

缺点:

早新增构件时不好对树干中的构件类型进行限制,不能依赖类型系统来施加这些约束,因为在大多数情况下,它们都来自于想听的抽象层,此时,必须进行类型检查来实现,这个实现过程较为复杂。

适用场景:

  • 只要是树形结构或者只要是要体现局部和整体的关系的时候,而且这种关系还可能比较深,就要考虑一下组合模式。

  • 从一个整体中能够独立出部分模块或功能的场景。

  • 维护和展示部分-整体关系的场景。

 

posted @ 2022-06-25 14:02  沙滩de流沙  阅读(54)  评论(0编辑  收藏  举报

关注「Java视界」公众号,获取更多技术干货