zoukankan      html  css  js  c++  java
  • 组合模式

    组合模式

    案例

    我们想开发一个界面控件库,界面控件分为两大类,一类是单元控件,例如按钮、文本框等,一类是容器控件,例如面板。面板界面内可以放入单元控件和其他面板。这样最终得到一个类似窗体的样子。下面就用代码模拟这一过程。

    1.首先定义一个面板类:

    /**
     * 面板内,可以添加按钮、文本框和其他的面板
     */
    public class Panel {
        private String name;
        // 存放面板的容器
        private List<Panel> panelList = new ArrayList<>();
        // 存放按钮的容器
        private List<Button> buttonList = new ArrayList<>();
        // 存放文本框的容器
        private List<TextBox> textBoxList = new ArrayList<>();
    
        public Panel(String name) {
            this.name = name;
        }
    
        // 三个添加面板、按钮和文本框的方法
        public void addPanel(Panel panel) {
            panelList.add(panel);
        }
    
        public void addButton(Button button) {
            buttonList.add(button);
        }
    
        public void addTextBox(TextBox textBox) {
            textBoxList.add(textBox);
        }
    
        // 分别调用展示面板、按钮和文本框的方法
        public void show(String prefix) {
            System.out.println(prefix + "展示面板[" + this.name + "]");
            for (Button button : buttonList) {
                button.show(prefix + "--");
            }
            for (TextBox textBox : textBoxList) {
                textBox.show(prefix + "--");
            }
            for (Panel panel : panelList) {
                panel.show(prefix + "--");
            }
        }
    }
    

    2.定义按钮组件

    /**
     * 按钮组件
     */
    public class Button {
        private String name;
    
        public Button(String name) {
            this.name = name;
        }
    
        public void show(String prefix) {
            System.out.println(prefix + "展示按钮[" + this.name + "]");
        }
    }
    

    3.定义文本框组件

    /**
     * 文本框组件
     */
    public class TextBox {
        private String name;
    
        public TextBox(String name) {
            this.name = name;
        }
    
        public void show(String prefix) {
            System.out.println(prefix + "展示文本框[" + this.name + "]");
        }
    }
    

    4.测试在面板上添加按钮、文本框和其他面板:

    public class Main {
        public static void main(String[] args) {
            // 面板 A
            Panel panelA = new Panel("A");
            // 面板 A 放入了一个按钮
            panelA.addButton(new Button("A-1"));
            // 面板 A 放入了一个文本框
            panelA.addTextBox(new TextBox("A-2"));
            // 面板 A 放入了另一个面板 B
            Panel panelB = new Panel("A-B");
            // 面板 B 放入了另一个按钮
            panelB.addButton(new Button("A-B-1"));
            // 面板 B 放入了另一个文本框
            panelB.addTextBox(new TextBox("A-B-2"));
            panelA.addPanel(panelB);
            // 展示面板 A 的内容
            panelA.show("");
        }
    }
    

    5.测试结果:

    展示面板[A]
    --展示按钮[A-1]
    --展示文本框[A-2]
    --展示面板[A-B]
    ----展示按钮[A-B-1]
    ----展示文本框[A-B-2]
    

    以上代码,就结果的结构来看与上面的要求是满足的。但是这一编码设计不够灵活,可扩展性也很差。比如我们新增一个密码框组件,我们除了需要新增一个类以外,还需要修改现有的代码,在Panel类中增加对其类的列表维护,还要修改show()方法中的内容。而且Panel类的设计由于需要定义多个集合存储不同的类型的成员并对其成员,本身就比较复杂了。下面就通过使用组合模式对这写问题进行改善。

    模式介绍

    组合模式(结构型模式),将对象组合成树形结构以表示“部分-整体”的层次结构,组合模式使得用户对单个对象和组合对象的使用具有一致性。掌握组合模式的重点是要理解清楚 “部分/整体” 还有 ”单个对象“ 与 "组合对象" 的含义。

    从上面的定义中可以看出,组合模式区分出单个对象与组合对象来表示部分与整体的关系。从我们的案例来说,其中 Panel就可以看作是组合对象,而ButtonTextBox类就可以看作是单个对象。

    角色构成:

    • Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。
    • Leaf(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。
    • Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。

    从角色构成上可以看出组合模式,通过引入抽象构件类Component,同时使用容器构件类Composite和叶子构件类Leaf,使得客户端只需针对Component类进行编码。

    UML类图:

    composite

    从图中我们可以看出,组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,使得客户端可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器。

    那么在最开始的案例中,我们的Panel类就可以看作是一个容器类,而ButtonTextBox类就可以看作是叶子节点。在这之前还需要引入一个Component抽象构件类,下面就根据这一思路对代码进行改造。

    代码改造

    1.首先引入Component抽象构件类:

    /**
     * 抽象构件类角色
     */
    public abstract class Component {
        // 添加成员
        public abstract void add(Component c);
        // 不同的实现类实现不同的展示方式
        public abstract void show(String prefix);
    }
    

    2.容器构件类Panel

    /**
     * 容器构件类角色
     */
    public class Panel extends Component {
        private String name;
        private List<Component> list = new ArrayList<>();
    
        public Panel(String name) {
            this.name = name;
        }
    
        @Override
        public void add(Component c) {
            list.add(c);
        }
    
        @Override
        public void show(String prefix) {
            System.out.println(prefix + "展示面板[" + this.name + "]");
            for (Component component : list) {
                component.show(prefix + "--");
            }
        }
    }
    

    3.两个叶子构建类:

    按钮组件:

    /**
     * 叶子构件类:按钮组件
     */
    public class Button extends Component {
        private String name;
    
        public Button(String name) {
            this.name = name;
        }
    
        @Override
        public void add(Component c) {
            // 这里通过抛异常的方式,拒绝添加子构件
            throw new UnsupportedOperationException();
        }
    
        public void show(String prefix) {
            System.out.println(prefix + "展示按钮[" + this.name + "]");
        }
    }
    

    文本框组件:

    /**
     * 叶子构件类:文本框组件
     */
    public class TextBox extends Component {
        private String name;
    
        public TextBox(String name) {
            this.name = name;
        }
    
        @Override
        public void add(Component c) {
            // 这里通过抛异常的方式,拒绝添加子构件
            throw new UnsupportedOperationException();
        }
    
        public void show(String prefix) {
            System.out.println(prefix + "展示文本框[" + this.name + "]");
        }
    }
    

    4.测试类:

    public class Main {
        // 这里我们只用针对抽象类 Component 编程
        public static void main(String[] args) {
            // 面板 A
            Component panelA = new Panel("A");
            // 面板 A 放入了一个按钮
            panelA.add(new Button("A-1"));
            // 面板 A 放入了一个文本框
            panelA.add(new TextBox("A-2"));
            // 面板 A 放入了另一个面板 B
            Component panelB = new Panel("A-B");
            // 面板 B 放入了另一个按钮
            panelB.add(new Button("A-B-1"));
            // 面板 B 放入了另一个文本框
            panelB.add(new TextBox("A-B-2"));
            panelA.add(panelB);
            // 展示面板 A 的内容
            panelA.show("");
        }
    }
    

    5.测试结果:

    展示面板[A]
    --展示按钮[A-1]
    --展示文本框[A-2]
    --展示面板[A-B]
    ----展示按钮[A-B-1]
    ----展示文本框[A-B-2]
    

    测试结果与上面最开始的测试结果是一模一样的,但是我们通过引入了Component抽象类,使得客户端只用针对Component类进行编程。同时我们在添加新的叶子构件,如一个密码框时,只需要继承Component类就可以达到扩展的目的,符合“开闭原则”。

    模式应用

    在我们使用 Java 来开发界面应用时使用到的java.swing.*包下面的类中就存在组合模式的应用。先来看一段简单的创建窗口的代码。

    ​ 1.创建一个窗体:

    public class Main {
        public static void main(String[] args) {
            // 创建 JFrame 实例
            JFrame jf = new JFrame();
            // 设置宽高
            jf.setSize(200, 100);
            // 设置在窗口中间打开
            jf.setLocationRelativeTo(null);
            // 设置默认关闭操作
            jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
    
            // 创建面板,类似于 html 中的 div
            JPanel panel = new JPanel();
    
            // 创建一个输入框
            JTextField textField = new JTextField(8);
            // 添加到面板中
            panel.add(textField);
    
            // 创建一个按钮
            JButton btn = new JButton("提交");
            // 添加到面板中
            panel.add(btn);
    
            // 添加面板到 JFrame 中
            jf.add(panel);
            // 设置界面可见
            jf.setVisible(true);
        }
    }
    

    2.运行结果:

    composite-JFrame

    在窗体上,先创建了一个JPanel面板,然后创建并添加了一个JTextField输入框和一个JButton按钮,最后把面板放入到JFrame中。为什么说这里用到了组合模式呢?下面它们之间的UML类图。

    composite-swing

    从类中可以看到抽象类Component就是组合模式中的抽象构件,JFrameJPanel类作为容器构件角色,而JButtonJTextField类作为叶子构件。这样的使用时容器构件中可以容纳其他容器构件,如代码中的jf.add(panel);。同时也可以在容器构件中添加叶子构件如panel.add(textField);panel.add(btn);

    总结

    1.主要优点

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

    2.主要缺点

    • 在增加新构件时很难对容器中的构件类型进行限制。有时候我们希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件,使用组合模式时,不能依赖类型系统来施加这些约束,因为它们都来自于相同的抽象层,在这种情况下,必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。

    3.适用场景

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

    参考资料

    本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/composite
    转载请说明出处,本篇博客地址:https://www.cnblogs.com/phoegel/p/13955969.html

  • 相关阅读:
    服务器迁移总结
    使用OpenSSL生成证书
    mysql Event、存储过程、表命令
    负载均衡 > 常见问题
    SpringMVC记住密码功能
    spring mvc +cookie+拦截器功能 实现系统自动登陆
    android studio之argument for @notnull parameter 'name'
    jQuery ajax表单提交实现局部刷新
    Spring MVC 中采用注解方式 Action中跳转到另一个Action的写法
    ajax表单提交全路径
  • 原文地址:https://www.cnblogs.com/phoegel/p/13955969.html
Copyright © 2011-2022 走看看