一.前言
最近项目有实习的同事加入,很多实习同事反映,看不懂项目中的一些使用了设计模式的代码,比如HandleService,Chains,Listener等。本篇就介绍一下策略模式在项目中的使用,也就是我们常在项目中看到的XXXHandleService。
二.业务场景描述
假如有一个业务场景,我们要导入一个excel文件,excel文件有多个sheet页,每个sheet页的数据对应一套处理逻辑(比如sheet页A的数据解析完在数据库的A表进行存储,sheet页B的数据解析完成后要存到mongodb),每个sheet页的处理逻辑由一个开发同事进行开发。
三.代码
1.定义一个接口,接口约定两个方法
public interface ISheetService {
/**
* 执行方法,入参的sheetData代表一个sheet页中的数据
* @param sheetData
*/
void execut(Object sheetData);
/**
* 要求每个实现 ISheetService 的实现类都有一个 获取sheetname的方法
* @return
*/
String getSheetName();
}
2.两个业务实现类,他们都实现了上面定义的接口。且getSheetName方法各自返回该业务类要处理的sheet名称。
@Service
public class BiddingService implements ISheetService {
@Override
public void execut(Object sheetData) {
// TODO: 真实的业务逻辑
}
@Override
public String getSheetName() {
return "Bidding";
}
}
@Service
public class InviteTendersSheetService implements ISheetService {
@Override
public void execut(Object sheetData) {
// TODO: 真实的业务逻辑
}
@Override
public String getSheetName() {
return "Invite";
}
}
3.编写一个 ExcelSheetHandleService ,作为管理业务类的地方
@Service
public class ExcelSheetHandleService {
@Autowired
private List<ISheetService> sheetServices;
@Async
public void analysisSheetData(Map<String, Object> sheetName2Sheet) {
sheetName2Sheet.entrySet().forEach(entry->{
String sheetName = entry.getKey();
Object sheetData = entry.getValue();
sheetServices.stream()
.filter(service->service.getSheetName().equals(sheetName))
.findFirst()
.ifPresent(iSheetService -> iSheetService.execut(sheetData));
});
}
}
4.controller层:
controller层作为入口层,做了两件事,第一件事是将excel文件解析成了Map<sheetName, sheetData> 的格式,第二件事是调用 excelSheetHandleService的analysisSheetData方法。
@Autowired
private ExcelSheetHandleService excelSheetHandleService;
@PostMapping("/uploadExcel")
public R uploadExcel(@RequestParam("file") MultipartFile file) {
// 解析excel 将多sheet页的excel解析成 key: sheetName value -> sheet页数据 的格式
Map<String, Object> sheetName2Data = convertExcelData(file);
if (!CollectionUtils.isEmpty(sheetName2Data)) {
excelSheetHandleService.analysisSheetData(sheetName2Data);
}
return R.ok();
}
四. 代码讲解
1.代码运行流程
前端调用文件上传后,首先controller层中,将excel文件根据sheet解析成Map<String, Object>的格式,map的key是sheet页的名称,map的value是该sheet页的数据。
excel文件解析完成后,调用ExcelSheetHandleService 的执行方法。
再看ExcelSheetHandleService ,首先注意该类的属性,有一个List<ISheetService>,并且在上面加了@Autowired属性,这里其实是一个spring的特性,spring允许我们注入一个Collection,比如List<interface>,Map<class, Service>。比如我们这里的这个List<ISheetService>,其实就是让spring帮我们把所有实现了ISheetService这个接口的实现类都注入到这里了。
接着我们看一下ISheetService及它的两个实现类,ISheetService规定了实现类必须实现getSheetName这个方法,然后它的两个实现类分别实现这个方法,并返回一个字符串,该字符串和excel文件中的sheet名是一致的。
回到ExcelSheetHandleService 的 analysisSheetData方法,在方法中,我们利用java8的stream来遍历了我们注入的ISheetService的List,调用每一个ISheetService实现类的getSheetName方法,如果sheet名称和getSheetName的返回值一致,则认为该实现类就是用于处理该sheet的Service,然后再调用该service的真实业务执行方法execut。
五.这样写代码的好处
大家理解上述代码后会发现,其实以上代码完全可以用if else 或者 switch case完成,比如这样:
switch(sheetName) {
case "sheetNameA":
functionA();
break;
case "sheetNameB":
functionB();
break;
case DEFAULT;
throw new Exception("非法sheetName");
}
然后在functionA,functionB中写自己的逻辑。而且,这样写似乎还更好理解,既然如此,为何我们还要使用策略模式写HandleService,来让代码变得很复杂呐?
其实,在写代码时,除了完成需求,保障性能之外,我们还需要考虑很多其他的问题,诸如代码的可维护性,团队的合作。拿本篇举例的这个业务场景来说,当前需求是解析excel,读两个sheet页,那么之后很可能拓展成三个,四个sheet页。在代码拓展时,增加更多的case条件和function在一个service中,会让这个类文件越来越庞大,难以维护,而写一个新的service,既不侵入老代码,后面代码维护也方便。再说团队开发问题,当前我们是三个人做这个需求,如果大家都在一个java类文件中通过switch case来写代码,然后提交都在这一个文件,很容易出现冲突,导致不小心覆盖掉代码之类的问题,而每一个人写自己的service,则可以规避这种问题。