组合模式

组合模式

案例

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

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

posted @ 2020-11-10 20:55  Phoegel  阅读(121)  评论(0编辑  收藏  举报