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


    背景###

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

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

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

    在业务开发中,常常需要根据一批 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 {
    
        NewCategoryCacheV2 newCategoryCache = new NewCategoryCacheV2()
    
        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] == '食品'
        }
    }
    

    小结###

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

  • 相关阅读:
    Springboot 拦截器配置(登录拦截)
    SVN server 服务端修改端口号
    Latex 添加新的宏包
    鼠标右键快捷键修改的所对应注册表位置
    软件和电脑分辨率不一致解决办法——更改高DPI设置
    Latex中使用pdflatex编译图片出错:Unknown graphics extension: .eps. ...raphics[height=3.3cm]{figures/Var.eps}
    latex-TexStudio-tex源文件与pdf正反搜索(正反定位)设置
    假设检验
    大数据预处理技术
    chrome无法从该网站添加应用、扩展程序和用户脚本的有效解决方法!
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/11614269.html
Copyright © 2020-2023  润新知