实战学习设计模式(一)

115 阅读6分钟

前言

最近阅读了一下spring源码,然后再看了一下自己之前写的代码,感觉就像在看一坨shi(我tm怎么会写出这么烂的代码?)

碰巧,最近项目上,做了一些有点复杂的东西,便把之前学习到的设计模式用上了,改完之后,整个人都清爽了

需求描述

在某款app上,官方可以组织比赛(Game),用户可以以组队(Team)的形式去参加

关于比赛,有两种类型:

  1. 成绩类型,比如:答题,走路。特点:有明确的,可量化的成绩,成绩越好,名次越靠前

  2. 其他类型,比如:发帖讨论。特点:没有明确的成绩,胜利者由管理员手动指定

其中,有一个排行榜的列表,

  1. 如果比赛是类型1,那么就按照成绩排名,并给出具体的名次。
  2. 如果比赛是类型2,那么就按照队伍名字排(没有名次),但如果管理员选择了胜利者,那么这几个胜利者队伍,会获取对应的名次(1、2、3...)
  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;
  }
}

采用设计模式改造后的写法

问题分析

首先,我们来分析一下,为什么这个方法被写成了屎山

  1. game类型为1和2,排名方式完全不同,但是却写到一个方法种,用if来分开

  2. 构建vo逻辑也不尽相同,game类型、是否有keyword,都影响了ranking值的设置方法,但是这些不同分支的逻辑,全都放到一起去了

这么做的坏处是什么呢?首先是阅读困难,一大堆if...else,不仔细读根本看不出业务流程;其次,不同分支的业务杂糅在一起,改动一个分支,难免会影响到别的分支逻辑;

代码展示

首先是,改造后的rank方法,这里做了两个抽象:

  1. 查询原始的team实体类
  2. 将实体类构建成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;
  }

}

总结

  1. 经过改造后,代码逻辑便没有纠缠在一起,在排查问题时,只要根据Game.typekeyword的具体情况,就可以找到对应的实现类,从而定位问题。

  2. 可能细心的朋友会发现,这段代码逻辑存在bug(score相同时,名次会有问题),不过这是写的demo代码,重点是讲解设计模式,也就没有去处理了。实际项目中,用到了一个比 select count(*) from team where score > #{score} 复杂得多的sql来做

  3. 用到的设计模式:策略模式、模板方法、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;
  }
}