只需要操作问题,就能编辑答案?

概念

组合模式实际上是树结构,每个节点被称为组件,组件分为容器和叶子节点的树形结构。通常来说,容器可以拥有子容器和子节点,而叶子节点不会有子级。组件会提供公共的接口,不需要理会当前数据是容器还是叶子节点,交由它们内部自行处理。

组合模式和常规的树形结构很像

//常用树形结构
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);
}

最终执行的结果:

总结

上面模拟的需求比较简单,在实际树形结构的需求中,可以进行扩展。

可能会涉及到例如:

将某个部门下所有离职人员的账号禁用:部门(容器),人员账号(叶子节点)

获取办公大楼内,所有已出租房间的收租状况:办公大楼(容器),房间(容器),房间收租详情(叶子节点)

点击转移办公室位置,将所有办公室登记资产的地址都迁移到新的位置:办公室(容器),资产记录(叶子节点)

这种方式可以将多次的数据库查询步骤转换成一次数据库查询,剩余的逻辑操作都使用代码进行。

大家有什么好建议或者新的使用场景呢?

  • 15
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值