组合模式:透明式与安全式、文件系统设计、HashMap中的组合
组合模式,说白了就是处理「部分-整体」关系的一种设计模式。它让客户端可以用一致的方式对待单个对象和组合对象。嗯,这个定义听起来有点绕,我换个说法——你想想看,在文件系统里,一个文件夹可以包含文件,也可以包含其他文件夹。对用户来说,双击一个文件和双击一个文件夹,操作方式是一样的。这就是组合模式要解决的问题。
为什么需要组合模式?
我在做企业网盘项目时遇到过这样一个场景:我们需要实现一个权限管理系统,用户、部门、角色之间存在着复杂的包含关系。一个部门包含多个用户,一个角色可以包含多个部门,部门下面还有子部门。如果不用组合模式,代码会写成什么样?
// 不用组合模式,判断逻辑散落在各处
if (obj instanceof User) {
// 处理用户
} else if (obj instanceof Department) {
// 处理部门
} else if (obj instanceof Role) {
// 处理角色
}
这种代码,每增加一种类型就要改一堆地方。我当时就重构了,用组合模式统一了处理逻辑。
组合模式的核心结构
组合模式有三个核心角色:
- Component(抽象构件):定义叶子节点和容器节点的公共接口
- Leaf(叶子节点):没有子节点的对象
- Composite(容器节点):包含子节点的对象
关键点:叶子节点和容器节点实现相同的接口,客户端不需要区分它们。
我画了一张图,帮你理清它们的关系:
透明式 vs 安全式
组合模式有两种实现风格,我分别说说它们的区别。
透明式组合模式
透明式把管理子节点的方法(add、remove、getChild)都定义在抽象构件中。叶子节点和容器节点都有这些方法。叶子节点虽然不支持添加子节点,但方法签名还在,调用时会抛出异常。
// 透明式:所有方法都在抽象类中定义
abstract class Component {
protected String name;
public Component(String name) {
this.name = name;
}
// 叶子节点和容器节点都有这些方法
public abstract void operation();
public void add(Component c) {
throw new UnsupportedOperationException();
}
public void remove(Component c) {
throw new UnsupportedOperationException();
}
public Component getChild(int i) {
throw new UnsupportedOperationException();
}
}
class File extends Component {
public File(String name) {
super(name);
}
@Override
public void operation() {
System.out.println("文件:" + name);
}
}
class Folder extends Component {
private List<Component> children = new ArrayList<>();
public Folder(String name) {
super(name);
}
@Override
public void operation() {
System.out.println("文件夹:" + name);
for (Component child : children) {
child.operation();
}
}
@Override
public void add(Component c) {
children.add(c);
}
@Override
public void remove(Component c) {
children.remove(c);
}
@Override
public Component getChild(int i) {
return children.get(i);
}
}
透明式的优点是客户端完全不用区分叶子节点和容器节点,代码写起来很统一。缺点是叶子节点有一些无意义的方法,违反了接口隔离原则。
安全式组合模式
安全式把管理子节点的方法只放在容器节点中。抽象构件只定义公共的业务方法。客户端需要自己判断对象类型。
// 安全式:抽象构件只定义业务方法
abstract class Component {
protected String name;
public Component(String name) {
this.name = name;
}
public abstract void operation();
}
class File extends Component {
public File(String name) {
super(name);
}
@Override
public void operation() {
System.out.println("文件:" + name);
}
}
class Folder extends Component {
private List<Component> children = new ArrayList<>();
public Folder(String name) {
super(name);
}
@Override
public void operation() {
System.out.println("文件夹:" + name);
for (Component child : children) {
child.operation();
}
}
// 管理方法只在容器节点中
public void add(Component c) {
children.add(c);
}
public void remove(Component c) {
children.remove(c);
}
public Component getChild(int i) {
return children.get(i);
}
}
安全式的问题是客户端需要做类型判断。我曾经在一个项目中用了安全式,结果到处都是 if (component instanceof Folder) 这样的代码,维护起来很痛苦。
我个人习惯用透明式。虽然叶子节点有一些空方法,但换来的是客户端代码的简洁和统一。你想想看,如果每个调用处都要判断类型,那组合模式的意义就大打折扣了。
实战:文件系统设计
我们用透明式组合模式来实现一个简单的文件系统。这个例子我当年在面试时用过,面试官挺认可的。
// 客户端代码
public class FileSystemClient {
public static void main(String[] args) {
// 创建文件系统
Folder root = new Folder("根目录");
Folder docs = new Folder("文档");
Folder pics = new Folder("图片");
File readme = new File("README.md");
File design = new File("设计文档.doc");
File photo = new File("照片.jpg");
// 组装树形结构
root.add(docs);
root.add(pics);
docs.add(readme);
docs.add(design);
pics.add(photo);
// 统一操作
root.operation();
// 输出:
// 文件夹:根目录
// 文件夹:文档
// 文件:README.md
// 文件:设计文档.doc
// 文件夹:图片
// 文件:照片.jpg
}
}
这个例子展示了组合模式最核心的价值:对根目录调用 operation(),它会递归遍历所有子节点。不管是文件还是文件夹,调用方式完全一样。
HashMap中的组合模式
说到组合模式在JDK中的应用,HashMap是一个很好的例子。你可能会问:HashMap跟组合模式有什么关系?
HashMap的 putAll 方法就体现了组合模式的思想。一个HashMap可以包含另一个HashMap,对整体Map的操作会递归到子Map上。
// HashMap中的组合模式体现
Map<String, Object> config = new HashMap<>();
Map<String, Object> dbConfig = new HashMap<>();
dbConfig.put("url", "jdbc:mysql://localhost");
dbConfig.put("username", "root");
Map<String, Object> cacheConfig = new HashMap<>();
cacheConfig.put("type", "redis");
cacheConfig.put("host", "127.0.0.1");
config.put("database", dbConfig);
config.put("cache", cacheConfig);
// 统一遍历
printConfig(config, "");
void printConfig(Map<String, Object> map, String prefix) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (entry.getValue() instanceof Map) {
System.out.println(prefix + entry.getKey() + ":");
printConfig((Map<String, Object>) entry.getValue(), prefix + " ");
} else {
System.out.println(prefix + entry.getKey() + " = " + entry.getValue());
}
}
}
嗯,这里要注意,HashMap本身并没有显式实现组合模式。但它的设计思路和组合模式是一致的——一个Map可以包含其他Map,对整体Map的操作会递归处理子Map。这种思想在很多框架中都能看到,比如Spring的配置管理。
避坑指南
我曾经在一个项目中过度使用组合模式,把业务逻辑也塞进了树形结构里。结果代码变得很难调试,因为递归调用太深了。后来我总结了几条经验:
- 不要滥用递归:树深度超过100层时,考虑用栈代替递归
- 注意循环引用:子节点不能包含父节点,否则会死循环
- 区分共享和独立:叶子节点可以被多个容器共享吗?这会影响你的设计
- 性能监控:递归操作要考虑性能,必要时加缓存
组合模式最适合处理树形结构的数据。如果你遇到的是网状结构,那就不太合适了。
组合模式说白了就是「树形结构的统一处理」。它让客户端代码变得简单,但代价是抽象层要承担更多责任。用不用、怎么用,取决于你的具体场景。我个人建议:如果你的业务天然就是树形结构(文件系统、组织架构、菜单树),组合模式是首选。如果只是偶尔用到树形结构,那用简单的递归遍历就够了。
公众号:蓝海资料掘金营,微信deep3321