背景###
很多業務代碼,摻雜着一些通用的大段邏輯;容易導致的后果是,當需要類似功能時,不得不重新寫一道,或者復制出幾乎相同的代碼塊,讓系統的無序性蹭蹭蹭往上漲。
具有良好抽象思維的有心的開發者,則會仔細觀察到這種現象,將這些通用的大塊邏輯抽離出來,做成一個可復用的微組件,使得以后再做類似的事情,只需要付出很小的工作即可。
那么,如何從業務代碼中抽離出可復用的微組件,使得一類事情只需要做一次,今后可以反復地復用呢? 本文將以一個例子來說明。
在業務開發中,常常需要根據一批 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();
}
}
這里有兩點要注意:
- 由於批量查詢接口 CategoryBackService.findCategoryList 對參數傳入的 ids 數目有限制,因此要對所有要查詢的 ids 進行划分,串行或並發地去獲取;
- 這里使用了一個線程安全的本地緩存,因為會存在多個線程同時寫或讀這個緩存; 之所以不用 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] == '食品'
}
}
小結###
本文用一個示例說明了,如何從業務代碼中抽離出可復用的微組件,使得一類事情只需要做一次,今后可以反復地復用。這種思維和技能是可以通過持續訓練強化的,對提升設計能力是很有助益的。