如何从业务代码中抽离出可复用的微组件

背景

很多业务代码,掺杂着一些通用的大段逻辑;容易导致的后果是,当需要类似功能时,不得不重新写一道,或者复制出几乎相同的代码块,让系统的无序性蹭蹭蹭往上涨。

具有良好抽象思维的有心的开发者,则会仔细观察到这种现象,将这些通用的大块逻辑抽离出来,做成一个可复用的微组件,使得以后再做类似的事情,只需要付出很小的工作即可。

那么,如何从业务代码中抽离出可复用的微组件,使得一类事情只需要做一次,今后可以反复地复用呢? 本文将以一个例子来说明。

在业务开发中,常常需要根据一批 id 查到相对应的 name 。比如根据一批员工ID查到员工的姓名,根据一批类目ID查到类目的名称,诸如此类。从叙述上看,就能感受到其中的相似性,那么如何将这种相似性抽离出来呢?

初步代码

假设要根据一批类目ID来获取相应的类目名称。大多数开发者都可以写出满足业务需求的代码:

@Component("newCategoryCache")
public class NewCategoryCache {
  private static Logger logger = LoggerFactory.getLogger(NewCategoryCache.class);

  /**
   * 类目ID与名称映射关系的缓存
   * 假设每个类目信息 50B , 总共 50000 个类目,
   * 那么总占用空间 2500000B = 2.38MB 不会造成影响
   */
  private Map<Long, String> categoryCache = new ConcurrentHashMap<>();

  @Resource
  private CategoryBackService categoryBackService;

  @Resource
  private MultiTaskExecutor multiTaskExecutor;

  public Map<Long, String> getCategoryMap(List<Long> categoryIds) {

    List<Long> undupCategoryIds = ListUtil.removeDuplicate(categoryIds);

    List<Long> unCached = new ArrayList<>();
    Map<Long,String> resultMap = new HashMap<>();
    for (Long categoryId: undupCategoryIds) {
      String categoryName = categoryCache.get(categoryId);
      if (StringUtils.isNotBlank(categoryName)) {
        resultMap.put(categoryId, categoryName);
      }
      else {
        unCached.add(categoryId);
      }
    }

    if (CollectionUtils.isEmpty(unCached)) {
      return resultMap;
    }

    Map<Long,String> uncacheCategoryMap = getCategoryMapFromGoods(unCached);
    categoryCache.putAll(uncacheCategoryMap);
    logger.info("add new categoryMap: {}", uncacheCategoryMap);
    resultMap.putAll(uncacheCategoryMap);

    return resultMap;

  }

  private Map<Long,String> getCategoryMapFromGoods(List<Long> categoryIds) {
    List<CategoryBackModel> categoryBackModels = multiTaskExecutor.exec(categoryIds,
        subCategoryIds -> getCategoryInfo(subCategoryIds), 30);
    return StreamUtil.listToMap(categoryBackModels, CategoryBackModel::getId, CategoryBackModel::getName);
  }

  private List<CategoryBackModel> getCategoryInfo(List<Long> categoryIds) {
    CategoryBackParam categoryBackParam = new CategoryBackParam();
    categoryBackParam.setIds(categoryIds);
    ListResult<CategoryBackModel> categoryResult = categoryBackService.findCategoryList(categoryBackParam);
    logger.info("categoryId: {}, categoryResult:{}", categoryIds, JSON.toJSONString(categoryResult));
    if (categoryResult == null || !categoryResult.isSuccess()) {
      logger.warn("failed to fetch category: categoryIds={}", categoryIds);
      return new ArrayList<>();
    }
    return categoryResult.getData();
  }
}

这里有两点要注意:

  1. 由于批量查询接口 CategoryBackService.findCategoryList 对参数传入的 ids 数目有限制,因此要对所有要查询的 ids 进行划分,串行或并发地去获取;
  2. 这里使用了一个线程安全的本地缓存,因为会存在多个线程同时写或读这个缓存; 之所以不用 guava 的 cache,是因为缓存的 key 只是个字符串,不是一个创建开销很大的对象。

复用改造

上述代码是典型的混合了业务和缓存微组件的样例。如果想要根据员工ID和员工姓名的映射,就不得不把上面的一部分复制出来,再写到另一个类里。这样会有不少重复工作量,而且还需要仔细编辑,把业务变量的名字替换掉,不然维护者会发现变量命名和业务含义对不上。你懂的。

有没有办法将缓存小组件的部分抽离出来呢? 要做到这一点,需要有对业务和通用组件的敏锐 sense ,能很好地将这两者区分开。

语义分离

首先要从语义上将业务和通用技术组件的逻辑分离开。

对于这个例子,可以先来审视业务部分,涉及到:

  • 一个类目对象 CategoryBackModel ,包含 id, name 属性和 getter 方法;
  • 获取一批类目对象的方法:categoryBackService.findCategoryList。
    其它的都是缓存相关的逻辑。

其次,看业务的部分多还是通用的部分多。如果是业务的部分多,就把通用的部分抽到另一个类里;如果是通用的部分多,就把业务的部分抽到另一个类。

在这个例子里,NewCategoryCache 缓存的部分占了大多数,实际上只依赖一个业务服务调用。因此,可以业务的部分抽出去。

通用抽离

模板方法是分离通用的部分与业务的部分的妙法。

接上述,getCategoryInfo 是业务部分,应该放在子类里,作为回调传给基类。可以先将这个方法抽象成 getList ,贴切表达了这个依赖要做的事情,是根据一个 id 列表获取到一个对象列表:

protected abstract List<Domain> getList(List<Long> ids);

这里 Domain 必须有 id, name 方法,因此,将 Domain 定义为一个接口:

public interface Domain {
    Long getId();
    String getName();
  }

这样,getCategoryMapFromGoods 可以写成如下形式,只依赖自己定义的接口,而不依赖具体的业务调用:

private Map<Long,String> getMapFromService(List<Long> ids) {
    List<Domain> models = multiTaskExecutor.exec(ids,
        subIds -> getList(subIds), 30);
    return StreamUtil.listToMap(models, Domain::getId, Domain::getName);
  }

然后将 NewCategoryCache 中所有的具有业务含义的名字部分(Category)去掉,就变成了:

public abstract class AbstractCache {

  private static Logger logger = LoggerFactory.getLogger(AbstractCache.class);

  @Resource
 protected MultiTaskExecutor multiTaskExecutor;

  public Map<Long, String> getMap(List<Long> ids) {

    List<Long> undupIds = ListUtil.removeDuplicate(ids);

    List<Long> unCached = new ArrayList<>();
    Map<Long,String> resultMap = new HashMap<>();
    for (Long id: undupIds) {
      String name = getCache().get(id);
      if (StringUtils.isNotBlank(name)) {
        resultMap.put(id, name);
      }
      else {
        unCached.add(id);
      }
    }

    if (CollectionUtils.isEmpty(unCached)) {
      return resultMap;
    }

    Map<Long,String> uncacheMap = getMapFromService(unCached);
    getCache().putAll(uncacheMap);
    logger.info("add new cacheMap: {}", uncacheMap);
    resultMap.putAll(uncacheMap);

    return resultMap;

  }

  private Map<Long,String> getMapFromService(List<Long> ids) {
    List<Domain> models = multiTaskExecutor.exec(ids,
        subIds -> getList(subIds), 30);
    return StreamUtil.listToMap(models, Domain::getId, Domain::getName);
  }

  protected abstract List<Domain> getList(List<Long> ids);

  protected abstract ConcurrentMap<Long,String> getCache();

  public interface Domain {
    Long getId();
    String getName();
  }

}

AbstractCache 这个类不再具有任何业务语义了。

注意: 之所以抽离出一个 getCache() 的抽象方法,是因为通常情况下不同业务的缓存是不能混用的。当然,如果 key 是带有业务前缀名字空间的值,从而有全局一致性的话,是可以只用一个缓存的。

业务抽离

接下来,可以把业务的部分新建一个类:

@Component("newCategoryCacheV2")
public class NewCategoryCacheV2 extends AbstractCache {

  private static Logger logger = LoggerFactory.getLogger(NewCategoryCacheV2.class);

  /**
   * 类目ID与名称映射关系的缓存
   * 假设每个类目信息 50B , 总共 50000 个类目,
   * 那么总占用空间 2500000B = 2.38MB 不会造成影响
   */
  private ConcurrentMap<Long, String> categoryCache = new ConcurrentHashMap<>();

  @Resource
  private CategoryBackService categoryBackService;

  public Map<Long,String> getCategoryMap(List<Long> categoryIds) {
    return getMap(categoryIds);
  }

  @Override
  public List<Domain> getList(List<Long> ids) {
    CategoryBackParam categoryBackParam = new CategoryBackParam();
    categoryBackParam.setIds(ids);
    ListResult<CategoryBackModel> categoryResult = categoryBackService.findCategoryList(categoryBackParam);
    logger.info("categoryId: {}, categoryResult:{}", ids, JSON.toJSONString(categoryResult));
    if (categoryResult == null || !categoryResult.isSuccess()) {
      logger.warn("failed to fetch category: categoryIds={}", ids);
      return new ArrayList<>();
    }
    return categoryResult.getData().stream().map( categoryBackModel -> new Domain() {
      @Override
      public Long getId() {
        return categoryBackModel.getId();
      }
      @Override
      public String getName() {
        return categoryBackModel.getName();
      }
    }).collect(Collectors.toList());
  }

  @Override
  protected ConcurrentMap<Long, String> getCache() {
    return categoryCache;
  }
}

这样,就大功告成了 ! 是不是有做成一道菜的感觉?

值得提及的是,为了彰显业务语义, newCategoryCacheV2 提供了一个 getMap 的适配包装,保证了对外服务的一致性。

单测

单测很重要。 这里贴出了上述 newCategoryCacheV2 的单测,供参考:

class NewCategoryCacheV2Test extends Specification {

    NewCategoryCache newCategoryCache = new NewCategoryCache()

    CategoryBackService categoryBackService = Mock(CategoryBackService)
    MultiTaskExecutor multiTaskExecutor = new MultiTaskExecutor()

    def setup() {
        Map<Long, String> categoryCache = new ConcurrentHashMap<>()
        categoryCache.put(3188L, "qin")
        categoryCache.put(3125L, 'qun')

        newCategoryCache.categoryCache = categoryCache
        newCategoryCache.categoryBackService = categoryBackService

        ExportThreadPoolExecutor exportThreadPoolExecutor = ExportThreadPoolExecutor.getInstance(5,5,1L,1, "export")
        multiTaskExecutor.generalThreadPoolExecutor = exportThreadPoolExecutor
        newCategoryCache.multiTaskExecutor = multiTaskExecutor

    }

    @Test
    def "tesGetCategoryMap"() {
        given:
        def categoryList = [
                new CategoryBackModel(id: 1122L, name: '衣服'),
                new CategoryBackModel(id: 2233L, name: '食品')
        ]
        categoryBackService.findCategoryList(_) >> [
                code: 200,
                message: 'success',
                success: true,
                data: categoryList,
                count: 2
        ]
        categoryList

        when:
        def categoryIds = [3188L, 3125L, 3125L, 3188L, 1122L, 2233L]


        def categoryMap = newCategoryCache.getCategoryMap(categoryIds)

        then:
        categoryMap[3188L] == 'qin'
        categoryMap[3125L] == 'qun'
        categoryMap[1122L] == '衣服'
        categoryMap[2233L] == '食品'
    }
}


小结

本文用一个示例说明了,如何从业务代码中抽离出可复用的微组件,使得一类事情只需要做一次,今后可以反复地复用。这种思维和技能是可以通过持续训练强化的,对提升设计能力是很有助益的。

上一篇:IdentityServer4:发布环境的数字签名证书


下一篇:[环境搭建]-[局域网ansible离线安装]