Breakthrough CRUD universal tree tool class encapsulation

Keywords: Programming Attribute less REST SpringBoot

0. You may be able to gain after learning this article

  • Experience the process of gradual optimization and perfection of a tree tool from the beginning

  • Design and implementation of tree tool package

  • Finally, we get a ready to use tree tool source code

Students who have some knowledge and use of the front-end tree components can directly jump to the beginning of Chapter 3.

1. What does a tree look like?

Most of the front-end tree components appear in the back-end management system, such as our common menu tree, organization tree, etc. Roughly as shown in the picture below.

Menu tree

Mechanism tree

org_tree.png

Tree form

Generally speaking, the presentation form of the front-end tree is the forms listed in the above three figures. The presentation of this front-end tree component depends on the data format returned by the back-end.

2. Data format

Combined with the front-end tree components I have used, they can be roughly divided into the following two types.

Tabular form

[
    { id:1, pId:0, name:"Parent node 1"}

    { id:11, pId:1, name:"Parent node 11"},
    { id:111, pId:11, name:"Leaf node 111"},
    { id:112, pId:11, name:"Leaf node 112"},
    { id:113, pId:11, name:"Leaf node 113"},
    { id:114, pId:11, name:"Leaf node 114"},

    { id:12, pId:1, name:"Parent node 12"},
    { id:121, pId:12, name:"Leaf node 121"},
    { id:122, pId:12, name:"Leaf node 122"},
    { id:123, pId:12, name:"Leaf node 123"},
    { id:124, pId:12, name:"Leaf node 124"}
]

tree structure

[{    name:"Parent node 1",
    children: [
        { 
            name:"Parent node 11",
            children: [
                { name:"Leaf node 111"},
                { name:"Leaf node 112"},
                { name:"Leaf node 113"},
                { name:"Leaf node 114"}
            ]
        },
        { 
            name:"Parent node 12",
            children: [
                { name:"Leaf node 121"},
                { name:"Leaf node 122"},
                { name:"Leaf node 123"},
                { name:"Leaf node 124"}
            ]
        }
    ]
}]

The tree tool encapsulation mentioned in this paper is mainly aimed at the tree structure of the second data format, because the first one does not need special processing itself, and there is no encapsulation, that is, simple list query display. The difference with the general data list data format is that more data ID and parent ID attributes are provided to the front end for tree component construction.

The second is to transform the data format in the form of list to form the tree structure as shown above. However, we found that there is no data ID and parent ID attribute in it, why? Because the back-end completes the construction of the tree structure of the data layer, the front-end tree component no longer needs to build the tree structure according to these two attributes. It's OK to show it directly, of course, it's not absolute. Finally, it depends on whether the front-end tree component needs to.

However, these two attributes are generally retained, because in addition to the construction requirements of the tree passing component itself, these two attributes are often required in business processing, and the tree structure of the later tree tool must be constructed by data ID and parent ID.

If you feel the trouble mentioned above, you should remember that whether it's a list structure or a tree structure, it's right to always keep the data ID and the parent ID attributes.

There is a new problem here. It is said that the list form can be used directly without any encapsulation. In this case, the structure of list form is finished. Why write a tool class and make a tree structure?

The reason is that there are many ways to implement the front-end tree components. Different tree plug-ins or components may require different data formats. Some of them support both list and tree formats, and some only support one of them. Therefore, in order to meet the display requirements of different front-end trees, it is necessary to provide a tree structure construction tool.

3. If you don't talk much, make a first edition

From the above content, we know that the rendering and presentation of the front-end tree component requires the back-end to provide the data format that meets the requirements. In fact, the core responsibility of the tree tool class is to transform the general data list structure into a tree structure, so as to provide it to the front-end.

To interpret the core responsibilities mentioned above, first of all, what is the general list? Let's assume that it is a menu list. Then there is the first class menueentity, followed by the conversion, who will convert to whom? The data list transforms the tree structure. The tree structure itself should be a class. Let's call it TreeNode for the time being. Combined with the menu list assumed in the first step, it is actually list < menunentity > transformed to list < TreeNode >, so we get the second TreeNode class. Who will do the transformation? That's TreeUtil today.

OK, so far, by analyzing the core responsibilities of the tree tool class, we get three classes.

  • MenuEntity

  • TreeNode

  • TreeUtil

OK, with the above content, let's have a simple implementation.

Tree node class

public class TreeNode {
    //Tree node ID
    private String id;
    //Tree node name
    private String name;
    //Tree node coding
    private String code;
    //Tree node link
    private String linkUrl;
    //Tree node icon
    private String icon;
    //Parent node ID
    private String parentId;
}

Menu class

public class MenuEntity {
    //Menu ID
    private String id;
    //Parent menu ID
    private String pid;
    //Menu name
    private String name;
    //Menu coding
    private String code;
    //Menu icon
    private String icon;
    //Menu links
    private String url;
}

Tree tool class

public class TreeUtil {

    /**
     * Tree construction
     */
    public static List<TreeNode> build(List<TreeNode> treeNodes,Object parentId){
        List<TreeNode> finalTreeNodes = CollectionUtil.newArrayList();
        for(TreeNode treeNode : treeNodes){
            if(parentId.equals(treeNode.getParentId())){
                finalTreeNodes.add(treeNode);
                innerBuild(treeNodes,treeNode);
            }
        }
        return finalTreeNodes;
    }

    private static void innerBuild(List<TreeNode> treeNodes,TreeNode parentNode){
        for(TreeNode childNode : treeNodes){
            if(parentNode.getId().equals(childNode.getParentId())){
                List<TreeNode> children = parentNode.getChildren();
                if(children == null){
                    children = CollectionUtil.newArrayList();
                    parentNode.setChildren(children);
                }
                children.add(childNode);
                childNode.setParentId(parentNode.getId());
                innerBuild(treeNodes,childNode);
            }
        }
    }
}

There are two key points in the implementation of tree tool class. First, the starting position of tree construction is where to start construction. Therefore, a parent ID parameter is needed to specify the starting position of the construction. Second, when the construction ends, if there is no restriction, our tree can be extended indefinitely. Therefore, the innerBuild method here performs recursive operation.

Test code

public static void main(String[] args{
    //1. Simulation menu data
    List<MenuEntity> menuEntityList = CollectionUtil.newArrayList();
    menuEntityList.add(new MenuEntity("1","0","system management","sys","/sys"));
    menuEntityList.add(new MenuEntity("11","1","user management","user","/sys/user"));
    menuEntityList.add(new MenuEntity("111","11","User add","userAdd","/sys/user/add"));
    menuEntityList.add(new MenuEntity("2","0","Store management","store","/store"));
    menuEntityList.add(new MenuEntity("21","2","Commodity management","shop","/shop"));

    // 2,MenuEntity -> TreeNode
    List<TreeNode> treeNodes = CollectionUtil.newArrayList();
    for(MenuEntity menuEntity : menuEntityList){
        TreeNode treeNode = new TreeNode();
        treeNode.setId(menuEntity.getId());
        treeNode.setParentId(menuEntity.getPid());
        treeNode.setCode(menuEntity.getCode());
        treeNode.setName(menuEntity.getName());
        treeNode.setLinkUrl(menuEntity.getUrl());
        treeNodes.add(treeNode);
    }

    //3. Tree structure construction
    List<TreeNode> treeStructureNodes = TreeUtil.build(treeNodes,"0");
    Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes)));
}

After work, the first version of the simple tree tool is implemented.

4. Iterative optimization

1.0 it's not my business

However, through testing the code, we found that this is not very good. To convert the menu data to the tree structure, I need to convert the menu list to the tree structure list before I can call the build method of the tree tool class. Here, the conversion operation is only a copy of the properties, but it has not completed the generation and construction of the tree structure. But is this what the caller needs to care about? It is obvious that the TreeNode collection creation and generation process should be what the tree tool class should do. So we made the following adjustments.

1. Adjust the parameters of the build method, and adjust the original treeNodes to menuEntityList, which means that the treeNodes construction structure mentioned above will be done by TreeUtil.

2. A new convert class is added, which contains the convert method. The function of this method is to copy the attributes from menu entity to tree node.

3 adjust the parameters of the build method again, and add the Convert conversion.

See the code for the results of the adjustment.

Tree tools

public class TreeUtil_1_0 {

    //New attribute conversion method
    public interface Convert<MenuEntity,TreeNode>{
        public void convert(MenuEntity menuEntity, TreeNode treeNode);
    }

    /**
     * Tree construction
     */
    public static List<TreeNode> build(List<MenuEntity> menuEntityList,Object parentId,Convert<MenuEntity,TreeNode> convert){

        //What the caller did
        List<TreeNode> treeNodes = CollectionUtil.newArrayList();
        for(MenuEntity menuEntity: menuEntityList){
            TreeNode treeNode = new TreeNode();
            convert.convert(menuEntity,treeNode);
            treeNodes.add(treeNode);
        }

        List<TreeNode> finalTreeNodes = CollectionUtil.newArrayList();
        for(TreeNode treeNode : treeNodes){
            if(parentId.equals(treeNode.getParentId())){
                finalTreeNodes.add(treeNode);
                innerBuild(treeNodes,treeNode);
            }
        }
        return finalTreeNodes;
    }

    private static void innerBuild(List<TreeNode> treeNodes,TreeNode parentNode){
        for(TreeNode childNode : treeNodes){
            if(parentNode.getId().equals(childNode.getParentId())){
                List<TreeNode> children = parentNode.getChildren();
                if(children == null){
                    children = CollectionUtil.newArrayList();
                    parentNode.setChildren(children);
                }
                children.add(childNode);
                childNode.setParentId(parentNode.getId());
                innerBuild(treeNodes,childNode);
            }
        }
    }
}

Test code

public static void main(String[] args{
    //1. Simulation menu data
    List<MenuEntity> menuEntityList = CollectionUtil.newArrayList();
    menuEntityList.add(new MenuEntity("1","0","system management","sys","/sys"));
    menuEntityList.add(new MenuEntity("11","1","user management","user","/sys/user"));
    menuEntityList.add(new MenuEntity("111","11","User add","userAdd","/sys/user/add"));
    menuEntityList.add(new MenuEntity("2","0","Store management","store","/store"));
    menuEntityList.add(new MenuEntity("21","2","Commodity management","shop","/shop"));

    //2. Tree structure construction
    List<TreeNode> treeStructureNodes = TreeUtil_1_0.build(menuEntityList, "0"new Convert<MenuEntity, TreeNode>() {
        @Override
        public void convert(MenuEntity menuEntity, TreeNode treeNode{
            treeNode.setId(menuEntity.getId());
            treeNode.setParentId(menuEntity.getPid());
            treeNode.setCode(menuEntity.getCode());
            treeNode.setName(menuEntity.getName());
            treeNode.setLinkUrl(menuEntity.getUrl());
        }
    });
    Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes)));
}

Comparing the test code of 1.0 with that of the first version, it is found that the process of tree node list building is less, and the work of attribute copying is handled as a callback process in the transformation process.

2.0 only support building menu tree

After 1.0 optimization, we have new requirements, and a mechanism tree also needs to be generated. At this time, the tree tool only supports the menu tree, so we have to modify it to support the tree generation of any other object.

The transformation point is mainly to convert the menu entities in TreeUtil into generics, and stick a core method code for space limitation

public static <T> List<TreeNode> build(List<T> list,Object parentId,Convert<T,TreeNode> convert){
    List<TreeNode> treeNodes = CollectionUtil.newArrayList();
    for(T obj : list){
        TreeNode treeNode = new TreeNode();
        convert.convert(obj,treeNode);
        treeNodes.add(treeNode);
    }

    List<TreeNode> finalTreeNodes = CollectionUtil.newArrayList();
    for(TreeNode treeNode : treeNodes){
        if(parentId.equals(treeNode.getParentId())){
            finalTreeNodes.add(treeNode);
            innerBuild(treeNodes,treeNode);
        }
    }
    return finalTreeNodes;
}

In this way, we can support any type of tree construction.

3.0 man, the attributes you returned are not enough

The first two points are easier to think of and implement, but at this time, the front-end students throw new questions. Man, the tree node attributes you return are not enough. Look at my interface. I need to note that you didn't come back.

Well, that's not really the case.

To meet the above requirements, simply add the remark attribute to the TreeNode class and assign a value to the Convert. That's not enough, but it's wrong to think about it. Today's front-end staff lacks a remark, and tomorrow's other staff may lack another attribute, all of which are added to the TreeNode. TreeNode is a tree node or a business entity, so we can't do this.

Here, it needs to be extensible and meet the opening and closing principle. Therefore, the proper processing method here is inheritance. If the TreeNode attribute cannot be satisfied, it can be realized by inheriting the tree node of the specific business to be extended.

The specific transformation points are as follows

1 the nodes of the new menu entity extension tree are as follows

public class MenuEntityTreeNode extends TreeNode {
    //Extended note properties
    private String remark;
    //Omit set get

}

2. Modify the TreeUtil.build method parameters, and add TreeNode Class type parameters, as follows

/**
 * Tree construction
 */
public static <T,E extends TreeNode> List<E> build(List<T> list,Object parentId,Class<EtreeNodeClass,Convert<T,Econvert){
    List<E> treeNodes = CollectionUtil.newArrayList();
    for(T obj : list){
        E treeNode = (E)ReflectUtil.newInstance(treeNodeClass);
        convert.convert(obj, treeNode);
        treeNodes.add(treeNode);
    }

    List<E> finalTreeNodes = CollectionUtil.newArrayList();
    for(E treeNode : treeNodes){
        if(parentId.equals(treeNode.getParentId())){
            finalTreeNodes.add((E)treeNode);
            innerBuild(treeNodes,treeNode);
        }
    }
    return finalTreeNodes;
}

Test code

public static void main(String[] args{
    //... simulation data creation is omitted here

    //2. Tree structure construction
    List<MenuEntityTreeNode> treeStructureNodes = TreeUtil_3_0.build(menuEntityList, "0",MenuEntityTreeNode.class,new TreeUtil_3_0.Convert<MenuEntity,MenuEntityTreeNode>(){

        @Override
        public void convert(MenuEntity object, MenuEntityTreeNode treeNode{
            treeNode.setId(object.getId());
            treeNode.setParentId(object.getPid());
            treeNode.setCode(object.getCode());
            treeNode.setName(object.getName());
            treeNode.setLinkUrl(object.getUrl());
            //New business attribute
            treeNode.setRemark("Add comment properties");
        }
    });
   Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeStructureNodes)));
}

In this way, when different attributes need to be added in different business scenarios, they can be extensible without any impact and change to the existing code.

4.0 man, my property name is not code

After the 3.0 version is completed, most of the requirements can be met. But at this time, the front-end students throw in new questions. Man, the number attribute of the tree node you returned is code, but my name is number. If you don't match it, the impact will be greater if I adjust it. Can you handle it when the back-end returns.

The code attribute name must not be adjusted, because the node numbers of other module trees are called code.

What can we do? In fact, it's also simple. Like version 3.0, add an attribute to the extended business tree node. This problem is solved. But in case that the attribute names of all treenodes don't correspond to the needs of the front end, it means that all tree attributes need to be extended and defined by themselves. This doesn't mean that all attributes in the useless parent treenode are returned. When serializing, it can be controlled. If it is empty, it will not be serialized, but it does not depend on the serialization framework. Is there any other way.

To sort out a little, the tree node attribute should be able to support the custom attribute name when returning to the front end.

Class attributes can't be changed after they are defined. How to customize them? Besides adding new classes and changing existing attributes, what else can we do? At this time, we should think of map

How to do it

1 first of all, define a new TreeNodeMap class, and see the name to see the map based implementation

public class TreeNodeMap extends HashMap {

    private TreeNodeConfig treeNodeConfig;

    public TreeNodeMap(){
        this.treeNodeConfig = TreeNodeConfig.getDefaultConfig();
    }

    public TreeNodeMap(TreeNodeConfig treeNodeConfig){
        this.treeNodeConfig = treeNodeConfig;
    }

    public <T> getId() {
        return (T)super.get(treeNodeConfig.getIdKey());
    }

    public void setId(String id) {
        super.put(treeNodeConfig.getIdKey(), id);
    }

    public <T> getParentId() {
        return (T)super.get(treeNodeConfig.getParentIdKey());
    }

    public void setParentId(String parentId) {
        super.put(treeNodeConfig.getParentIdKey(), parentId);
    }

    public <T> getName() {
        return (T)super.get(treeNodeConfig.getNameKey());
    }

    public void setName(String name) {
        super.put(treeNodeConfig.getNameKey(), name);
    }

    public <T> T  getCode() {
        return (T)super.get(treeNodeConfig.getCodeKey());
    }

    public TreeNodeMap setCode(String code) {
        super.put(treeNodeConfig.getCodeKey(), code);
        return this;
    }

    public List<TreeNodeMap> getChildren() {
        return (List<TreeNodeMap>)super.get(treeNodeConfig.getChildrenKey());
    }

    public void setChildren(List<TreeNodeMap> children) {
        super.put(treeNodeConfig.getChildrenKey(),children);
    }

    public void extra(String key,Object value){
        super.put(key,value);
    }
}

2. Since attribute name customization is supported, a new configuration class, treenoteconfig, is added to accomplish this task. At the same time, the default attribute name is provided

public class TreeNodeConfig {

    //Singleton object with default attribute
    private static TreeNodeConfig defaultConfig = new TreeNodeConfig();

    //Tree node default attribute constant
    static final String TREE_ID = "id";
    static final String TREE_NAME = "name";
    static final String TREE_CODE = "code";
    static final String TREE_CHILDREN = "children";
    static final String TREE_PARENT_ID = "parentId";

    //Attribute
    private String idKey;
    private String codeKey;
    private String nameKey;
    private String childrenKey;
    private String parentIdKey;

    public String getIdKey() {
        return getOrDefault(idKey,TREE_ID);
    }

    public void setIdKey(String idKey) {
        this.idKey = idKey;
    }

    public String getCodeKey() {
        return getOrDefault(codeKey,TREE_CODE);
    }

    public void setCodeKey(String codeKey) {
        this.codeKey = codeKey;
    }

    public String getNameKey() {
        return getOrDefault(nameKey,TREE_NAME);
    }

    public void setNameKey(String nameKey) {
        this.nameKey = nameKey;
    }

    public String getChildrenKey() {
        return getOrDefault(childrenKey,TREE_CHILDREN);
    }

    public void setChildrenKey(String childrenKey) {
        this.childrenKey = childrenKey;
    }

    public String getParentIdKey() {
        return getOrDefault(parentIdKey,TREE_PARENT_ID);
    }

    public void setParentIdKey(String parentIdKey) {
        this.parentIdKey = parentIdKey;
    }

    public String getOrDefault(String key,String defaultKey){
        if(key == null) {
            return defaultKey;
        }
        return key;
    }

    public static TreeNodeConfig getDefaultConfig(){
        return defaultConfig;
    }

}

3 finally, the TreeUtil.build method is modified. Based on version 2.0, only TreeNode is replaced by TreeNodeMap.

/**
 * Tree construction
 */
public static <T> List<TreeNodeMap> build(List<T> list,Object parentId,Convert<T,TreeNodeMap> convert){
    List<TreeNodeMap> treeNodes = CollectionUtil.newArrayList();
    for(T obj : list){
        TreeNodeMap treeNode = new TreeNodeMap();
        convert.convert(obj,treeNode);
        treeNodes.add(treeNode);
    }

    List<TreeNodeMap> finalTreeNodes = CollectionUtil.newArrayList();
    for(TreeNodeMap treeNode : treeNodes){
        if(parentId.equals(treeNode.getParentId())){
            finalTreeNodes.add(treeNode);
            innerBuild(treeNodes,treeNode);
        }
    }
    return finalTreeNodes;
}

Test code

public static void main(String[] args{
     //... omit creation of menu simulation data

    TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
    //Custom property name
    treeNodeConfig.setCodeKey("number");
    List<TreeNodeMap> treeNodes = TreeUtil_4_0.build(menuEntityList, "0",treeNodeConfig,new TreeUtil_4_0.Convert<MenuEntity, TreeNodeMap>() {
        @Override
        public void convert(MenuEntity object, TreeNodeMap treeNode{
            treeNode.setId(object.getId());
            treeNode.setParentId(object.getPid());
            treeNode.setCode(object.getCode());
            treeNode.setName(object.getName());
            //Attribute extension
            treeNode.extra("extra1","123");
        }
    });

    Console.log(JSONUtil.formatJsonStr(JSONUtil.toJsonStr(treeNodes)));
}

After the above transformation, we have realized the customization of tree node attributes, and by the way, we have also realized the extensibility of attributes, achieving two goals with one stone.

3, summary

At present, some scenarios may not be satisfied, but most of the problem scenarios based on version 3.0 or 4.0 can be solved with a little modification. The rest is optimized and adjusted according to the scene.

4. Source & Video

springboot-tree

5. More exciting

I think it's OK. I like it when I move my fingers.

That's what we have today. I'll see you next.

More high-quality content, the first official account, like the south of the wind, welcome your attention.

Posted by ale8oneboy on Fri, 21 Feb 2020 02:26:49 -0800