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

背景

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

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

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

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

小结

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

原文地址:https://www.cnblogs.com/lovesqcc/p/11614269.html

时间: 2024-08-28 01:37:19

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

JVM 性能调优实战之:使用阿里开源工具 TProfiler 在海量业务代码中精确定位性能代码

本文是<JVM 性能调优实战之:一次系统性能瓶颈的寻找过程> 的后续篇,该篇介绍了如何使用 JDK 自身提供的工具进行 JVM 调优将 TPS 由 2.5 提升到 20 (提升了 7 倍),并准确定位系统瓶颈:我们应用里静态对象不是太多.有大量的业务线程在频繁创建一些生命周期很长的临时对象,代码里有问题.那么问题来了,如何在海量业务代码里边准确定位这些性能代码?本文将介绍如何使用阿里开源工具 TProfiler 来定位这些性能代码,成功解决掉了 GC 过于频繁的性能瓶颈,并最终在上次优化的基础

漫谈反射在业务代码中的应用

很多人都觉得写业务代码很枯燥,没有什么技术含量,大部分就是if-else逻辑的叠加.写业务代码确实没有写中间件来的高大上,但是我觉得不管是写什么代码,想要写出好的代码都不是一件容易的事情.这不,最近我们生产系统的版本迭代过程中一个需求就让我思考了很多,并且在实现方式上做得更加的优雅. 场景如下:我们在生产系统中需要维护各个游戏的状态,当需要上线一个游戏时,需要对该游戏的各方面的信息做一遍检查,当检查所有的字段都达到要求之后,再切换该游戏的状态为已上线.首先,针对安卓游戏,我们需要检查该游戏的资质

业务代码中(java class)中如何实现多线程,并且将子线程中的值随方法返回返回值

转载自http://bbs.csdn.net/topics/390731832 问题: public static String getAddress(final InputStream inputStream, final String mobile) { new Thread() { public void run() { try { Log.i(TAG, "inputStream: " + inputStream.available()); String soap = readS

从std::function中抽离出函数指针类型

template <typename Function>struct function_traits : public function_traits < decltype(&Function::operator()) >{ }; template <typename ClassType, typename ReturnType, typename Args>struct function_traits < ReturnType(ClassType::*)

mysql 为何在代码中查不出数据,同样的语句客户端可以呢?

今天把数据导入到mysql 数据库,然后网站提起来,发现数据拿不到,也不报错.看样子就是数据为空?不应该呀,仔细调试到sql 语句,发现正确,将sql 语句粘贴到客户端,执行正确.奇怪~ 然后问题出现了.为毛? 哦,sql 语句的 where 条件中有中文,就这么简单. 为毛中文不行,因为编码不一样,将mysql 断掉,然后修改 my.ini 将编码设置成通用的UTF8.问题解决.

ifeve.com 南方《JVM 性能调优实战之:使用阿里开源工具 TProfiler 在海量业务代码中精确定位性能代码》

https://blog.csdn.net/defonds/article/details/52598018 多次拉取 JStack,发现很多线程处于这个状态:    at jrockit/vm/Allocator.getNewTla(JJ)V(Native Method)    at jrockit/vm/Allocator.allocObjectOrArray(Allocator.java:354)[optimized]    at java/util/HashMap.resize(Hash

天天写业务代码,如何成为技术大牛

前序 在工作之余浏览公司的技术网站,看到了以下这篇文章,细细读来真心觉得不错,写得有价值很实在.于是想联系下作者,问一下是否可以转载.打开钉钉一搜,作者是资深技术专家,差不多就是技术总监级别啊,这也从侧面旁征了,以下的内容是有其亲身经历,切实体会的,而不是鸡汤口号之流.相较与作者的级别,自己确实惭愧汗颜,所以没好直接聊天询问而是在文章底下留言.在得到了作者的同意后将文章的内容贴到这里,作为分享也作为自己的鞭策和提醒.在这里谢谢我的大牛同事了^_^. ....................以下内

天天写业务代码,如何成为技术大牛?

不管是开发.测试.运维,每个技术人员心理多多少少都有一个成为技术大牛的梦,毕竟"梦想总是要有的,万一实现了呢"!正是对技术梦的追求,促使我们不断地努力和提升自己. 然而"梦想是美好的,现实却是残酷的",很多同学在实际工作后就会发现,梦想是成为大牛,但做的事情看起来跟大牛都不沾边,例如,程序员说"天天写业务代码还加班,如何才能成为技术大牛",测试说"每天都有执行不完的测试用例",运维说"扛机器接网线敲shell命令,这

面试题精选:Android埋点,减少对业务代码的如侵!

前言 前几天去参加了一场面试.面试的题目大多很基础,有一道关于埋点的问题,面试官问我如果不用第三方SDK进行埋点,自己埋点的话,如何减少埋点对业务代码的ru侵. 当时没想太多,就说创建一个 BaseView 类,在这个类中进行埋点的操作,然后使需要进行埋点操作的 View 继承这个 Base 类.后来想想,这个方案其实存在很多问题,因为让每个需要埋点的 View 去继承 BaseView 类,说明 View 需要自定义,会耗费很多的时间和精力,对于自带的 Button 等控件的埋点,这种方法又无