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

背景

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

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

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

在业务开发中,常常需要根据一批 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 categoryCache = new ConcurrentHashMap<>();

@Resource

private CategoryBackService categoryBackService;

@Resource

private MultiTaskExecutor multiTaskExecutor;

public Map getCategoryMap(List categoryIds) {

List undupCategoryIds = ListUtil.removeDuplicate(categoryIds);

List unCached = new ArrayList<>();

Map 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 uncacheCategoryMap = getCategoryMapFromGoods(unCached);

categoryCache.putAll(uncacheCategoryMap);

logger.info("add new categoryMap: {}", uncacheCategoryMap);

resultMap.putAll(uncacheCategoryMap);

return resultMap;

}

private Map getCategoryMapFromGoods(List categoryIds) {

List categoryBackModels = multiTaskExecutor.exec(categoryIds,

subCategoryIds -> getCategoryInfo(subCategoryIds), 30);

return StreamUtil.listToMap(categoryBackModels, CategoryBackModel::getId, CategoryBackModel::getName);

}

private List getCategoryInfo(List categoryIds) {

CategoryBackParam categoryBackParam = new CategoryBackParam();

categoryBackParam.setIds(categoryIds);

ListResult 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 getList(List ids);

这里 Domain 必须有 id, name 方法,因此,将 Domain 定义为一个接口:

public interface Domain {

Long getId();

String getName();

}

这样,getCategoryMapFromGoods 可以写成如下形式,只依赖自己定义的接口,而不依赖具体的业务调用:

private Map getMapFromService(List ids) {

List 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 getMap(List ids) {

List undupIds = ListUtil.removeDuplicate(ids);

List unCached = new ArrayList<>();

Map 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 uncacheMap = getMapFromService(unCached);

getCache().putAll(uncacheMap);

logger.info("add new cacheMap: {}", uncacheMap);

resultMap.putAll(uncacheMap);

return resultMap;

}

private Map getMapFromService(List ids) {

List models = multiTaskExecutor.exec(ids,

subIds -> getList(subIds), 30);

return StreamUtil.listToMap(models, Domain::getId, Domain::getName);

}

protected abstract List getList(List ids);

protected abstract ConcurrentMap 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 categoryCache = new ConcurrentHashMap<>();

@Resource

private CategoryBackService categoryBackService;

public Map getCategoryMap(List categoryIds) {

return getMap(categoryIds);

}

@Override

public List getList(List ids) {

CategoryBackParam categoryBackParam = new CategoryBackParam();

categoryBackParam.setIds(ids);

ListResult 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 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 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] == '食品'

}

}

小结

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

查看原文