Design-Pattern-Composite
组合模式(composite)是一种结构型设计模式,可以使用它将对象组合成树状结构, 并且能像使用独立对象一样使用它们。
场景
在开发中会遇到需要处理/统计的对象们,属于层级或者包含关系。学过数据结构的同学肯定知道,通常用树来组织这些对象。
树由节点和边构成。其中节点用于存储数据,而边构建了节点之间的关系。一个节点通过持有另一些节点的引用,来构建边。一段典型树结构代码如下所示。
1 |
|
在此基础上进行一些拓展。上例中,所有节点都属于同一类TreeNode
。而在实际建模时,不同维度节点需要储存和表示的信息不同,通常会使用不同类。例如想统计国内经济指标,节点可以是省、市、区、县类。当节点属于不同类时,我们没法在所有节点类中都使用List<TreeNode> children
去定义所有可能的子节点。一种朴素的解决方法是,每种类仅持有其子类的节点。例如可以在定义省
时,持有市
的节点,市
中再持有区
的节点。但也存在例外,例如直辖市不属于任一省,某些市没有设置区。为了兼容这些特殊情况,需要定义多种类型子节点变量,如下所示。
1 |
|
而在客户端代码中,当需要统计某个省或者某个市的经济时,可能会定义如下方法。
1 |
|
可以发现节点种类越多,定义节点的代码和客户端引用节点的代码就会越复杂。
此外也会设计容器节点。容器节点指只提供结构和组织,而不直接存储数据的节点。例如上例中,想要统计一个省加一个市加一个区的数据,可以在容器节点中持有这三个不同类节点的引用。客户端只与容器节点交互,而不关心容器内具体包含什么。所以容器节点类需要依赖其他所有节点类。容器节点类内通常也会定义递归遍历其子节点的方法,当子节点种类越多时,容器节点类的代码也会越复杂。
参考数据结构中经典的树结构,我们期望对所有节点类型一视同仁。如此节点内定义子节点和客户端引用的代码会更简洁。又期望保持每种节点类的特色。这显然要利用到java
多态的特性。
组合模式
组合模式中,所有节点类型都实现一个Component
接口,用来表示是树结构中的一个节点。客户端通过接口与不同的节点类交互,并不关心其具体类型,移除了对具体节点类的依赖。Component
接口中定义节点类需要向外暴露的共同特性,例如上例中统计GDP指标的方法。对于非叶子节点类,这些方法实现通常为调用其子节点的同方法,并汇总返回;而在叶子节点中,实际进行属性查询并计算。这种调用方式也被称为树的递归。
根据组合模式,我们可以重构上例中的代码。
1 |
|
组合模式的类图相对比较简单,核心便是代表节点的接口Component
。对于节点类,通常还会分为叶子节点Leaf
和非叶子节点(也称组合或容器)Composite
。叶子节点由于不包含子节点,因此无需定义子节点的成员变量,也不需要提供添加/删除子节点的方法。
classDiagram
direction TB
class Client
class Component {
<< interface >>
+methodA() ResultA
}
Client --> Component
class Leaf {
+methodA() ResultA
}
class Composite {
-List~Composite~ children
+methodA() ResultA
+addChild(Component) void
+removeChild(Component) Component
}
Leaf ..|> Component
Composite ..|> Component
添加和删除子节点的方法
由于客户端与接口进行交互,而上例中接口中并不包含添加或删除子节点的方法。因此当客户端需要进行这类操作时,不可避免会依赖具体容器节点类。如果想移除这种依赖,可以将这些方法同样定义在接口中。但注意,这违反了单一职责原则,因为对于叶子节点来说,添加和删除子节点的方法是无意义,且不该被支持的。
不论是否在接口中声明这类方法,都是合理的选择。通常根据客户端使用这类方法的频率,来决定是否在接口中添加。这是对耦合性和接口隔离性的权衡与取舍。
总结
组合模式通常使用于,需要处理统计的对象属于层级关系或包含关系。
组合模式通过定义一个Component
接口来表示树上的一个节点,以此对所有具体节点类一视同仁,移除了对具体节点类的依赖,简化了具体节点类和客户端的代码。
Component
接口内需要声明对所有节点类都有意义的方法。
实现接口声明的方法时,容器应该将大部分工作交给其子元素来完成,也就是递归。
也可以在接口中定义添加或删除子节点的方法。这样允许客户端通过接口对容器元素进行操作,但也会违反单一职责原则。