组合模式
大约 10 分钟
组合模式
什么是组合模式
组合模式(Composite Pattern)是一种结构型设计模式,它允许你将对象组合成树形结构来表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
组合模式的核心思想是:
- 将对象组合成树形结构以表示"部分-整体"的层次结构
- 使得用户对单个对象和组合对象的使用具有一致性
- 简化客户端代码,统一处理单个对象和组合对象
为什么需要组合模式
在实际开发中,我们经常遇到具有"部分-整体"层次结构的问题,比如:
- 文件系统(文件和文件夹)
- 图形界面(基本图形和组合图形)
- 组织架构(员工和部门)
- 菜单系统(菜单项和子菜单)
如果使用传统的面向对象方法,我们需要区分处理单个对象和组合对象,这会导致代码复杂且难以维护。组合模式通过统一的接口来处理单个对象和组合对象,简化了客户端代码。
组合模式的结构
组合模式包含以下几个角色:
- 组件(Component):抽象构件角色,为组合中的对象声明接口
- 叶子(Leaf):叶子构件角色,表示叶子对象,没有子节点
- 容器(Composite):容器构件角色,表示容器对象,可以包含子节点
- 客户端(Client):通过组件接口操作组合结构中的对象
组合模式的实现
透明式组合模式
// 抽象组件
abstract class Component {
protected String name;
public Component(String name) {
this.name = name;
}
// 公共方法
public abstract void add(Component component);
public abstract void remove(Component component);
public abstract void display(int depth);
// 工具方法:生成指定深度的缩进
protected String getDepthString(int depth) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < depth; i++) {
sb.append("-");
}
return sb.toString();
}
}
// 叶子构件
class Leaf extends Component {
public Leaf(String name) {
super(name);
}
@Override
public void add(Component component) {
System.out.println("叶子节点不能添加子节点");
}
@Override
public void remove(Component component) {
System.out.println("叶子节点没有子节点可删除");
}
@Override
public void display(int depth) {
System.out.println(getDepthString(depth) + " " + name);
}
}
// 容器构件
class Composite extends Component {
private List<Component> children = new ArrayList<>();
public Composite(String name) {
super(name);
}
@Override
public void add(Component component) {
children.add(component);
}
@Override
public void remove(Component component) {
children.remove(component);
}
@Override
public void display(int depth) {
System.out.println(getDepthString(depth) + "+" + name);
for (Component component : children) {
component.display(depth + 2);
}
}
}
// 使用示例
public class TransparentCompositeDemo {
public static void main(String[] args) {
// 创建根节点
Component root = new Composite("根节点");
// 创建分支节点
Component branch1 = new Composite("分支1");
Component branch2 = new Composite("分支2");
// 创建叶子节点
Component leaf1 = new Leaf("叶子1");
Component leaf2 = new Leaf("叶子2");
Component leaf3 = new Leaf("叶子3");
// 构建树形结构
root.add(branch1);
root.add(branch2);
root.add(leaf1);
branch1.add(leaf2);
branch2.add(leaf3);
// 显示树形结构
root.display(1);
}
}
安全式组合模式
// 抽象组件
interface Component {
void display(int depth);
// 工具方法
default String getDepthString(int depth) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < depth; i++) {
sb.append("-");
}
return sb.toString();
}
}
// 叶子构件
class Leaf implements Component {
private String name;
public Leaf(String name) {
this.name = name;
}
@Override
public void display(int depth) {
System.out.println(getDepthString(depth) + " " + name);
}
}
// 容器构件
class Composite implements Component {
private String name;
private List<Component> children = new ArrayList<>();
public Composite(String name) {
this.name = name;
}
public void add(Component component) {
children.add(component);
}
public void remove(Component component) {
children.remove(component);
}
public List<Component> getChildren() {
return children;
}
@Override
public void display(int depth) {
System.out.println(getDepthString(depth) + "+" + name);
for (Component component : children) {
component.display(depth + 2);
}
}
}
// 使用示例
public class SafeCompositeDemo {
public static void main(String[] args) {
// 创建根节点
Composite root = new Composite("根节点");
// 创建分支节点
Composite branch1 = new Composite("分支1");
Composite branch2 = new Composite("分支2");
// 创建叶子节点
Leaf leaf1 = new Leaf("叶子1");
Leaf leaf2 = new Leaf("叶子2");
Leaf leaf3 = new Leaf("叶子3");
// 构建树形结构
root.add(branch1);
root.add(branch2);
root.add(leaf1);
branch1.add(leaf2);
branch2.add(leaf3);
// 显示树形结构
root.display(1);
}
}
实际应用示例
// 文件系统示例
// 抽象文件组件
abstract class FileSystemComponent {
protected String name;
public FileSystemComponent(String name) {
this.name = name;
}
// 公共方法
public abstract void add(FileSystemComponent component);
public abstract void remove(FileSystemComponent component);
public abstract void display(int depth);
public abstract long getSize();
// 获取名称
public String getName() {
return name;
}
// 工具方法:生成指定深度的缩进
protected String getDepthString(int depth) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < depth; i++) {
sb.append(" ");
}
return sb.toString();
}
}
// 文件(叶子节点)
class File extends FileSystemComponent {
private long size;
public File(String name, long size) {
super(name);
this.size = size;
}
@Override
public void add(FileSystemComponent component) {
System.out.println("文件不能添加子节点");
}
@Override
public void remove(FileSystemComponent component) {
System.out.println("文件没有子节点可删除");
}
@Override
public void display(int depth) {
System.out.println(getDepthString(depth) + name + " (" + size + " bytes)");
}
@Override
public long getSize() {
return size;
}
}
// 文件夹(容器节点)
class Folder extends FileSystemComponent {
private List<FileSystemComponent> children = new ArrayList<>();
public Folder(String name) {
super(name);
}
@Override
public void add(FileSystemComponent component) {
children.add(component);
}
@Override
public void remove(FileSystemComponent component) {
children.remove(component);
}
@Override
public void display(int depth) {
System.out.println(getDepthString(depth) + "[" + name + "]");
for (FileSystemComponent component : children) {
component.display(depth + 1);
}
}
@Override
public long getSize() {
long totalSize = 0;
for (FileSystemComponent component : children) {
totalSize += component.getSize();
}
return totalSize;
}
}
// 使用示例
public class FileSystemDemo {
public static void main(String[] args) {
// 创建文件系统结构
Folder root = new Folder("根目录");
Folder documents = new Folder("文档");
Folder images = new Folder("图片");
Folder videos = new Folder("视频");
File readme = new File("README.txt", 1024);
File config = new File("config.ini", 512);
File photo1 = new File("photo1.jpg", 2048000);
File photo2 = new File("photo2.jpg", 3072000);
File movie1 = new File("movie1.mp4", 1073741824); // 1GB
File movie2 = new File("movie2.mp4", 2147483648L); // 2GB
// 构建文件系统结构
root.add(documents);
root.add(images);
root.add(videos);
root.add(readme);
documents.add(config);
images.add(photo1);
images.add(photo2);
videos.add(movie1);
videos.add(movie2);
// 显示文件系统结构
System.out.println("文件系统结构:");
root.display(0);
System.out.println("\n根目录总大小: " + root.getSize() + " bytes");
System.out.println("文档目录大小: " + documents.getSize() + " bytes");
System.out.println("图片目录大小: " + images.getSize() + " bytes");
System.out.println("视频目录大小: " + videos.getSize() + " bytes");
}
}
组合模式的应用场景
- 表示对象的部分-整体层次结构:当你想表示对象的部分-整体层次结构时
- 希望用户忽略组合对象与单个对象的不同:当你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象时
- 文件系统:文件和文件夹的层次结构
- 图形界面:基本图形和组合图形的层次结构
- 组织架构:员工和部门的层次结构
- 菜单系统:菜单项和子菜单的层次结构
组合模式的优缺点
优点
- 高层模块调用简单:高层模块调用时不必关系是叶子节点还是容器节点,也不必关心具体的实现过程,只负责调用即可
- 节点自由增加:使用组合模式后,如果想增加一个树枝节点、树叶节点都很容易,只要找到它的父节点即可,非常容易扩展,符合开闭原则
- 统一接口:组合模式让客户端能够以一致的方式处理单个对象和组合对象
缺点
- 不易控制节点类型:在透明式的组合模式中,客户端不知道正在处理的是一个叶子节点还是一个容器节点,这可能会导致类型安全性问题
- 设计复杂:设计更加抽象,不容易理解
- 违反依赖倒置原则:在安全式的组合模式中,叶子和容器类都直接依赖于抽象组件,这违反了依赖倒置原则
组合模式与其他模式的比较
与装饰器模式的区别
- 组合模式:关注的是对象的组织结构,将对象组合成树形结构
- 装饰器模式:关注的是动态地给对象添加职责
与访问者模式的区别
- 组合模式:提供了一种组织对象的方式
- 访问者模式:提供了一种操作对象的方式
与迭代器模式的区别
- 组合模式:关注对象的层次结构
- 迭代器模式:关注对象的遍历方式
组合模式的变体
1. 带迭代功能的组合模式
// 带迭代功能的组合模式
class IterableComposite extends FileSystemComponent {
private List<FileSystemComponent> children = new ArrayList<>();
public IterableComposite(String name) {
super(name);
}
@Override
public void add(FileSystemComponent component) {
children.add(component);
}
@Override
public void remove(FileSystemComponent component) {
children.remove(component);
}
@Override
public void display(int depth) {
System.out.println(getDepthString(depth) + "[" + name + "]");
for (FileSystemComponent component : children) {
component.display(depth + 1);
}
}
@Override
public long getSize() {
long totalSize = 0;
for (FileSystemComponent component : children) {
totalSize += component.getSize();
}
return totalSize;
}
// 提供迭代器
public Iterator<FileSystemComponent> iterator() {
return children.iterator();
}
}
2. 带路径功能的组合模式
// 带路径功能的组合模式
abstract class PathComponent {
protected String name;
protected PathComponent parent;
public PathComponent(String name) {
this.name = name;
}
public abstract void add(PathComponent component);
public abstract void remove(PathComponent component);
public abstract void display(int depth);
// 获取完整路径
public String getPath() {
if (parent == null) {
return "/" + name;
} else {
return parent.getPath() + "/" + name;
}
}
// 设置父节点
public void setParent(PathComponent parent) {
this.parent = parent;
}
}
实际项目中的应用
// 菜单系统示例
// 菜单组件抽象类
abstract class MenuComponent {
// 公共方法
public void add(MenuComponent component) {
throw new UnsupportedOperationException();
}
public void remove(MenuComponent component) {
throw new UnsupportedOperationException();
}
public MenuComponent getChild(int i) {
throw new UnsupportedOperationException();
}
public String getName() {
throw new UnsupportedOperationException();
}
public String getDescription() {
throw new UnsupportedOperationException();
}
public double getPrice() {
throw new UnsupportedOperationException();
}
public boolean isVegetarian() {
throw new UnsupportedOperationException();
}
public abstract void print();
}
// 菜单项(叶子节点)
class MenuItem extends MenuComponent {
private String name;
private String description;
private boolean vegetarian;
private double price;
public MenuItem(String name, String description, boolean vegetarian, double price) {
this.name = name;
this.description = description;
this.vegetarian = vegetarian;
this.price = price;
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public double getPrice() {
return price;
}
@Override
public boolean isVegetarian() {
return vegetarian;
}
@Override
public void print() {
System.out.print(" " + getName());
if (isVegetarian()) {
System.out.print("(v)");
}
System.out.println(", $" + getPrice());
System.out.println(" -- " + getDescription());
}
}
// 菜单(容器节点)
class Menu extends MenuComponent {
private List<MenuComponent> menuComponents = new ArrayList<>();
private String name;
private String description;
public Menu(String name, String description) {
this.name = name;
this.description = description;
}
@Override
public void add(MenuComponent component) {
menuComponents.add(component);
}
@Override
public void remove(MenuComponent component) {
menuComponents.remove(component);
}
@Override
public MenuComponent getChild(int i) {
return menuComponents.get(i);
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public void print() {
System.out.print("\n" + getName());
System.out.println(", " + getDescription());
System.out.println("---------------------");
for (MenuComponent component : menuComponents) {
component.print();
}
}
}
// 服务员类
class Waitress {
private MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
allMenus.print();
}
}
// 使用示例
public class MenuDemo {
public static void main(String[] args) {
MenuComponent pancakeHouseMenu = new Menu("煎饼屋菜单", "早餐");
MenuComponent dinerMenu = new Menu("餐厅菜单", "午餐");
MenuComponent cafeMenu = new Menu("咖啡厅菜单", "晚餐");
MenuComponent dessertMenu = new Menu("甜品菜单", "甜品!");
MenuComponent allMenus = new Menu("所有菜单", "所有餐厅的菜单");
allMenus.add(pancakeHouseMenu);
allMenus.add(dinerMenu);
allMenus.add(cafeMenu);
// 添加煎饼屋菜单项
pancakeHouseMenu.add(new MenuItem("K&B煎饼早餐", "薄煎饼配炒蛋和吐司", true, 2.99));
pancakeHouseMenu.add(new MenuItem("常规煎饼早餐", "薄煎饼配炒蛋和香肠", false, 2.99));
pancakeHouseMenu.add(new MenuItem("蓝莓煎饼", "新鲜蓝莓制作的煎饼", true, 3.49));
pancakeHouseMenu.add(new MenuItem("华夫饼", "华夫饼配浆果和冰淇淋", true, 3.59));
// 添加餐厅菜单项
dinerMenu.add(new MenuItem("素食BLT", "(假)培根配生菜和番茄", true, 2.99));
dinerMenu.add(new MenuItem("BLT", "培根配生菜和番茄", false, 2.99));
dinerMenu.add(new MenuItem("汤", "今日汤品,配土豆沙拉", false, 3.29));
dinerMenu.add(new MenuItem("热狗", "热狗配酸菜,德国香肠,芥末酱", false, 3.05));
// 添加甜品菜单
dinerMenu.add(dessertMenu);
dessertMenu.add(new MenuItem("苹果派", "苹果派配香草冰淇淋", true, 1.59));
dessertMenu.add(new MenuItem("芝士蛋糕", "纽约芝士蛋糕配蓝莓", true, 1.99));
dessertMenu.add(new MenuItem("巧克力慕斯", "巧克力慕斯配薄荷", true, 1.79));
// 添加咖啡厅菜单项
cafeMenu.add(new MenuItem("素食汉堡", "全麦面包配生菜番茄和薯条", true, 3.99));
cafeMenu.add(new MenuItem("汉堡", "牛肉汉堡配生菜番茄和薯条", false, 4.99));
cafeMenu.add(new MenuItem("咖啡", "现磨咖啡", true, 1.99));
cafeMenu.add(new MenuItem("茶", "各种茶", true, 1.49));
Waitress waitress = new Waitress(allMenus);
waitress.printMenu();
}
}
总结
组合模式是一种非常实用的结构型设计模式,它通过将对象组合成树形结构来表示"部分-整体"的层次关系,使得客户端可以统一地处理单个对象和组合对象。在实际开发中,当我们需要处理具有层次结构的对象时,组合模式是一个很好的选择。
使用组合模式的关键点:
- 识别出系统中具有"部分-整体"层次结构的对象
- 定义统一的组件接口
- 区分叶子节点和容器节点的实现
- 客户端通过统一接口操作所有对象
组合模式的优点是可以简化客户端代码,统一处理不同层次的对象,但也需要注意设计的复杂性和类型安全性问题。在现代Java开发中,组合模式常用于文件系统、菜单系统、组织架构、图形界面等需要层次结构处理的场景。