目录
前言
组合模式将具有父子关系的对象组合成树形结构。
使用时需先抽象出节点的共有方法,提供统一的接口,使得客户端对叶子节点(无子节点)和组合节点(有子节点)的使用具有一致性。
提取共有方法,可分为两种方式
透明模式
接口除了定义了所有节点的共有方法,还定义了组合节点的独有方法,这么做的好处是使用时便可以依赖抽象,符合依赖倒置原则,但也同时违背了接口隔离原则,叶子节点可以使用组合节点的方法。
安全模式
接口只定义所有节点的共有方法,在使用节点对象时就必须指定具体的组合节点类或者叶子节点类,没法依赖抽象,违背依赖倒置原则,但是遵循了接口隔离原则。
UML
plantuml
@startuml 'https://plantuml.com/class-diagram abstract Component { + operation() : void + add() : void + remove() : void; + getChildren() : List<Component> } class Composite { + children : List<Component + operation() : void + add() : void + remove() : void; + getChildren() : List<Component> } class Leaf { + operation() : void } class Client {} Component <|-- Composite Component <|-- Leaf Composite "1" --> "n" Component Client ..> Component @enduml
类图
实战代码
安全模式
区分组合节点和叶子节点,就是要看节点下是否有子节点。
实际上,叶子节点和组合节点也可以有相同的子节点字段(children),只需通过子节点字段(children)是否为空数组就能区分组合节点还是叶子节点了。
所以将子节点字段(children)也一起抽象了 。
//抽象节点类
abstract class Node {
private String id;
private String pid;
private List<Node> children;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPid() {
return pid;
}
public void setPid(String pid) {
this.pid = pid;
}
public List<Node> getChildren() {
return children;
}
public void setChildren(List<Node> children) {
this.children = children;
}
}
//具体节点类
class ConcreteNode extends Node {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class TreeUtils {
public final static String ROOT = "root";
/**
* 递归构造树
*
* @param sources 按parentId分类的节点
* @param parentId 父id
* @return 根节点集合
*/
public static List<Node> buildTree(Map<String, List<Node>> sources, String parentId) {
List<Node> nodes = sources.getOrDefault(parentId, emptyList());
for (Node node : nodes) {
List<Node> subNodes = buildTree(sources, node.getId());
node.setChildren(subNodes);
}
return nodes;
}
//测试代码
public static void main(String[] args) {
ConcreteNode root = new ConcreteNode();
root.setId("1");
root.setPid(null);
root.setName("root");
ConcreteNode child1 = new ConcreteNode();
child1.setId("2");
child1.setPid("1");
child1.setName("child1");
ConcreteNode child2 = new ConcreteNode();
child2.setId("3");
child2.setPid("1");
child2.setName("child2");
List<Node> children = Arrays.asList(child1, child2);
root.setChildren(children);
//模拟从数据库中查到的节点数据
List<Node> nodes = Arrays.asList(root, child1, child2);
//按父节点分类
Map<String, List<Node>> sources = nodes.stream()
.collect(groupingBy(e -> Objects.isNull(e.getPid()) ? ROOT : e.getPid()));
//构造树
TreeUtils.buildTree(sources, ROOT);
}
}
优化
实际使用中可直接简化成一个节点类
//节点实体类
public class Node {
private String id;
private String pid;
private String name;
private List<Node> children;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPid() {
return pid;
}
public void setPid(String pid) {
this.pid = pid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Node> getChildren() {
return children;
}
public void setChildren(List<Node> children) {
this.children = children;
}
}
在开发过的项目中,一般都是直接构造完树后返回给前端做展示,需要的只是树的结构,组合节点和叶子节点除了基础的 get / set 方法没有其他方法。
既然无需关心节点的方法,组合节点和叶子节点的属性又都可以一致,就不需要专门抽象出一个统一的接口,只需要一个节点类就足够了。
使用时查询出所有节点,再通过通用工具类构造成树,最后返回前端。
有关通用工具类构造数方法:Java泛型+函数式接口实战:简单的通用构造树方法