前言
最近阅读了一下spring源码,然后再看了一下自己之前写的代码,感觉就像在看一坨shi(我tm怎么会写出这么烂的代码?)
碰巧,最近项目上,做了一些有点复杂的东西,便把之前学习到的设计模式用上了,改完之后,整个人都清爽了
需求描述
在某款app上,官方可以组织比赛(Game),用户可以以组队(Team)的形式去参加
关于比赛,有两种类型:
-
成绩类型,比如:答题,走路。特点:有明确的,可量化的成绩,成绩越好,名次越靠前
-
其他类型,比如:发帖讨论。特点:没有明确的成绩,胜利者由管理员手动指定
其中,有一个排行榜的列表,
- 如果比赛是类型1,那么就按照成绩排名,并给出具体的名次。
- 如果比赛是类型2,那么就按照队伍名字排(没有名次),但如果管理员选择了胜利者,那么这几个胜利者队伍,会获取对应的名次(1、2、3...)
- 前端可以传关键词keyword过来,对队伍的名字做模糊匹配
基础设计
首先是相关的几个实体类
/**
* 比赛实体
*/
@Data
public class Game {
//主键
private Integer id;
//标题
private String title;
//内容描述
private String content;
//比赛类型:1 走路步数 2 其他(无具体成绩)
private Integer type;
//管理员选择的胜利者
private String winners;
//管理员id
private Integer adminId;
}
/**
* 队伍实体
*/
@Data
public class Team extends Model<Team> {
//主键
private Integer id;
//名字
private String name;
//成绩
private Integer score;
//上次同步数据的时间
private Long lastSyncTime;
//比赛id
private Integer gameId;
//队长id
private Integer captainId;
}
/**
* 用户类
*/
@Data
public class GameUser {
private Integer id;
private String name;
}
然后是Service接口
public interface TeamService {
/**
* 比赛排行榜,根据{@link Game#type}的不同,逻辑有所区别
* 1. type = 1,根据成绩排名
* 2. type = 2,按照名字升序(没有具体名次),如果管理员选择了胜利者,这几个队伍将获得名次
*/
PageResult<TeamVo> rank(String keyword, Integer gameId, int pageNum, int pageSize);
}
mapper类
@Mapper
public interface TeamMapper extends BaseMapper<Team> {
/**
* 查询有多少队伍成绩比这个成绩更好
*/
@Select("select count(*) from team where score > #{score}")
int selectBetterCount(@Param("score") Integer score);
}
常规形式的排行榜
先看一下常规的写法(大多数人应该都写过这种初(shi)级(shan)代码),是不是有种熟悉又嫌弃的感觉
/**
* 简单实现类,常规的MVC事务脚本式的代码逻辑
*/
@Service
public class SimpleTeamServiceImpl extends ServiceImpl<TeamMapper, Team> implements TeamService {
@Autowired
private TeamMapper teamMapper;
@Autowired
private GameService gameService;
@Autowired
private GameUserService gameUserService;
@Override
public PageResult<TeamVo> rank(String keyword, Integer gameId, int pageNum, int pageSize) {
Game game = gameService.get(gameId);
//查询entity
LambdaQueryWrapper<Team> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Team::getGameId, gameId);
if (StringUtils.isNotBlank(keyword)) {
wrapper.like(Team::getName, "%" + keyword + "%");
}
Page<Team> teamPage;
String winners = game.getWinners();
//有成绩的类型
List<Integer> winnerIds = new ArrayList<>();
if (winners != null) {
Arrays.stream(winners.split(",")).map(Integer::valueOf).forEach(winnerIds::add);
}
if (game.getType() == 1) {
wrapper.orderByDesc(Team::getScore);
Page<Team> pg = new Page<>();
pg.setCurrent(pageNum);
pg.setSize(pageSize);
teamPage = teamMapper.selectPage(pg, wrapper);
}
//没有成绩的类型
else if (game.getType() == 2) {
wrapper.orderByDesc(Team::getName);
//管理员没有选择胜利者
if (winners == null || winners.isEmpty()) {
Page<Team> pg = new Page<>();
pg.setCurrent(pageNum);
pg.setSize(pageSize);
teamPage = teamMapper.selectPage(pg, wrapper);
}
//管理员选择了胜利者
else {
//查出胜利者队伍
List<Team> winnerTeams = listByIds(winnerIds);
//查出其他,因为不确定胜利者在按照名字排序的列表中处于什么位置,因此要把前pageNum页数据全查出来
Page<Team> pg = new Page<>();
pg.setCurrent(1);
pg.setSize((long) pageNum * pageSize);
Page<Team> namePage = teamMapper.selectPage(pg, wrapper);
for (Team team : namePage.getRecords()) {
if (!winnerIds.contains(team.getId())) {
winnerTeams.add(team);
}
}
List<Team> list = winnerTeams.stream().skip((pageNum - 1) * pageSize).limit(pageSize)
.collect(Collectors.toList());
namePage.setRecords(list).setCurrent(pageNum).setSize(pageSize);
teamPage = namePage;
}
} else {
throw new RuntimeException("仅支持类型为1、2的挑战");
}
//-_-经历50行,终于把分页的原始entity查出来了,现在开始构建vo
List<Team> teams = teamPage.getRecords();
//收集队长信息
Set<Integer> captainIds = teams.stream().map(Team::getCaptainId).collect(Collectors.toSet());
Map<Integer, GameUser> captainUserMap = gameUserService.getAll(captainIds);
//构建vo
MutableInt initRanking = new MutableInt(0);
if (game.getType() == 1 && keyword == null) {
initRanking.set(teamMapper.selectBetterCount(teams.get(0).getScore()) + 1);
}
List<TeamVo> voList = teams.stream().map(team -> {
TeamVo vo = new TeamVo();
BeanUtils.copyProperties(team, vo);
vo.setCaptain(captainUserMap.get(team.getCaptainId()));
//最“恶心”的点,要设置ranking值 (这么多if你怕不怕)
if (game.getType() == 1) {
//有keyword为null,那么这些队伍的排名是连续的
if (keyword == null) {
vo.setRanking(initRanking.get());
initRanking.add(1);
}
//因为有keyword模糊搜索,因此这些队伍的排名,可能不是连续的,因此每个team都要单独去查一次
else {
vo.setRanking(teamMapper.selectBetterCount(team.getScore()) + 1);
}
} else if (game.getType() == 2) {
//只有管理员选择了的胜利者才需要rank值
if (winners != null) {
int i = winnerIds.indexOf(team.getId());
if (i != -1) {
vo.setRanking(i + 1);
}
}
}
return vo;
}).collect(Collectors.toList());
PageResult<TeamVo> result = new PageResult<>();
result.setData(voList);
result.setTotal(teamPage.getTotal());
result.setHasMore(teamPage.getTotal() > ((long) pageNum * pageSize));
//至此,一个接近100行的排行榜方法写完了,第一眼什么感觉(这tm谁写的shi山)
//然而这还只是简化版的,我工作中遇到的项目,比赛还有各种阶段(报名、比赛、结束),每个阶段查询逻辑还各不一样
return result;
}
}
采用设计模式改造后的写法
问题分析
首先,我们来分析一下,为什么这个方法被写成了屎山
-
game类型为1和2,排名方式完全不同,但是却写到一个方法种,用if来分开
-
构建vo逻辑也不尽相同,game类型、是否有keyword,都影响了ranking值的设置方法,但是这些不同分支的逻辑,全都放到一起去了
这么做的坏处是什么呢?首先是阅读困难,一大堆if...else,不仔细读根本看不出业务流程;其次,不同分支的业务杂糅在一起,改动一个分支,难免会影响到别的分支逻辑;
代码展示
首先是,改造后的rank方法,这里做了两个抽象:
- 查询原始的team实体类
- 将实体类构建成vo
改造后的rank方法,不到10行,而且可以很清晰的看出代码逻辑:先查出实体类,再构建成vo
/**
* 采用了设计模式改造后的实现类
*/
@Service
public class DesignedTeamServiceImpl implements TeamService {
@Autowired
private TeamRankSelectorFactory teamRankSelectorFactory;
@Autowired
private GameService gameService;
@Autowired
private GameUserService gameUserService;
@Autowired
private TeamVoBuilderFactory teamVoBuilderFactory;
@Override
public PageResult<TeamVo> rank(String keyword, Integer gameId, int pageNum, int pageSize) {
Game game = gameService.get(gameId);
//此处为策略模式:根据实际情况,选择一个对应的策略
TeamRankSelector selector = teamRankSelectorFactory.getSelector(game);
//相比之前的实现方案,这里两行代码就完事儿,想要调代码,直接根据game类型,进入对应的selector实现类即可
PageResult<Team> teamPage = selector.selectPage(keyword, game, pageNum, pageSize);
//此处为builder模式
TeamVoBuilder builder = teamVoBuilderFactory.getBuilder(game, keyword);
//收集builder必须的属性
Set<Integer> captainUserIds = teamPage.getData().stream().map(Team::getCaptainId)
.collect(Collectors.toSet());
builder.setCaptainUsers(gameUserService.getAll(captainUserIds));
List<TeamVo> vos = builder.build();
return teamPage.transData(vos);
}
}
我们先来看一下TeamRankSelector,它只是一个interface,我根据Game的类型,写了不同的实现类
/**
* 查询分页数据的原始entity分页信息
*/
public interface TeamRankSelector {
PageResult<Team> selectPage(String keyword, Game game, int pageNum, int pageSize);
}
类型1(有具体成绩的game)
@Component
public class Type1TeamRankSelector implements TeamRankSelector {
@Autowired
private TeamMapper teamMapper;
@Override
public PageResult<Team> selectPage(String keyword, Game game, int pageNum, int pageSize) {
LambdaQueryWrapper<Team> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Team::getGameId, game.getId());
wrapper.orderByDesc(Team::getScore);
Page<Team> pg = new Page<>();
pg.setCurrent(pageNum);
pg.setSize(pageSize);
Page<Team> teamPage = teamMapper.selectPage(pg, wrapper);
return new PageResult<>(teamPage);
}
}
类型2(没有具体成绩的game)
/**
* type2类型的分页查询器,虽然代码量没减少,但是明显减少了if的数量
*/
@Component
public class Type2TeamRankSelector implements TeamRankSelector {
@Autowired
private TeamMapper teamMapper;
@Override
public PageResult<Team> selectPage(String keyword, Game game, int pageNum, int pageSize) {
String winners = game.getWinners();
//管理员没有选择胜利者
if (winners == null || winners.isEmpty()) {
return withNoWinners(game, pageNum, pageSize);
}
//管理员选择了胜利者
else {
return withWinners(game, pageNum, pageSize, winners);
}
}
private PageResult<Team> withWinners(Game game, int pageNum, int pageSize, String winners) {
List<Integer> winnerIds = Arrays.stream(winners.split(",")).map(Integer::valueOf)
.collect(Collectors.toList());
//查出胜利者队伍
List<Team> winnerTeams = teamMapper.selectBatchIds(winnerIds);
//查出其他,因为不确定胜利者在按照名字排序的列表中处于什么位置,因此要把前pageNum页数据全查出来
LambdaQueryWrapper<Team> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Team::getGameId, game.getId());
Page<Team> pg = new Page<>();
pg.setCurrent(1);
pg.setSize((long) pageNum * pageSize);
Page<Team> namePage = teamMapper.selectPage(pg, wrapper);
for (Team team : namePage.getRecords()) {
if (!winnerIds.contains(team.getId())) {
winnerTeams.add(team);
}
}
List<Team> list = winnerTeams.stream().skip((long) (pageNum - 1) * pageSize).limit(pageSize)
.collect(Collectors.toList());
namePage.setRecords(list).setCurrent(pageNum).setSize(pageSize);
return new PageResult<>(namePage);
}
private PageResult<Team> withNoWinners(Game game, int pageNum, int pageSize) {
LambdaQueryWrapper<Team> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Team::getGameId, game.getId());
Page<Team> pg = new Page<>();
pg.setCurrent(pageNum);
pg.setSize(pageSize);
Page<Team> teamPage = teamMapper.selectPage(pg, wrapper);
return new PageResult<>(teamPage);
}
}
观察一下,可以发现,其实整体代码量并没有减少,但是通过不同实现类,来隔离了不同业务逻辑之间的区别
当然,SelectorFactory也必不可少
//此处为工厂模式
@Component
public class TeamRankSelectorFactory {
@Autowired
private Type1TeamRankSelector type1TeamRankSelector;
@Autowired
private Type2TeamRankSelector type2TeamRankSelector;
public TeamRankSelector getSelector(Game game) {
switch (game.getType()) {
case 1:
return type1TeamRankSelector;
case 2:
return type2TeamRankSelector;
default:
return null;
}
}
}
至此,完成了对数据库实体类查询的改造,下面我们接着看TeamVoBuilder
首先是基类,它只负责公共部分的逻辑(任何挑战类型的队伍,都要设置这些属性)
/**
* 模板方法+适配器:prepareBuild()和processVo(),都是交由子类去实现
* 这两个方法,都没有写成abstract,因为考虑到并不是所有子类都需要实现这两个方法(参考适配器模式)
*/
@Setter
public abstract class TeamVoBuilder {
protected List<Team> teams;
protected Map<Integer, GameUser> captainUsers;
protected Game game;
public List<TeamVo> build(){
prepareBuild();
List<TeamVo> vos = new ArrayList<>(teams.size());
for (Team team : teams) {
TeamVo vo = buildOne(team);
processVo(vo, team);
vos.add(vo);
}
return vos;
}
protected TeamVo buildOne(Team team) {
TeamVo vo = new TeamVo();
BeanUtils.copyProperties(team, vo);
vo.setCaptain(captainUsers.get(team.getCaptainId()));
return vo;
}
protected void prepareBuild(){
}
protected void processVo(TeamVo vo, Team team) {
}
}
然后看它的一些实现类,Game类型为1,且没有keyword的场景
/**
* game类型是1且没有带keyword的vo builder
*/
public class SimpleType1TeamVoBuilder extends TeamVoBuilder {
private TeamMapper teamMapper;
private int initRankingVal;
public SimpleType1TeamVoBuilder(TeamMapper teamMapper) {
this.teamMapper = teamMapper;
}
@Override
protected void prepareBuild() {
//这里将第一个队伍(成绩最好)拿出来,查出比它成绩好的有多少队伍,并作为初始的ranking值
Team team = teams.get(0);
this.initRankingVal = teamMapper.selectBetterCount(team.getScore());
}
@Override
protected void processVo(TeamVo vo, Team team) {
//因为队伍排名是连续的,因此这个值做累加即可
vo.setRanking(initRankingVal++);
}
}
下面是game类型为1,但有keyword的场景
/**
* game类型是1,且有keyword关键词搜索的builder
*/
public class KeywordType1TeamVoBuilder extends TeamVoBuilder {
private TeamMapper teamMapper;
public KeywordType1TeamVoBuilder(TeamMapper teamMapper) {
this.teamMapper = teamMapper;
}
@Override
protected void processVo(TeamVo vo, Team team) {
//因为有keyword模糊匹配,因此这些队伍的名次并不连续,所以每只队伍都要单独去查
int ranking = teamMapper.selectBetterCount(team.getScore()) + 1;
vo.setRanking(ranking);
}
}
最后是game类型为2的场景
/**
* game类型是2的vo builder
*/
public class Type2TeamVoBuilder extends TeamVoBuilder {
private Map<Integer, Integer> teamRanking;
@Override
protected void prepareBuild() {
//如果管理员选择了胜利者,那么它们的名次就是1,2,3..,这里提前解析出来,供后续使用
teamRanking = new HashMap<>();
String winners = game.getWinners();
if (winners != null) {
String[] split = winners.split(",");
for (int i = 0; i < split.length; i++) {
teamRanking.put(Integer.valueOf(split[i]), i + 1);
}
}
}
@Override
protected void processVo(TeamVo vo, Team team) {
vo.setRanking(teamRanking.get(team.getId()));
}
}
三种实现类都完成了,最后登场的,是TeamVoBuilderFactory
@Component
public class TeamVoBuilderFactory {
@Autowired
private TeamMapper teamMapper;
/**
* 这里虽然有很多if,但每个if都只有简单的一行代码,因此阅读起来并不复杂
*/
public TeamVoBuilder getBuilder(Game game, String keyword) {
if (game.getType() == 1) {
if (keyword == null) {
return new SimpleType1TeamVoBuilder(teamMapper);
} else {
return new KeywordType1TeamVoBuilder(teamMapper);
}
} else if (game.getType() == 2) {
return new Type2TeamVoBuilder();
}
return null;
}
}
总结
-
经过改造后,代码逻辑便没有纠缠在一起,在排查问题时,只要根据Game.type和keyword的具体情况,就可以找到对应的实现类,从而定位问题。
-
可能细心的朋友会发现,这段代码逻辑存在bug(score相同时,名次会有问题),不过这是写的demo代码,重点是讲解设计模式,也就没有去处理了。实际项目中,用到了一个比 select count(*) from team where score > #{score} 复杂得多的sql来做
-
用到的设计模式:策略模式、模板方法、builder、工厂,具体用到的地方,都在代码里面有注释
补充
补充一些上面没有贴出来的类和依赖
这里主要用到了mybatis-plus和lombok
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
@Data
public class PageResult<T> {
private long total;
private boolean hasMore;
private List<T> data;
public PageResult() {
}
public PageResult(long total, boolean hasMore, List<T> data) {
this.total = total;
this.hasMore = hasMore;
this.data = data;
}
public PageResult(Page<T> page) {
this.total = page.getTotal();
this.hasMore = page.getCurrent() * page.getSize() < this.total;
this.data = page.getRecords();
}
public <E> PageResult<E> transData(List<E> list) {
PageResult<E> pr = new PageResult<>();
pr.total = this.total;
pr.hasMore = this.hasMore;
pr.data = list;
return pr;
}
}