07. 23种经典设计模式-21-组合模式

1. 组合模式

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

1.1 核心思想

  • 将整体和部分合二为一, 整合成树形结构, 用同一个对象来表示整体和部分的关系
  • 整体和部分操作方式一致, 可进行递归操作

1.2 组合模式类图

  • AbsComponent: 节点操作抽象类. 用于声明组件的公用方法和子组件的管理方法, 也可给予默认实现。
  • Leaf:叶子节点, 没有子节点. 无须重写子组件管理的相关方法
  • Composite: 非叶子节点,重写子组件管理的相关方法

1.3 适用场景

整体与部分关系能抽象为树形结构关系的场景, 如树形菜单, 文件系统管理等.

2. Java 实现组合模式

Linux 系统的文件结构就是一个树状结构,我们可以使用组合模式实现一下.我们首先分析一下linux的文件特点:

  • 文件可以抽象为两种节点: 目录节点(非叶子节点)和文件节点(叶子节点).
  • 根节点为/, 是目录节点为null的特殊情况
  • 任意非根节点, 名称不能为空, 且都拥有一个父目录
  • 对于目录节点,可进行子节点管理, 因此需要重写子节点管理相关方法
  • 对于文件节点, 不可进行子节点管理

2.1 类图分析

  • AbsFile: AbsComponent角色,
    • 对于无须子类重写的方法,笔者设置了final修饰符, 防止子类窜改
    • 对于需要非叶子节点重写的方法, 笔者给予了默认实现.
    • 对于子节点管理API, 笔者采取了链式编程风格进行设计
  • MyFile: 叶子节点, 无须重写任何方法.
  • MyDirectory: 非叶子节点, 需要实现子节点相关管理节点.

单例模式类图

2.2 抽象类-AbsFile

  • 笔者将子类不应该重写的方法使用final修饰, 以防止子类篡改实现
  • 笔者将子节点管理的相关方法给予了默认实现, 但是抛出了异常. 这样既能保证叶子节点无须重写方法, 也能保证叶子节点不能调用此方法
  • 笔者采用链式编程风格设计了子节点管理方法, 这样在使用时更为便捷

public abstract class AbsFile{

    private String name;

    private AbsFile parent ;

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

    // 子类不能重写的方法
    public final void setParent(AbsFile parent) {
        this.parent = parent;
    }

    public final AbsFile getParent() {
        return parent;
    }

    public final String getName() {
        return name;
    }

    public final String getPath(){
        String path = "";

        // 如果节点父节点存在,则先获取父节点路径
        if(getParent() != null) path += getParent().getPath();

        // 如果父节点为根节点, 则不拼接"/"
        if(!"/".equals(path)) path += "/";

        // 如果不是根节点, 则拼接名称. 只允许根节点名称为null
        if(this.name != null) path +=name;
        return path;
    }

    // 需要子类实现的方法
    public AbsFile add(AbsFile... absFile) {
        throw new UnsupportedOperationException();
    }

    public AbsFile remove(AbsFile... absFile) {
        throw new UnsupportedOperationException();
    }

    public AbsFile getChild(int index) {
        throw new UnsupportedOperationException();
    }

    public List<AbsFile> getChildren() {
        throw new UnsupportedOperationException();
    }

    // 重写hash和equals 方法
    @Override
    public boolean equals(Object o) {
        return Objects.equals(getPath(),((AbsFile)o).getPath());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getPath());
    }
}

2.3 目录节点-MyDirectory

目录节点需要重新实现抽象类AbsFile中定义的子节点管理方法.

public class MyDirectory extends AbsFile {

    // 存储子节点
    private List<AbsFile> children = new ArrayList<AbsFile>();

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

    @Override
    public AbsFile add(AbsFile... absFiles) {
        for (AbsFile absFile : absFiles) {
            // 判断非根目录节点不能为空
            if (null == absFile.getName() || "".equals(absFile.getName().trim())) {
                throw new UnsupportedOperationException("不支持非根节点名称为空");
            }

            // 设置父节点
            absFile.setParent(this);

            // 添加节点
            children.add(absFile);
        }
        return this;
    }

    @Override
    public AbsFile remove(AbsFile... absFiles) {
        for (AbsFile absFile : absFiles) {
            children.remove(absFile);
        }
        return this;
    }

    @Override
    public AbsFile getChild(int index) {
        return children.get(index);
    }

    @Override
    public List<AbsFile> getChildren() {
        return this.children;
    }

}

2.4 文件节点-MyFile

叶子节点, 无须重新实现任何接口.

public class MyFile extends AbsFile{

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

2.5 测试用例

遍历输出节点及其所有子节点信息时,采用递归.

public class TestComposite {

    /** 构建目录树 */
    private static AbsFile createTree() {

        // 创建根目录:/
        AbsFile rootDir = new MyDirectory(null);

        // 创建家目录:/home
        AbsFile homeDir = new MyDirectory("home");

        // 创建用户目录: /home/zongf
        AbsFile userDir = new MyDirectory("zongf");

        // 创建视频目录: /home/zongf/videos
        AbsFile videoDir = new MyDirectory("videos");
        // 创建文档目录: /home/zongf/documents
        AbsFile documentDir = new MyDirectory("documents");
        // 创建下载目录: /home/zongf/download
        AbsFile downloadDir = new MyDirectory("downloads");

        // 添加文件
        videoDir.add(new MyFile("video1.avi"))
                .add(new MyFile("video2.avi"))
                .add(new MyFile("video3.avi"));

        documentDir.add(new MyFile("word1.doc"))
                   .add(new MyFile("word2.doc"))
                   .add(new MyFile("word3.doc"));

        downloadDir.add(new MyFile("word1.doc"))
                .add(new MyFile("word2.doc"))
                .add(new MyFile("word3.doc"));

        // 维护目录关系
        userDir.add(videoDir, documentDir, downloadDir);
        rootDir.add(homeDir.add(userDir));

        return rootDir;
    }

    // 递归遍历输出文件
    private void printAbsFile(AbsFile absFile, String prefix) {

        // 输出目录
        System.out.println(prefix + absFile.getPath());

        // 遍历子节点
        if (absFile instanceof MyDirectory) {
            for (AbsFile child : absFile.getChildren()) {
                printAbsFile(child, "  " + prefix);
            }
        }
    }

    /** 测试输出节点及其子节点信息 */
    @Test
    public void test_print(){
        AbsFile fileTree = createTree();
        printAbsFile(fileTree, "");
    }

    /** 测试删除 */
    @Test
    public void test_remove(){
        AbsFile fileTree = createTree();

        // 获取目录: /home/zongf
        AbsFile user = fileTree.getChild(0).getChild(0);

        // 获取videos 和 documents
        AbsFile videos = user.getChild(0);
        AbsFile documents = user.getChild(1);

        // 删除videos 和 documents
        user.remove(videos, documents);

        printAbsFile(user, "");
    }

    // 测试文件是否相同
    @Test
    public void testEquals() {
        AbsFile dir = new MyDirectory("aaa");
        MyFile file = new MyFile("aaa");

        Assert.assertEquals(true, dir.equals(file));

        // 建立目录关系之后,文件便不再相同
        dir.add(file);

        Assert.assertEquals(false, dir.equals(file));
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值