组合模式属于结构型模式,又可以叫做部分-整体模式,主要解决客户程序在具有整体和部分的层次结构中,处理一组相似对象比处理单一对象费时费力的问题。例如,一个图形,它可以是一个简单的圆形、方形或一条线(部分),同时它也可以是有圆形、方形和直线组合而成的复杂图形(整体),而实现这些简单图形相比复杂图形肯定一个简单一个复杂。此时客户端程序不干了,既然复杂图形是由多个简单图形构成,其本质都是一样的,那为什么不能像操作简单图形一样操作复杂图形,这就需要使用组合模式了。
文章目录
- 组合模式的介绍
- 优点
- 缺点
- 应用场景
- 组合模式的使用
- 类图
- 组合模式中存在三个角色
- 组合模式的封装(透明)实现方法
- 第一步,编写抽象构件角色
- 第二步,编写树叶构件角色
- 第三步,编写树枝构件角色
- 第四步,编写测试类测试
- 组合模式的安全实现方法
- 第一步,修改抽象构件角色
- 第二步,修改树叶构件角色
- 第三步,修改树枝构件角色
- 第四步,修改测试类测试
组合模式的介绍
组合模式通过将一组相似的简单对象按照树形结构组合成一个单一的复杂对象,用以表示“部分-整体”的层次结构,使得客户程序在调用组合对象和单一对象时具有一致性。此时客户程序可以忽略组合对象与单一对象的不同,能够像处理单一对象一样处理组合对象。
组合模式规定简单对象和组合对象都需继承一个相同的父类,此时将父类看作根节点,一个个不能拆分的简单对象是叶子节点,而组合对象就是其他子节点。这样就完成了一个清晰的树形组合。
优点
- 让客户程序忽略层次的差异
- 客户程序与复杂对象的内部结构解耦
- 通过递归可以方便的对整个层次结构进行控制
- 新增一个简单对象和一个组合对象并不用改动此结构中的其他代码,符合开闭原则
缺点
- 因为子节点继承的是实现类,不是接口,违反了依赖倒置原则
应用场景
- 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异
- 使用部分-整体的场景,如树形菜单,文件、文件夹的管理
- Java AWT/Swing 中的简单组件
JTextComponent
有子类JTextField
、JTextArea
,容器组件Container
也有子类Window
、Panel
- 公司存在多个部门,同时公司下属的分公司中也存在多个部门,而分公司下属的办事处也存在相同部门
组合模式的使用
假设我现在要设计一套UI界面,而UI界面是由多个组件构成的,我现在要准备一些简单的组件:按钮组件、文本组件、输入框组价,以及用这些简单组件组合的登录框组件。
类图
组合模式中存在三个角色
- 抽象构件(Component)角色:它是一个抽象类,上面实现中的抽象组件类充当这个角色,它给参加组合的对象定义出了公共的接口及默认行为,可以用来管理所有的子对象(在封装下的组合模式中是这样的)。在安全式的组合模式里,构件角色并不定义出管理子对象的方法,这一定义由树枝结构对象给出(也就是登录框组件)。
- 树叶构件(Leaf)角色:这是一个基本的组件类,是树型结构中的叶子节点,上面实现中的按钮组件、文本域组件和输入框组件充当这个角色,主要实现参加组合的原始对象的基本行为。
- 树枝构件(Composite)角色:代表参加组合的有下级子对象的对象,上面实现中的登录框组件充当这个角色,树枝对象给出所有管理子对象的方法实现,如添加子组件、删除子组件等等
组合模式的封装(透明)实现方法
在此方式中,由于抽象构件声明所有子类中的全部方法,所以客户程序没必要区分简单组件和组合组件,这对客户程序来说是透明的、看不见的。但缺点是简单组件本身不具备增加、删除子组件等管理方法,但现在必须却要实现它们,这样会带来一些安全性问题。
第一步,编写抽象构件角色
抽象组件
package 设计模式.组件模式;
/**
* 组件的抽象类,抽象构件角色
*/
public abstract class 抽象组件 {
public String name; // 组件名称
public 抽象组件(String name) {
this.name = name;
}
public abstract void 绘制();
public abstract void 添加(抽象组件 组件);
public abstract void 删除(抽象组件 组件);
}
第二步,编写树叶构件角色
按钮组件
package 设计模式.组件模式;
/**
* 简单组件
*/
public class 按钮组件 extends 抽象组件 {
public 按钮组件(String name) {
super(name);
}
@Override
public void 绘制() {
// 重写父类抽象方法
System.out.println("名为“" + name + "”的【按钮组件】绘制完成");
}
@Override
public void 添加(抽象组件 组件) {
// 因为简单组件无法添加或移除其他组件,所以简单组件的添加和删除方法没有任何意义
System.out.println("基础组件不支持添加!");
}
@Override
public void 删除(抽象组件 组件) {
// 因为简单组件无法添加或移除其他组件,所以简单组件的添加和删除方法没有任何意义
System.out.println("基础组件不支持删除!");
}
}
文本域组件
package 设计模式.组件模式;
/**
* 简单组件
*/
public class 文本域组件 extends 抽象组件 {
public 文本域组件(String name) {
super(name);
}
@Override
public void 绘制() {
// 重写父类抽象方法
System.out.println("名为“" + name + "”的【文本域组件】绘制完成");
}
@Override
public void 添加(抽象组件 组件) {
System.out.println("基础组件不支持添加!");
}
@Override
public void 删除(抽象组件 组件) {
System.out.println("基础组件不支持删除!");
}
}
输入框组件
package 设计模式.组件模式;
/**
* 简单组件
*/
public class 输入框组件 extends 抽象组件 {
public 输入框组件(String name) {
super(name);
}
@Override
public void 绘制() {
// 重写父类抽象方法
System.out.println("名为“" + name + "”的【输入框组件】绘制完成");
}
@Override
public void 添加(抽象组件 组件) {
System.out.println("基础组件不支持添加!");
}
@Override
public void 删除(抽象组件 组件) {
System.out.println("基础组件不支持删除!");
}
}
第三步,编写树枝构件角色
登录框组件
package 设计模式.组件模式;
import java.util.ArrayList;
import java.util.List;
/**
* 组合组件,由多个简单组件组成
*/
public class 登录框聚合组件 extends 抽象组件 {
public List<抽象组件> 子组件列表 = new ArrayList<>();
public 登录框聚合组件(String name) {
super(name);
}
@Override
public void 绘制() {
// 编写复制的绘制方法,无需让客户程序(测试类)再编写
System.out.println("名为" + name + "的【登录框组件】开始绘制:");
for (抽象组件 子组件 : 子组件列表) {
System.out.print("\t├---"); // 做一点显示的加工
子组件.绘制();
}
System.out.println(name + "绘制完成!");
}
@Override
public void 添加(抽象组件 组件) {
// 添加子组件
子组件列表.add(组件);
}
@Override
public void 删除(抽象组件 组件) {
// 删除不需要的子组件
子组件列表.remove(组件);
}
}
第四步,编写测试类测试
测试类
package 设计模式.组件模式;
public class 测试类 {
public static void main(String[] args) {
// 绘制两个按钮
new 按钮组件("一号按钮").绘制();
new 按钮组件("二号按钮").绘制();
// 绘制一个输入框和文本域
System.out.println();
new 输入框组件("一号输入框").绘制();
new 文本域组件("测试文本").绘制();
// 绘制一个登陆框
System.out.println();
// 在透明方式下,客户程序无序知道是简单组件还是组合组件,直接用抽象组件接收就行
抽象组件 条令条例文本域 = new 文本域组件("条令条例文本域");
抽象组件 登录框 = new 登录框聚合组件("一号登录框");
登录框.添加(new 按钮组件("登陆按钮"));
登录框.添加(new 按钮组件("忘记密码按钮"));
登录框.添加(new 输入框组件("账号输入框"));
登录框.添加(new 输入框组件("密码输入框"));
登录框.添加(条令条例文本域);
// 从登录框中删除了文本组件
登录框.删除(条令条例文本域);
登录框.绘制();
}
}
测试结果
名为“一号按钮”的【按钮组件】绘制完成
名为“二号按钮”的【按钮组件】绘制完成
名为“一号输入框”的【输入框组件】绘制完成
名为“测试文本”的【文本域组件】绘制完成
名为一号登录框的【登录框组件】开始绘制:
├---名为“登陆按钮”的【按钮组件】绘制完成
├---名为“忘记密码按钮”的【按钮组件】绘制完成
├---名为“账号输入框”的【输入框组件】绘制完成
├---名为“密码输入框”的【输入框组件】绘制完成
一号登录框绘制完成!
Process finished with exit code 0
组合模式的安全实现方法
在安全方式中,抽象构件将管理子构件的方法移到树枝构件中,抽象构件和树叶构件不再持有对子对象的管理方法,这样就避免了上一种方式的安全性问题,但由于叶子和分支有不同的接口,客户程序在调用时就必须要知道树叶对象和树枝对象的存在,这样也就失去了透明性。
第一步,修改抽象构件角色
仅仅去掉了无用的添加删除方法
抽象组件
package 设计模式.组件模式;
/**
* 组件的抽象类,抽象构件角色
*/
public abstract class 抽象组件 {
public String name; // 组件名称
public 抽象组件(String name) {
this.name = name;
}
public abstract void 绘制();
}
第二步,修改树叶构件角色
仅仅去掉了无用的添加删除方法
按钮组件
package 设计模式.组件模式;
/**
* 简单组件
*/
public class 按钮组件 extends 抽象组件 {
public 按钮组件(String name) {
super(name);
}
@Override
public void 绘制() {
// 重写父类抽象方法
System.out.println("名为“" + name + "”的【按钮组件】绘制完成");
}
}
文本域组件
package 设计模式.组件模式;
/**
* 简单组件
*/
public class 文本域组件 extends 抽象组件 {
public 文本域组件(String name) {
super(name);
}
@Override
public void 绘制() {
// 重写父类抽象方法
System.out.println("名为“" + name + "”的【文本域组件】绘制完成");
}
}
输入框组件
package 设计模式.组件模式;
/**
* 简单组件
*/
public class 输入框组件 extends 抽象组件 {
public 输入框组件(String name) {
super(name);
}
@Override
public void 绘制() {
// 重写父类抽象方法
System.out.println("名为“" + name + "”的【输入框组件】绘制完成");
}
}
第三步,修改树枝构件角色
登录框组件
无需变化,和上面的一样,只需要去掉@Override
注解
第四步,修改测试类测试
仅仅修改两行代码:
文本域组件 条令条例文本域 = new 文本域组件("条令条例文本域");
登录框聚合组件 登录框 = new 登录框聚合组件("一号登录框");
测试类
package 设计模式.组件模式;
public class 测试类 {
public static void main(String[] args) {
new 按钮组件("一号按钮").绘制();
new 按钮组件("二号按钮").绘制();
// 绘制一个输入框和文本域
System.out.println();
new 输入框组件("一号输入框").绘制();
new 文本域组件("测试文本").绘制();
// 绘制一个登陆框
System.out.println();
// 在安全方式下,客户程序需要知道一个组件是简单组件还是组合组件
文本域组件 条令条例文本域 = new 文本域组件("条令条例文本域");
登录框聚合组件 登录框 = new 登录框聚合组件("一号登录框");
登录框.添加(new 按钮组件("登陆按钮"));
登录框.添加(new 按钮组件("忘记密码按钮"));
登录框.添加(new 输入框组件("账号输入框"));
登录框.添加(new 输入框组件("密码输入框"));
登录框.添加(条令条例文本域);
// 从登录框中删除了文本组件
登录框.删除(条令条例文本域);
登录框.绘制();
}
}
测试结果
和之前的实现方法中的一样