跳到主要内容

组合模式(Composite Pattern)

序言

组合模式是一种结构型设计模式,用于将对象组合成树形结构以表示"部分-整体"的层次关系。这种模式使得客户端对单个对象和组合对象的使用具有一致性,从而无需关心处理的是单个对象还是整个对象树。组合模式常常用于处理树形结构的数据,例如文件系统、HTML文档结构,XML文档结构、组织架构等。通过使用组合模式,可以简化对复杂结构的操作,同时也提高了代码的可扩展性和可维护性。

定义

Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients
treat individual objects and compositions of objects uniformly.

将对象组合成树结构以表示部分整体层次结构。 组合可以使客户统一对待单个对象和组合对象。

结构

以HTML文档结构为例,HTML文档结构是一种典型的树形结构,无论我们在哪一层获取 outerHTML 得到的都是,当前节点及其所有下级节点的文档内容。在下面的例子中忽略了HTML文档元素中的各种属性处理。

代码实现

public interface HTMLElement {

String getOuterHTML();
}

public abstract class AbstractHTMLElement implements HTMLElement {

@Getter
private final String tagName;

protected AbstractHTMLElement(String tagName) {
this.tagName = tagName;
}
}

public abstract class HTMLElementComposite extends AbstractHTMLElement {

private final List<HTMLElement> children = new ArrayList<>();

protected HTMLElementComposite(String tagName) {
super(tagName);
}

public void append(HTMLElement htmlElement) {
this.children.add(htmlElement);
}

@Override
public String getOuterHTML() {
StringBuilder html = new StringBuilder();
html.append("<").append(this.getTagName()).append(">");
for (HTMLElement child : children) {
html.append(child.getOuterHTML());
}
html.append("</").append(this.getTagName()).append(">");
return html.toString();
}
}
java
  • HTMLElement:HTML元素顶层接口
  • AbstractHTMLElement:所有有标签的HTML元素的抽象
  • HTMLElementComposite:所有可以包含子元素的HTML元素的抽象,有些元素是无法包含子元素的,如<br/>, <hr/>等元素

文本在HTML文档中也是一种元素,并不是一种属性。并且其可以作为其他元素的子元素。

public class Text implements HTMLElement {

private final String value;

public Text(String value) {
this.value = value;
}

@Override
public String getOuterHTML() {
return value;
}
}
java

<br/> 是一种不可包含子元素的HTML元素。

public class Br extends AbstractHTMLElement {

public Br() {
super("br");
}

@Override
public String getOuterHTML() {
return "<br/>";
}
}
java

body,div,span,button都可以包含子元素。

public class Body extends HTMLElementComposite {

public Body() {
super("body");
}
}

public class Div extends HTMLElementComposite {

public Div() {
super("div");
}

// 提供一个可以直接添加子元素的构造函数,为了方便后续测试
public Div(HTMLElement...elements) {
this();
for (HTMLElement element : elements) {
this.append(element);
}
}
}

public class Span extends HTMLElementComposite {

public Span() {
super("span");
}

public Span(HTMLElement...elements) {
this();
for (HTMLElement element : elements) {
this.append(element);
}
}
}

public class Button extends HTMLElementComposite {

public Button() {
super("button");
}

public Button(HTMLElement...elements) {
this();
for (HTMLElement element : elements) {
this.append(element);
}
}
}
java

使用


public class Main {

public static void main(String[] args) {

Body body = new Body();
body.append(new Text("hello world!"));
body.append(new Br());
body.append(new Span(
new Text("span内文字")
));
body.append(new Div(
new Text("div内文字"),
new Span(new Text("span内文字2")),
new Button(new Text("click me"))
));
System.out.println(body.getOuterHTML());
}
}

java

输出

<body>hello world!<br/><span>span内文字</span><div>div内文字<span>span内文字2</span><button>click me</button></div></body>

上例中的文档结构如下

实际的业务实例

在开源框架中的应用

在Netty中的应用

Netty框架中的 CompositeByteBuf 就是组合模式的一种应用。

CompositeByteBuf中有个Component数组,Component里面放着缓冲区,还有各种索引。对外操作CompositeByteBuf,实际上操作的是Compoent数组。这使得在Netty中处理大量数据时能够以一致的方式处理单个数据块和数据块的组合。

关于的详细介绍可以参考这篇博文:https://blog.csdn.net/wangwei19871103/article/details/104486129

在Spring Cloud中的应用

Spring Cloud中的CompositeDiscoveryClient也是组合模式的一种应用。它使得Spring Cloud可以支持多种服务发现机制。

CompositeDiscoveryClient源码
public class CompositeDiscoveryClient implements DiscoveryClient {

private final List<DiscoveryClient> discoveryClients;

public CompositeDiscoveryClient(List<DiscoveryClient> discoveryClients) {
AnnotationAwareOrderComparator.sort(discoveryClients);
this.discoveryClients = discoveryClients;
}

@Override
public String description() {
return "Composite Discovery Client";
}

@Override
public List<ServiceInstance> getInstances(String serviceId) {
if (this.discoveryClients != null) {
for (DiscoveryClient discoveryClient : this.discoveryClients) {
List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);
if (instances != null && !instances.isEmpty()) {
return instances;
}
}
}
return Collections.emptyList();
}

@Override
public List<String> getServices() {
LinkedHashSet<String> services = new LinkedHashSet<>();
if (this.discoveryClients != null) {
for (DiscoveryClient discoveryClient : this.discoveryClients) {
List<String> serviceForClient = discoveryClient.getServices();
if (serviceForClient != null) {
services.addAll(serviceForClient);
}
}
}
return new ArrayList<>(services);
}

public List<DiscoveryClient> getDiscoveryClients() {
return this.discoveryClients;
}

}
java

CompositeDiscoveryClient 中并不会将每个DiscoveryClient的调用结果汇总,如getInstances方法,只要在某个DiscoveryClient中找到了服务实例,就直接返回。

何时使用

  • 当有一组对象以树形结构组织,并且需要以统一的方式对待单个对象和对象组合时,可以使用组合模式。例如,文件系统中的文件和文件夹可以使用组合模式来统一处理。
  • 当需要表示对象的部分-整体层次结构,并且希望用户能够忽略单个对象和组合对象之间的区别时,也可以考虑使用组合模式。例如,图形用户界面中的UI组件,如面板、按钮和文本框等,可以使用组合模式来统一处理用户交互。
  • 当希望通过递归结构来表示对象时,组合模式也是一个合适的选择。例如,菜单中的菜单项,可以使用组合模式来统一处理菜单和菜单项之间的操作。

与其他设计模式的联系

  • 组合模式常常与迭代器模式一起使用,以便对组合对象的子节点进行遍历和操作。
  • 装饰器模式与组合模式可以结合使用,通过对组合模式中的对象进行装饰,动态地为对象添加新的功能。

Netty中的CompositeByteBuf就是组合模式与迭代器模式一起使用的一个实例。

总结

组合模式的主要作用就是像操作单个对象一样操作整体