不少業務代碼,摻雜着一些通用的大段邏輯;容易致使的後果是,當須要相似功能時,不得不從新寫一道,或者複製出幾乎相同的代碼塊,讓系統的無序性蹭蹭蹭往上漲。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
上述代碼是典型的混合了業務和緩存微組件的樣例。若是想要根據員工ID和員工姓名的映射,就不得不把上面的一部分複製出來,再寫到另外一個類裏。這樣會有很多重複工做量,並且還須要仔細編輯,把業務變量的名字替換掉,否則維護者會發現變量命名和業務含義對不上。你懂的。線程
有沒有辦法將緩存小組件的部分抽離出來呢? 要作到這一點,須要有對業務和通用組件的敏銳 sense ,能很好地將這二者區分開。
設計
首先要從語義上將業務和通用技術組件的邏輯分離開。code
對於這個例子,能夠先來審視業務部分,涉及到:對象
其次,看業務的部分多仍是通用的部分多。若是是業務的部分多,就把通用的部分抽到另外一個類裏;若是是通用的部分多,就把業務的部分抽到另外一個類。
在這個例子裏,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] == '食品' } }
本文用一個示例說明了,如何從業務代碼中抽離出可複用的微組件,使得一類事情只須要作一次,從此能夠反覆地複用。這種思惟和技能是能夠經過持續訓練強化的,對提高設計能力是頗有助益的。