如何從業務代碼中抽離出可複用的微組件

背景

不少業務代碼,摻雜着一些通用的大段邏輯;容易致使的後果是,當須要相似功能時,不得不從新寫一道,或者複製出幾乎相同的代碼塊,讓系統的無序性蹭蹭蹭往上漲。java

具備良好抽象思惟的有心的開發者,則會仔細觀察到這種現象,將這些通用的大塊邏輯抽離出來,作成一個可複用的微組件,使得之後再作相似的事情,只須要付出很小的工做便可。緩存

那麼,如何從業務代碼中抽離出可複用的微組件,使得一類事情只須要作一次,從此能夠反覆地複用呢? 本文將以一個例子來講明。安全

在業務開發中,經常須要根據一批 id 查到相對應的 name 。好比根據一批員工ID查到員工的姓名,根據一批類目ID查到類目的名稱,諸如此類。從敘述上看,就能感覺到其中的類似性,那麼如何將這種類似性抽離出來呢?

併發

初步代碼

假設要根據一批類目ID來獲取相應的類目名稱。大多數開發者均可以寫出知足業務需求的代碼:ide

@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();
  }
}

這裏有兩點要注意:fetch

  1. 因爲批量查詢接口 CategoryBackService.findCategoryList 對參數傳入的 ids 數目有限制,所以要對全部要查詢的 ids 進行劃分,串行或併發地去獲取;
  2. 這裏使用了一個線程安全的本地緩存,由於會存在多個線程同時寫或讀這個緩存; 之因此不用 guava 的 cache,是由於緩存的 key 只是個字符串,不是一個建立開銷很大的對象。

複用改造

上述代碼是典型的混合了業務和緩存微組件的樣例。若是想要根據員工ID和員工姓名的映射,就不得不把上面的一部分複製出來,再寫到另外一個類裏。這樣會有很多重複工做量,並且還須要仔細編輯,把業務變量的名字替換掉,否則維護者會發現變量命名和業務含義對不上。你懂的。線程

有沒有辦法將緩存小組件的部分抽離出來呢? 要作到這一點,須要有對業務和通用組件的敏銳 sense ,能很好地將這二者區分開。

設計

語義分離

首先要從語義上將業務和通用技術組件的邏輯分離開。code

對於這個例子,能夠先來審視業務部分,涉及到:對象

  • 一個類目對象 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] == '食品'
    }
}


小結

本文用一個示例說明了,如何從業務代碼中抽離出可複用的微組件,使得一類事情只須要作一次,從此能夠反覆地複用。這種思惟和技能是能夠經過持續訓練強化的,對提高設計能力是頗有助益的。

相關文章
相關標籤/搜索