需求场景:
在一个管理系统中都有多个导出报表功能,通常后端开发人员是一个接口处理所有的导出功能,根据前端传递的类型进行判断是何种报表的导出功能,但是随着导出报表的需求越来越多后端的if-else 或者 switch代码块就会越来越长,本篇文章要导出的报表多达13种,通过前端传递的类型来进行区分导出的报表类型。
本篇文章一起探讨如何把这种导出功能优化
导出功能条件参数类:
public class ExportParam{
//1为园区库数据导出,2为专利库数据导出 ...
private Integer type;
// 其他条件参数
...
}
公共导出接口:
/**
* 管理员端大数据库导出
*/
@PostMapping("/lib/export")
@Auth(value = "admin_adminExport_export", name = "管理员端导出功能")
@ApiOperation("管理员端导出功能")
public void export(@RequestBody ExportParam exportParam, HttpServletResponse response) throws Exception {
CurrentUser currentUser = UserUtil.getCurrentUser();
exportService.export(exportParam,response,currentUser);
}
在本例中使用的导出工具是 alibaba 的 easyexcel工具:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.6</version>
</dependency>
数据映射到excel的类: 公共接口:BaseExcelModel
public interface BaseExcelModel {
}
这个接口的设计只是为了之后的多态设计,具体的表格映射类如下:
相信做过导出功能的朋友自然会明白这个类的作用了,这个类将对应这excel表的表头,数据等。数据库查询的数据需要转为为这个类的集合
导出功能实现:
导出实现类:
@Override
public void export(Integer type, HttpServletResponse response) throws Exception {
//将要导出的数据,来源数据库查询后转换为导出对象集合的数据
List exportList=null;
//导出表格的名称,根据类型不同导出表格的名称不同
String title="";
// 导出映射excle类,
BaseExcelModel obj=null;
switch (type) {
case 1 :
//数据来源数据库查询之后转化为导出类的对象集合,根据导出数据不同查询不同数据
exportList=getXXX();
title="xxx信息导出";
obj=new OriganizationStructureExcelModel();
break;
case 2 :
//数据来源数据库查询之后转化为导出类的对象集合,根据导出数据不同查询不同数据
exportList=getXXX();
title="xxx信息导出";
obj=new EnterpriseExcelModel();
break;
case 3 :
//数据来源数据库查询之后转化为导出类的对象集合,根据导出数据不同查询不同数据
exportList=getXXX();
title="xxx信息导出";
obj=new EnterpriseTownshipExcelModel();
break;
//往下还有很多个导出
default:
break;
}
if(exportList.size()>0 && obj!=null){
// 调用工具类的导出功能
ExportExcelUtil.writeExcel(response,exportList,title,"第一页",obj);
}
}
从上面的伪代码中只是展示了几个导出功能,随着导出功能的增多,这个switch 也会一直很长,而查询数据库数据的方法getXXX(),也全都是写在这个实现类里面,随着功能增多方法也很多,这个类代码行数也很多,虽然功能上是可以实现导出,但是对于阅读代码其实并不是很好。
以上代码缺点(个人观点):
- 所有查询数据的方法都在一个类中实现,而这个类主要功能是实现导出功能,数据查询应该写在其对应的数据查询实现类(xxxImpl)中(单一职责)
- switch 中cast的值使用枚举类型比较好
- 在case 中我们还要处理表格的名称和导出excelModel的类型,如果需要的其他信息可能还需要在每个case中做处理。
- 导出功能越多switch越长,在一个类中一个switch很长,代码很难看
导出功能优化第一版:
定义一个导出抽象父类:
/**
* 导出抽象父类
*/
public abstract class AbstractExport {
//导出方法需求子类去实现
public abstract void doExport(CurrentUser currentUser, ExportParam exportParam, HttpServletResponse response) throws Exception;
}
其实现类如图:
从上图中我们可以看到我们把各个导出功能都分离出来,每个导出功能单独实现,代码不拥堵在exportService类中。
每个导出实现类,继承父类重写方法:
@Component
public class ExpertAssessExport extends AbstractExport {
@Resource
DockMapper dockMapper;
@Autowired
private RequirementServiceImpl requirementService;
@Override
public void doExport(CurrentUser currentUser, ExportParam exportParam, HttpServletResponse response) throws Exception {
// 查询数据
List expertAssess = getExpertAssess(currentUser, exportParam);
// 调用工具类的导出方法
ExportUtils.excelWriter(response,expertAssess,"特派员绩效",new ExpertAssessExcelModel());
}
}
新增一个导出工具类ExportUtils:
在这个工具类中声明了获取导出实现类对象,和执行导出的公共方法,表格名称、导出数据、excel数据映射对象,这些数据都由导出实现类传入参数;
具体看这个 getOjb方法:
public Object getObj(Integer type){
switch (type){
case 1:
// 园区库导出
return industryParkExport;
case 2:
//专利库导出
return intellectualPropertyExport;
case 3:
//企业库/合作社库/乡镇库导出
return enterpriseExport;
case 4:
//科技特派员团队库
return expertTeamExport;
}
}
这个getObj()负责根据type返回实现导出功能的各个对象即可
原先的导出方法修改为:
@Override
public void export(ExportParam exportParam,HttpServletResponse response,CurrentUser currentUser) throws Exception {
/**
* 1.导出已经优化为继承AbstractExport 并重写其doExport()方法,并需要在子类上添加@Component 注解
* 2.在ExportUtils 类getObj中返回实现类对象,
*/
AbstractExport obj = (AbstractExport)exportUtils.getObj(exportParam.getType());
if(obj!=null){
obj.doExport(currentUser,exportParam,response);
}
}
}
以上优化中把各个导出功能的实现都分离出来,当某个导出功能变动时只需要修改其重写方法即可,但是在这个版本中添加导出功能时需要新增一个导出功能类并继承实现父类的抽象方法,且需要在ExportUtils 类的getObj()方法中添加一个case 块。虽然switch也会越来越长但case块中的代码都很单一而且很简洁,只需要返回实现类的对象即可。
导出功能优化第二版
以上代码可能相对于第一个版本来说简洁了许多,但是随着导出功能的增加 switch块也会越来越长,依旧觉得代码不是很完美。事实上去可是使用Map来代替Switch块,实现思路是把所有的导出功能实现类对象存入一个map中,key就是type类型,value就是其对应的导出实现类对象。之后获取对象时map.get(type)即可,但是问题是该何时把这些对象存入到一个map中呢?
其实也不难想到,
- 在我们的ExportUtils类上添加上@Component注解
- 在类中提供一个map类型的成员变量用于存储对象
- 写一个方法把对象put到map中,并在方法上添加@Bean注解,这样在容器扫描到@Bean注解时就会执行该方法,自然也就把各个导出对象存入到map中了
修改代码如下:
@Component
public class ExportUtils {
//导出实现类的map,key为导出的类型,value为实现导出功能的对象
public ConcurrentHashMap<Integer,Object> map=new ConcurrentHashMap<>();
@Bean
public void inputObjToMap(){
// 园区库导出
map.put(1,industryParkExport);
//专利库导出
map.put(2,intellectualPropertyExport);
//企业库/合作社库/乡镇库导出
map.put(3,enterpriseExport);
//....
}
public Object getObj(Integer type){
return map.get(type);
}
}
以上代码使用了ConcurrentHashMap 类型的数据结构,在这里使用HashMap也是可以达到所想要的效果,经过以上优化之后确实是鄙弃了Switch代码块。新增导出功能时,只需要在ExportUtils类中引入实现类,再在inputObjToMap()方法中put一个类型和对应的对象即可,也不再看到一个Switch代码又十几个case的情况了。
经过以上的代码优化还剩下一个问题就是:导出的类型应该使用枚举类来管理,相信大家对枚举类都很知晓在此就不必再演示和赘述了。
最后反思
在这样的需求场景下 是需要每一个导出功能一个接口呢?还是像本篇文章所述使用同一个导出接口,通过参数来区分导出数据类型?反思以上代码还可以如何简化呢?或者说这样优化是否有必要呢?