背景
公司主要做数据治理相关的产品,奈何初期为了追求速度留下了一些饮鸩止渴的设计,后期影响最大的就是文件夹这块,治理数据首先要归档数据,通过文件夹的方式来将同一业务属性的数据收拢到一起。
前期的业务逻辑简单,简单的结构足以满足文件夹树查询、搜索等等需求,但发展到后期越来越多的满足文件夹结构但又不是文件夹的操作增加,比如分类、比如主题等等虽然有父子级关系但数据表不一致的情况。
导致前后端交互上同一个业务逻辑产生不同的数据结构,而且接口也不统一,有些在python服务上,有些在java服务上,每次开发都要写很多重复逻辑的代码复用性很低不通用。
于是乎,高层在某天召开会议,会议内容主要确定全面深化改革文件夹功能,并提出三个全面
全面抽象实体
全面建设低耦合、高内聚、易扩展接口
全面优化加速接口速度
作为新时代夹娃工程师,一定不能辜负组织上交给我的任务,虽然我深知越恶心的功能重构起来坑越多,但这些都难不倒勤劳勇敢的打工人(领导答应搞好给高绩效)。
在之前的代码上优化属于是屎上雕花了,革命就要流血,这次准备直接遗弃掉旧代码,重新抽象好整体文件夹基架。不管业务中到底是不是文件夹都抽象成文件夹,只要文件夹满足父子级关系、并且绑定了一些文件都走同一个逻辑。
全面抽象文件夹实体
标准的数据库结构主要就是三张表,FOLDER表存储文件夹信息、FOLDER_REL表存储文件夹与文件关联表、FILE表存储文件信息。
改版文件夹操作第一步就得先定义出抽象文件夹来,不管数据库中的代码结构和业务含义,只要是可以被抽象成文件夹的都可以按统一的文件夹操作。
主要功能就是抽象代码,把每一步都剥离出来,搞一个策略模式,抽出一个AbstractFolderService抽象类定义方法,提供一个FolderBaseServiceImpl以标准的FOLDER表做基础实现。
方法入参封装一个查询基本信息对象,
@Data
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public static class FolderKey {
@ApiModelProperty(value = "文件夹id")
private String folderId;
@ApiModelProperty(value = "操作文件夹类型")
private Integer type;
@ApiModelProperty(value = "企业域id做数据隔离")
private String entId;
}
复制代码
Abstract层接下来定义各种通用方法,AbstractTreeService<T extends FolderFile, P extends FolderFileRel, Q extends FolderInfo>这个先是后面会提到的文件夹树逻辑,以及如果不是标准FOLDER对象的话只需要定义对应的泛型即可开箱即用
public abstract class AbstractFolderService extends AbstractTreeService<T extends FolderFile, P extends FolderFileRel, Q extends FolderInfo> {
// 检查文件夹名称是否存在,新建时使用
public abstract void checkFolderNameExist(FolderBaseForm.FolderKey folderKey, String folderName);
// 获取文件夹详情,Q为集成了抽象文件夹的泛型
public abstract Q checkFolderIdAndGetInfo(FolderBaseForm.FolderKey folderKey);
// 获取文件夹最大序号用于排序
public abstract Integer getMaxSeqNo(FolderBaseForm.FolderKey folderKey);
// 保存文件夹
public abstract String saveFolder(Q folderInfo);
// 删除文件夹
public abstract void deleteFolderFromDb(List<FolderBaseForm.FolderKey> folderIdList);
// 删除文件夹与文件关联关系
public abstract void deleteFolderFileRelFromDb(FolderDto.FolderRelBatchDel folderRelBatchDel);
// 获取文件与文件夹关联关系
public abstract FolderBaseForm.FolderKey getFolderByFile(String fileId, String userId, String entId, Integer treeType,
String libFolderId);
// 获取子文件夹
public abstract List<String> getSubFolderList(FolderBaseForm.FolderKey folderKey, boolean includeSub);
// 获取指定文件夹下的文件
public abstract List<String> getFileListFromFolder(FolderBaseForm.FolderKey folderKey, boolean includeSub);
// 更新文件夹与文件关联关系
public abstract void updateFolderFileRel(String fileId, FolderBaseForm.FolderKey folderKey, Integer seqNo);
// 更新文件夹folder-path(便于查询用的folder_id链路)
public abstract void updateFolderPath(FolderBaseForm.FolderKey folderKey, String oldFolderPath, String newFolderPath);
// 更新父级文件夹
public abstract void updateFolderParentIdAndSeqNo(FolderBaseForm.FolderKey folderKey, String parentFolderId, Integer seqNo);
// 更新文件夹基础信息
public abstract void updateFolderNameAndComment(FolderBaseForm.FolderKey folderKey, String folderName, String comment);
}
复制代码
抽象好基础文件夹操作后实现一份标准文件夹实现类去实现抽象出来的基础方法
public class FolderBaseServiceImpl extends AbstractFolderService {
}
复制代码
如此一来所有后续新加的文件夹操作都可以继承FolderBaseServiceImpl, 把基础的方法都继承过来,如果在业务上有区别的话可以单独重写这个方法,在上层去调用的时候是无感知的。
全面优化加速接口速度
增删改架子搞好之后就该查了,查询可谓是重中之重的功能,特别是构造树的逻辑,回想当时在python代码实现的树逻辑上debug。。。
用户打开数据管理页面第一个接口加载,加载速度直接影响了用户体验,所以这块设计上也需要琢磨一下。可以简单的将树查询拆成三块:
-
查询所有指定类型的文件夹
-
判断查询是否要携带文件,不需要直接过
- 查询所有文件夹下的文件关联关系
- 拿到关联关系的file_ids查询所有文件信息
-
根据父子关联字段组成树
简单来讲就是还需要一个抽象类去实现基础的拼装树逻辑,这里不仅需要实现方法还要用泛型抽象各种业务场景下的文件夹、文件夹关联关系、文件。
/*
*抽象文件
*/
@Data
@SuperBuilder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
public static class FolderFile {
private String fileId;
private String entId;
}
/**
* 抽象文件夹(folder、topic、category)
*/
public static class FolderInfo {
private String folderId;
private String folderName;
private String comment;
private Integer seqNo;
private String parentId;
private String entId;
private String userId;
private Integer treeType;
private String folderPath;
private String creator;
private String englishName;
}
/**
* 抽象文件夹与工作关系
*/
@Data
@SuperBuilder(toBuilder = true)
public static class FolderFileRel {
private String folderId;
private String fileId;
}
复制代码
这样就可以使用泛型来统一整个操作,规定使用这套框架只需要继承FolderFile、FolderInfo、FolderFileRel即可。
那么抽象出统一的树结构代码
public abstract class AbstractTreeService<T extends FolderFile, P extends FolderFileRel, Q extends FolderInfo> {
// 获取所有文件夹
public abstract List<? extends Q> getFolderInfos(String entId, Integer treeType);
// 获取所有文件夹关联关系
public abstract List<? extends P> getFolderFileRel(String entId, Integer treeType);
// 获取所有文件详情
public abstract List<? extends T> getFiles(List<String> fileIds, String entId);
}
复制代码
树结构代码中只处理拿到各类信息后组成树的逻辑,具体如何拿到各类信息集合需要各个实现类去实现,这样的话我们之前定义的AbstractFolderService抽象基础文件夹操作类就可以继承AbstractTreeService树结构类,这三个方法既可以在FolderBaseServiceImpl得到实现。
如何定义通用树结构的返回类型,不同场景下返回的信息也不同,比如专题场景下没有英文名那么通用VO中返回一个english_name为null也不合适,只能再使用泛型来定义。
public static class FolderTree<T, E extends FolderInfo> {
private E folderInfo;
// 详细内容通过泛型定义保证通用
private List<FolderTree<T, ? extends FolderInfo>> subFolder;
private List<T> fileList;
}
复制代码
结构分成三部分,本级文件夹详情、子级文件夹列表、本级文件夹关联文件列表。具体的转树逻辑就不贴了,本质上是一个深度优先搜索dfs的过程,根据parent_id形成父子级关联关系。
全面建设易扩展接口
整完这俩大块逻辑其实在大部分场景下都适用了,但是我们这个场景下还不够,因为本次改动主要是后端模块改动,尽量不要避免返回的数据结构发生变化,但是上述的树查询逻辑中就已经把VO都改掉了,还需要一步转成旧VO的逻辑。
那就再定义一个旧版VO的基础类,因为旧版的逻辑也并不是全都统一的,那就在原来的基础上再加一个转换外部旧版VO的泛型。
public abstract class AbstractTreeService<T extends FolderFile, P extends FolderFileRel, Q extends FolderInfo> {
// 获取所有文件夹
public abstract List<? extends Q> getFolderInfos(String entId, Integer treeType);
// 获取所有文件夹关联关系
public abstract List<? extends P> getFolderFileRel(String entId, Integer treeType);
// 获取所有文件详情
public abstract List<? extends T> getFiles(List<String> fileIds, String entId);
// 转换外部VO
public abstract G convertGlobalTreeVo(FolderTree<T, ? extends FolderInfo> rootTreeNode);
}
复制代码
转换逻辑还是交由实现类处理,不同的业务场景需要的字段也不一样,只需要定义一个全局抽象旧版外部VO类
public static class GlobalBaseTreeVo {
private String folderId;
private String parentId = "";
@JsonProperty("sub_levels")
@Builder.Default
private List<? super GlobalBaseTreeVo> subLevels = new ArrayList<>();
}
复制代码
转换旧VO逻辑使用广度优先算法bfs实现,这里贴一下吧,自从不做ACM之后很久没有写算法代码了,偶尔逮到一两个场景实现出来还是有些恍如隔世的感觉。
public G bfsConvertVo(FolderTree<T, FolderTreeDto.FolderInfo> innerRootTreeNode) {
Map<String, G> parentNodeMap = new HashMap<>();
G resultVo = this.convertGlobalTreeVo(innerRootTreeNode);
parentNodeMap.put(resultVo.getFolderId(), resultVo);
LinkedBlockingQueue<FolderTree<T, ? extends FolderInfo>> bfsQueue = new LinkedBlockingQueue<>(innerRootTreeNode.getSubFolder());
while (!bfsQueue.isEmpty()) {
FolderTree<T, ? extends FolderInfo> firstObj = bfsQueue.poll();
G currentNode = this.convertGlobalTreeVo(firstObj);
G parentNode = parentNodeMap.get(currentNode.getParentId());
parentNode.getSubLevels().add(currentNode);
parentNodeMap.put(currentNode.getFolderId(), currentNode);
bfsQueue.addAll(firstObj.getSubFolder());
}
return resultVo;
}
复制代码
如此一来这套结构可以满足公司所有类文件夹结构的增删改查逻辑的处理,树查询接口从3s优化到200ms,代码结构清晰,后期在次基础上开发新的文件夹功能也非常简单。
高绩效我拿定了!
作者:在下uptown
链接:https://juejin.cn/post/7201526458397196348