概念
组合模式实际上是树结构,每个节点被称为组件,组件分为容器和叶子节点的树形结构。通常来说,容器可以拥有子容器和子节点,而叶子节点不会有子级。组件会提供公共的接口,不需要理会当前数据是容器还是叶子节点,交由它们内部自行处理。
组合模式和常规的树形结构很像
//常用树形结构
Tree<Node> treeNode;
//组合模式
TreeInter<Node> treeInter;
两种模式的区别在于:
常用树形结构:用于数据结构算法或给前端展示树形结构数据,迭代时只能进行相同的逻辑处理(除非在方法内加入大量的if判断)
组合模式:在常规树形基础上,使用接口类型来代替Tree对象。通过调用接口提供的公共方法,可以调用内部不同的处理逻辑。
适用场景
组合模式的适用场景包括但不限于:
1.在文件Tree中删除文件或文件夹
2.不同类型的流对象的相同功能
3.带树形结构且需要对多个节点进行逻辑操作的
需求模拟
智能问答功能就是常见的树形结构数据,点击问题链接后,会出现下一级问题或者答案。
在客户使用时一般使用pid查询数据库来实现,但是在问答配置页面,对存在子级的问题进行操作时,就能提现组合模式的优势。
约束:
每个问题都可以添加回答,也可以拥有子问题。
这里假设有两个需求:
需求:
1.禁用状态的回答全部添加“已过期”文字。(对叶子节点修改数据)
2.删除指定层级被禁用的问题,并删除它的回答和子问题。(对树结构数据进行数据删除)
代码实现
首先创建数据对象DTO:
@Data
@Accessors(chain = true)
public class BaseComposeDTO {
/**
* id
*/
private Long id;
/**
* pid
*/
private Long pid;
/**
* 展示的文字
*/
private String value;
/**
* 启用/禁用
*/
private Boolean enable;
}
/**
* 问题DTO
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class QuestionDTO extends BaseComposeDTO{
/**
* 第几层问题,数据库字段或循环逻辑写入
*/
private Integer level;
}
/**
* 答案DTO
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class AnswersDTO extends BaseComposeDTO{
}
构建模拟前端传入或数据库查出的数据列表:
/**
* 模拟数据
* @return
*/
private List<BaseComposeDTO> getData(){
BaseComposeDTO question1 = new QuestionDTO().setLevel(1).setValue("第1问题").setId(1L).setPid(0L);
BaseComposeDTO question1_1 = new QuestionDTO().setLevel(2).setEnable(false).setValue("第1-1问题").setId(2L).setPid(1L);
BaseComposeDTO question1_1_1 = new QuestionDTO().setLevel(3).setValue("第1-1-1问题").setId(3L).setPid(2L);
BaseComposeDTO question1_2 = new QuestionDTO().setLevel(2).setValue("第1-2问题").setId(4L).setPid(1L);
BaseComposeDTO question2 = new QuestionDTO().setLevel(1).setValue("第2问题").setId(5L).setPid(0L);
BaseComposeDTO answers1_1 = new AnswersDTO().setValue("第1-1答案").setEnable(true).setId(6L).setPid(2L);
BaseComposeDTO answers1_1_1 = new AnswersDTO().setValue("第1-1-1答案").setId(7L).setPid(3L);
List<BaseComposeDTO> baseComposes = new ArrayList<>();
baseComposes.add(question1);
baseComposes.add(question1_1);
baseComposes.add(question1_1_1);
baseComposes.add(question2);
baseComposes.add(question1_2);
baseComposes.add(answers1_1);
baseComposes.add(answers1_1_1);
return baseComposes;
}
前期准备工作已经完成,现在需要构建组合模式的容器-叶子模型,首先创建统一的组件interface。
/**
* 组件
* @param <T>
*/
public interface IComponent<T> {
/**
* 获取子组件
*/
default List<IComponent<T>> getChildren(){return null;};
/**
* 获取组件内存放的数据
*/
T get();
/**
* 功能接口:给value的值加上备注前缀
*/
default void addRemark(String remark){};
/**
* 功能接口:删除层级为level,且为指定启用状态的数据
*/
default void delByLevel(int level,boolean enabled){};
/**
* 功能接口:删除数据
*/
void remove();
}
创建问题实现类(容器),答案实现类(叶子节点)
/**
* 问题(容器)
*/
@Slf4j
public class QuestionComposite implements IComponent<BaseComposeDTO> {
private final List<IComponent<BaseComposeDTO>> components = new ArrayList<>();
private final BaseComposeDTO baseComposeDTO;
public QuestionComposite(BaseComposeDTO baseComposeDTO) {
this.baseComposeDTO = baseComposeDTO;
}
@Override
public BaseComposeDTO get(){
return baseComposeDTO;
}
@Override
public void remove() {
//TODO 进行数据删除 dataSource.delById(this.baseComposeDTO.getId())
log.info("id:{}的问题已被删除",this.baseComposeDTO.getId());
}
public void delByLevel(int level,boolean enabled){
QuestionDTO questionDTO = (QuestionDTO) baseComposeDTO;
if(questionDTO.getLevel().compareTo(level) != 0){
return;
}
if(BooleanUtil.isTrue(questionDTO.getEnable()) != enabled){
return;
}
List<IComponent<BaseComposeDTO>> delComponents = new ArrayList<>();
delChildrenComponents(delComponents, this);
for (IComponent<BaseComposeDTO> delComponent : delComponents) {
log.info("删除ID为:{}的数据",delComponent.get().getId());
//可以额外提供对外的remove接口,分别在Question
}
}
/**
* 遍历调用容器的删除方法
* @param delComponents
* @param component
*/
private void delChildrenComponents(List<IComponent<BaseComposeDTO>> delComponents,IComponent<BaseComposeDTO> component){
if(Objects.isNull(component)){
return;
}
delComponents.add(component);
if(CollectionUtil.isEmpty(component.getChildren())){
return;
}
for (IComponent<BaseComposeDTO> child : component.getChildren()) {
delChildrenComponents(delComponents, child);
}
}
public void add(IComponent<BaseComposeDTO> iComponent){
this.components.add(iComponent);
}
public List<IComponent<BaseComposeDTO>> getChildren(){
return this.components;
}
}
/**
* 答案(叶子节点)
*/
@Slf4j
public class AnswersComposite implements IComponent<BaseComposeDTO> {
private final BaseComposeDTO baseComposeDTO;
public AnswersComposite(BaseComposeDTO baseComposeDTO) {
this.baseComposeDTO = baseComposeDTO;
}
@Override
public BaseComposeDTO get(){
return baseComposeDTO;
}
@Override
public void addRemark(String remark){
if(!BooleanUtil.isTrue(baseComposeDTO.getEnable())) {
baseComposeDTO.setValue(remark + ":" + baseComposeDTO.getValue());
log.info("id:{}数据已修改", baseComposeDTO.getId());
}
}
@Override
public void remove() {
//TODO 进行数据删除 dataSource.delById(this.baseComposeDTO.getId())
log.info("id:{}的答案已被删除",this.baseComposeDTO.getId());
}
}
组件提供实现需求的两个对外的功能接口:
1.对答案的value值进行编辑,因此只需要AnswersComposite实现这个功能
2.对问题及其子问题进行编辑,因此只需要QuestionComposite实现这个功能。并且在这个实现方法中可以看到,只需要收集所有的IComponent,调用提供的remove方法。具体的删除步骤交由它们内部自行处理。
开始实现我们之前提到的两个功能需求:
首先提供一个将数据列表转换成组件结构的方法,如需要可以提取成工具类,支持任意数据对象。
/**
* 将前端或查询得到的数据列表封装成组合模式结构
*/
private List<IComponent<BaseComposeDTO>> buildTree(List<BaseComposeDTO> baseComposeDTOS){
List<IComponent<BaseComposeDTO>> roots = new ArrayList<>();
Map<Long, IComponent<BaseComposeDTO>> idBaseComposeMap = baseComposeDTOS.stream().collect(Collectors.toMap(BaseComposeDTO::getId, x -> {
if(x instanceof QuestionDTO){
return new QuestionComposite(x);
}else {
return new AnswersComposite(x);
}
}));
for (BaseComposeDTO baseCompose : baseComposeDTOS) {
if(idBaseComposeMap.get(baseCompose.getPid()) == null){
//root节点
roots.add(idBaseComposeMap.get(baseCompose.getId()));
}else{
IComponent<BaseComposeDTO> component = idBaseComposeMap.get(baseCompose.getPid());
if(Objects.nonNull(component) && component instanceof QuestionComposite){
((QuestionComposite)component).add(idBaseComposeMap.get(baseCompose.getId()));
}
}
}
return roots;
}
从现在开始,不需要知道我们的数据是问题还是答案了,对于使用者来说,它们都是同样的组件。
先提供一个统一执行组件方法的入口:
/**
* 执行组件方法
*/
public void executeComponents(IComponent<BaseComposeDTO> node,Function<IComponent<BaseComposeDTO>,Void> function){
if(Objects.isNull(node)){
return;
}
function.apply(node);
if(CollectionUtils.isEmpty(node.getChildren())){
return;
}
for (IComponent<BaseComposeDTO> child : node.getChildren()) {
executeComponents(child, function);
}
}
/**
* 打印组件值
* @param components
*/
private void print(List<IComponent<BaseComposeDTO>> components){
for (IComponent<BaseComposeDTO> component : components) {
log.info("id:{},value:{}",component.get().getId(),component.get().getValue());
if(Objects.nonNull(component.getChildren())){
print(component.getChildren());
}
}
}
实现功能:
//根据data数据构建组件,可能存在多个根组件
List<IComponent<BaseComposeDTO>> roots = buildTree(getData());
//只修改答案value值
Function<IComponent<BaseComposeDTO>,Void> addAnswersRemarkFunction = (x) -> {x.addRemark("这条答案已过期");return null;};
//删除第二层级且enable为false的问题及所有子问题和答案
Function<IComponent<BaseComposeDTO>,Void> delByLevelFunction = (x) -> {x.delByLevel(2,false);return null;};
log.info("=============开始修改答案");
for (IComponent<BaseComposeDTO> root : roots) {
executeComponents(root, addAnswersRemarkFunction);
}
print(roots);
log.info("=============开始删除数据");
for (IComponent<BaseComposeDTO> root : roots) {
executeComponents(root, delByLevelFunction);
}
最终执行的结果:
总结
上面模拟的需求比较简单,在实际树形结构的需求中,可以进行扩展。
可能会涉及到例如:
将某个部门下所有离职人员的账号禁用:部门(容器),人员账号(叶子节点)
获取办公大楼内,所有已出租房间的收租状况:办公大楼(容器),房间(容器),房间收租详情(叶子节点)
点击转移办公室位置,将所有办公室登记资产的地址都迁移到新的位置:办公室(容器),资产记录(叶子节点)
这种方式可以将多次的数据库查询步骤转换成一次数据库查询,剩余的逻辑操作都使用代码进行。
大家有什么好建议或者新的使用场景呢?