组合模式
案例
我们想开发一个界面控件库,界面控件分为两大类,一类是单元控件,例如按钮、文本框等,一类是容器控件,例如面板。面板界面内可以放入单元控件和其他面板。这样最终得到一个类似窗体的样子。下面就用代码模拟这一过程。
1.首先定义一个面板类:
/**
* 面板内,可以添加按钮、文本框和其他的面板
*/
public class Panel {
private String name;
// 存放面板的容器
private List panelList = new ArrayList<>();
// 存放按钮的容器
private List
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
就可以看作是组合对象,而Button
和TextBox
类就可以看作是单个对象。
角色构成:
- Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。
- Leaf(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。
- Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。
从角色构成上可以看出组合模式,通过引入抽象构件类Component
,同时使用容器构件类Composite
和叶子构件类Leaf
,使得客户端只需针对Component
类进行编码。
UML类图:
从图中我们可以看出,组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,使得客户端可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器。
那么在最开始的案例中,我们的Panel
类就可以看作是一个容器类,而Button
和TextBox
类就可以看作是叶子节点。在这之前还需要引入一个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 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.运行结果:
在窗体上,先创建了一个JPanel
面板,然后创建并添加了一个JTextField
输入框和一个JButton
按钮,最后把面板放入到JFrame
中。为什么说这里用到了组合模式呢?下面它们之间的UML类图。
从类中可以看到抽象类Component
就是组合模式中的抽象构件,JFrame
和JPanel
类作为容器构件角色,而JButton
和JTextField
类作为叶子构件。这样的使用时容器构件中可以容纳其他容器构件,如代码中的jf.add(panel);
。同时也可以在容器构件中添加叶子构件如panel.add(textField);
和panel.add(btn);
。
总结
1.主要优点
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
- 在组合模式中增加新的容器构件和叶子构件都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子对象和容器对象的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
2.主要缺点
- 在增加新构件时很难对容器中的构件类型进行限制。有时候我们希望一个容器中只能有某些特定类型的对象,例如在某个文件夹中只能包含文本文件,使用组合模式时,不能依赖类型系统来施加这些约束,因为它们都来自于相同的抽象层,在这种情况下,必须通过在运行时进行类型检查来实现,这个实现过程较为复杂。
3.适用场景
- 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们。
- 在一个使用面向对象语言开发的系统中需要处理一个树形结构。
- 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型。
参考资料
- 大话设计模式
- 设计模式Java版本-刘伟
- 设计模式|组合模式及经典应用
本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/composite
转载请说明出处,本篇博客地址:https://www.jianshu.com/p/cc5a931b8771