如何从业务代码中抽离出可复用的微组件
背景
很多业务代码,掺杂着一些通用的大段逻辑;容易导致的后果是,当需要类似功能时,不得不重新写一道,或者复制出几乎相同的代码块,让系统的无序性蹭蹭蹭往上涨。
具有良好抽象思维的有心的开发者,则会仔细观察到这种现象,将这些通用的大块逻辑抽离出来,做成一个可复用的微组件,使得以后再做类似的事情,只需要付出很小的工作即可。
那么,如何从业务代码中抽离出可复用的微组件,使得一类事情只需要做一次,今后可以反复地复用呢? 本文将以一个例子来说明。
在业务开发中,常常需要根据一批 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] == '食品'
}
}
小结
本文用一个示例说明了,如何从业务代码中抽离出可复用的微组件,使得一类事情只需要做一次,今后可以反复地复用。这种思维和技能是可以通过持续训练强化的,对提升设计能力是很有助益的。