美文网首页
设计模式之十二——组合模式

设计模式之十二——组合模式

作者: dd299 | 来源:发表于2019-06-30 16:28 被阅读0次

    原文传送门

    1 介绍

    组合模式属于对象的结构模式,有时又叫做“部分——整体”模式。

    1.1 什么是组合模式

    将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

    1.2 解决了什么问题

    它在我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。

    2 原理

    合成模式的实现根据所实现接口的区别分为两种形式,分别称为安全式和透明式。

    2.1 安全式合成模式的结构

    涉及到三个角色:

    • Component:抽象构件角色。这是一个抽象角色,它给参加组合的对象定义出公共的接口及其默认行为,可以用来管理所有的子对象。合成对象通常把它所包含的子对象当做类型为Component的对象。在安全式的合成模式里,构件角色并不定义出管理子对象的方法,这一定义由树枝构件对象给出。
    • Leaf:树叶构件角色。树叶对象是没有下级子对象的对象,定义出参加组合的原始对象的行为。
    • Composite:树枝构件角色。代表参加组合的有下级子对象的对象。树枝构件类给出所有的管理子对象的方法,如add()、remove()以及getChild()。
    2.1.1 uml图
    安全模式的合成模式

    安全模式的合成模式要求管理聚集的方法只出现在树枝构件类中,而不出现在树叶构件类中。

    2.1.2 代码示例

    Component代码示例

    
    public interface Component {
        /**
         * 输出组件自身的名称
         */
        public void printStruct(String preStr);
    }
    

    Leaf代码示例

    
    public class Leaf implements Component {
        /**
         * 叶子对象的名字
         */
        private String name;
        /**
         * 构造方法,传入叶子对象的名称
         * @param name 叶子对象的名字
         */
        public Leaf(String name){
            this.name = name;
        }
        /**
         * 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
         * @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
         */
        @Override
        public void printStruct(String preStr) {
            // TODO Auto-generated method stub
            System.out.println(preStr + "-" + name);
        }
    
    }
    

    Composite代码示例

    
    public class Composite implements Component {
        /**
         * 用来存储组合对象中包含的子组件对象
         */
        private List<Component> childComponents = new ArrayList<Component>();
        /**
         * 组合对象的名字
         */
        private String name;
        /**
         * 构造方法,传入组合对象的名字
         * @param name    组合对象的名字
         */
        public Composite(String name){
            this.name = name;
        }
        /**
         * 聚集管理方法,增加一个子构件对象
         * @param child 子构件对象
         */
        public void addChild(Component child){
            childComponents.add(child);
        }
        /**
         * 聚集管理方法,删除一个子构件对象
         * @param index 子构件对象的下标
         */
        public void removeChild(int index){
            childComponents.remove(index);
        }
        /**
         * 聚集管理方法,返回所有子构件对象
         */
        public List<Component> getChild(){
            return childComponents;
        }
        /**
         * 输出对象的自身结构
         * @param preStr 前缀,主要是按照层级拼接空格,实现向后缩进
         */
        @Override
        public void printStruct(String preStr) {
            // 先把自己输出
            System.out.println(preStr + "+" + this.name);
            //如果还包含有子组件,那么就输出这些子组件对象
            if(this.childComponents != null){
                //添加两个空格,表示向后缩进两个空格
                preStr += "  ";
                //输出当前对象的子对象
                for(Component c : childComponents){
                    //递归输出每个子对象
                    c.printStruct(preStr);
                }
            }
            
        }
    
    }
    

    调用示例

    
    public class Client {
        public static void main(String[]args){
            Composite root = new Composite("服装");
            Composite c1 = new Composite("男装");
            Composite c2 = new Composite("女装");
            
            Leaf leaf1 = new Leaf("衬衫");
            Leaf leaf2 = new Leaf("夹克");
            Leaf leaf3 = new Leaf("裙子");
            Leaf leaf4 = new Leaf("套装");
            
            root.addChild(c1);
            root.addChild(c2);
            c1.addChild(leaf1);
            c1.addChild(leaf2);
            c2.addChild(leaf3);
            c2.addChild(leaf4);
            
            root.printStruct("");
        }
    }
    

    运行结果

    
    
    2.1.3 优缺点
    • 优点:可以看出,树枝构件类(Composite)给出了addChild()、removeChild()以及getChild()等方法的声明和实现,而树叶构件类则没有给出这些方法的声明或实现。这样的做法是安全的做法,由于这个特点,客户端应用程序不可能错误地调用树叶构件的聚集方法,因为树叶构件没有这些方法,调用会导致编译错误。

    • 安全式合成模式的缺点是不够透明,因为树叶类和树枝类将具有不同的接口。

    2.2 透明式合成模式的结构

    与安全式的合成模式不同的是,透明式的合成模式要求所有的具体构件类,不论树枝构件还是树叶构件,均符合一个固定接口。

    2.2.1 uml图
    透明式的合成模式
    2.2.2 代码示例

    Component代码示例

    
    public abstract class Component {
        /**
         * 输出组建自身的名称
         */
        public abstract void printStruct(String preStr);
        /**
         * 聚集管理方法,增加一个子构件对象
         * @param child 子构件对象
         */
        public void addChild(Component child){
            /**
             * 缺省实现,抛出异常,因为叶子对象没有此功能
             * 或者子组件没有实现这个功能
             */
            throw new UnsupportedOperationException("对象不支持此功能");
        }
        /**
         * 聚集管理方法,删除一个子构件对象
         * @param index 子构件对象的下标
         */
        public void removeChild(int index){
            /**
             * 缺省实现,抛出异常,因为叶子对象没有此功能
             * 或者子组件没有实现这个功能
             */
            throw new UnsupportedOperationException("对象不支持此功能");
        }
        
        /**
         * 聚集管理方法,返回所有子构件对象
         */
        public List<Component> getChild(){
            /**
             * 缺省实现,抛出异常,因为叶子对象没有此功能
             * 或者子组件没有实现这个功能
             */
            throw new UnsupportedOperationException("对象不支持此功能");
        }
    }
    

    Composite代码示例

    
    public class Composite extends Component {
        /**
         * 用来存储组合对象中包含的子组件对象
         */
        private List<Component> childComponents = new ArrayList<Component>();
        /**
         * 组合对象的名字
         */
        private String name;
        /**
         * 构造方法,传入组合对象的名字
         * @param name    组合对象的名字
         */
        public Composite(String name){
            this.name = name;
        }
        /**
         * 聚集管理方法,增加一个子构件对象
         * @param child 子构件对象
         */
        public void addChild(Component child){
            childComponents.add(child);
        }
        /**
         * 聚集管理方法,删除一个子构件对象
         * @param index 子构件对象的下标
         */
        public void removeChild(int index){
            childComponents.remove(index);
        }
        /**
         * 聚集管理方法,返回所有子构件对象
         */
        public List<Component> getChild(){
            return childComponents;
        }
        /**
         * 输出对象的自身结构
         * @param preStr 前缀,主要是按照层级拼接空格,实现向后缩进
         */
        @Override
        public void printStruct(String preStr) {
            // 先把自己输出
            System.out.println(preStr + "+" + this.name);
            //如果还包含有子组件,那么就输出这些子组件对象
            if(this.childComponents != null){
                //添加两个空格,表示向后缩进两个空格
                preStr += "  ";
                //输出当前对象的子对象
                for(Component c : childComponents){
                    //递归输出每个子对象
                    c.printStruct(preStr);
                }
            }
            
        }
    
    }
    

    Leaf代码示例

    
    public class Leaf extends Component {
        /**
         * 叶子对象的名字
         */
        private String name;
        /**
         * 构造方法,传入叶子对象的名称
         * @param name 叶子对象的名字
         */
        public Leaf(String name){
            this.name = name;
        }
        /**
         * 输出叶子对象的结构,叶子对象没有子对象,也就是输出叶子对象的名字
         * @param preStr 前缀,主要是按照层级拼接的空格,实现向后缩进
         */
        @Override
        public void printStruct(String preStr) {
            // TODO Auto-generated method stub
            System.out.println(preStr + "-" + name);
        }
    
    }
    

    调用示例

    
    public class Client {
        public static void main(String[]args){
            Component root = new Composite("服装");
            Component c1 = new Composite("男装");
            Component c2 = new Composite("女装");
            
            Component leaf1 = new Leaf("衬衫");
            Component leaf2 = new Leaf("夹克");
            Component leaf3 = new Leaf("裙子");
            Component leaf4 = new Leaf("套装");
            
            root.addChild(c1);
            root.addChild(c2);
            c1.addChild(leaf1);
            c1.addChild(leaf2);
            c2.addChild(leaf3);
            c2.addChild(leaf4);
            
            root.printStruct("");
        }
    }
    

    运行结果

    
    
    2.2.3 优缺点
    • 优点:可以看出,客户端无需再区分操作的是树枝对象(Composite)还是树叶对象(Leaf)了;对于客户端而言,操作的都是Component对象。

    2.3 两种实现方法的选择

    这里所说的安全性合成模式是指:从客户端使用合成模式上看是否更安全,如果是安全的,那么就不会有发生误操作的可能,能访问的方法都是被支持的。

    这里所说的透明性合成模式是指:从客户端使用合成模式上,是否需要区分到底是“树枝对象”还是“树叶对象”。如果是透明的,那就不用区分,对于客户而言,都是Compoent对象,具体的类型对于客户端而言是透明的,是无须关心的。

    对于合成模式而言,在安全性和透明性上,会更看重透明性,毕竟合成模式的目的是:让客户端不再区分操作的是树枝对象还是树叶对象,而是以一个统一的方式来操作。

    而且对于安全性的实现,需要区分是树枝对象还是树叶对象。有时候,需要将对象进行类型转换,却发现类型信息丢失了,只好强行转换,这种类型转换必然是不够安全的。

    因此在使用合成模式的时候,建议多采用透明性的实现方式。

    2.4 优缺点

    • 组合模式的主要优点如下:

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

      • 使得设计更加复杂,客户端需要花更多时间理清类之间的层次关系。
      • 在增加新构件时很难对容器中的构件类型进行限制。

    3 适用场景

    1. 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。
    2. 在一个使用面向对象语言开发的系统中需要处理一个树形结构。
    3. 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。

    4 总结

    组合对象的关键在于它定义了一个抽象构建类,它既可表示叶子对象,也可表示容器对象,客户仅仅需要针对这个抽象构建进行编程,无须知道他是叶子对象还是容器对象,都是一致对待。

    组合模式虽然能够非常好地处理层次结构,也使得客户端程序变得简单,但是它也使得设计变得更加抽象,而且也很难对容器中的构件类型进行限制,这会导致在增加新的构件时会产生一些问题。


    参考书籍及文章
    1.《Java与模式》,电子工业出版社,阎宏

    1. 《大话设计模式》,清华大学出版社,程杰
    2. 《设计模式——可复用面向对象软件的基础》,机械工业出版社,Erich Gamma,Richard Helm,Ralph Johnson,John Vlissides
    3. 《Head First 设计模式(中文版)》,中国电力出版社
    4. 《图说设计模式》,https://design-patterns.readthedocs.io/zh_CN/latest/index.html
    5. 《设计模式 | 组合模式及典型应用
      》,https://juejin.im/post/5bb730e96fb9a05d0c37e66b

    相关文章

      网友评论

          本文标题:设计模式之十二——组合模式

          本文链接:https://www.haomeiwen.com/subject/euuucctx.html