淘宝创新业务的优化迭代是非常高频且迅速的,在这过程中要求技术也必须是快且稳的,而为了适应这种快速变化的节奏,我们在项目开发过程中采用了一些面向拓展以及敏捷开发的设计,本文旨在总结并思考其中一些通用的编程模式。
前言
静心守护业务是淘宝今年4月份启动的创新项目,项目的核心逻辑是通过敲木鱼、冥想、盘手串等疗愈玩法为用户带来内心宁静的同时推动文物的保护与修复,进一步弘扬我们的传统文化。
作为创新项目,业务形态与产品方案的优化迭代是非常高频且迅速的:项目从4月底投入开发到7月份最终外灰,整体方案经历过大的推倒重建,也经历过多轮小型重构优化,项目上线后也在做持续的迭代优化甚至改版升级。
模式清单
▐ 基于Spring容器与反射的策略模式
策略模式是一种经典的行为设计模式,它的本质是定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换,后续也能根据需要灵活拓展出新的算法。这里推荐的是一种基于Spring容器和反射结合的策略模式,这种模式的核心思路是:每个策略模式的实现都是一个bean,在Spring容器启动时基于反射获取每个策略场景的接口类型,并基于该接口类型再获取此类型的所有策略实现bean并记录到一个map(key为该策略bean的唯一标识符,value为bean对象)中,后续可以自定义路由策略来从该map中获取bean对象并使用相应的策略。
- 模式解构
模式具体实现方式大致如下面的UML类图所描述的:
其中涉及的各个组件及作用分别为:
- Handler(interface):策略的顶层接口,定义的type方法表示策略唯一标识的获取方式。
- HandlerFactory(abstract class):策略工厂的抽象实现,封装了反射获取Spring bean并维护策略与其标识映射的逻辑,但不感知策略的真实类型。
- AbstractHandler(interface or abstracr class):各个具体场景下的策略接口定义,该接口定义了具体场景下策略所需要完成的行为。如果各个具体策略实现有可复用的逻辑,可以结合模版方法模式在该接口内定义模版方法,如果模板方法依赖外部bean注入,则该接口的类型需要为abstract class,否则为interface即可。
- HandlerImpl(class):各个场景下策略接口的具体实现,承载主要的业务逻辑,也可以根据需要横向拓展。
- HandlerFactoryImpl(class):策略工厂的具体实现,感知具体场景策略接口的类型,如果有定制的策略路由逻辑也可以在此实现。
这种模式的主要优点有:
- 策略标识维护自动化:策略实现与标识之间的映射关系完全委托给Spring容器进行维护(在HandlerFactory中封装,每个场景的策略工厂直接继承该类即可,无需重复实现),后续新增策略不用再手动修改关系映射。
- 场景维度维护标识映射:HandlerFactory中在扫描策略bean时是按照AbstractHandler的类型来分类维护的,从而避免了不同场景的同名策略发生冲突。
- 策略接口按场景灵活定义:具体场景的策略行为定义在AbstractHandler中,在这里可以根据真实的业务需求灵活定义行为,甚至也可以结合其他设计模式做进一步抽象处理,在提供灵活拓展的同时减少重复代码。
- 实践案例分析
该模式在静心守护项目中的许多功能模块都有使用,下面以称号解锁模块为例来介绍其实际应用。
我们先简单了解下该模块的业务背景:静心守护的成就体系中有一类是称号,如下图。用户可以通过多种行为去解锁不同类型的称号,比如说通过参与主玩法(敲木鱼、冥想、盘手串),主玩法参与达到一定次数后即可解锁特定类型的称号。当然后续也可能会有其他种类的称号:比如签到类(按照用户签到天数解锁)、捐赠类(按照用户捐赠项目的行为解锁),所以对于称号的解锁操作应该是面向未来可持续拓展的。
基于这样的思考,我选择使用上面的策略模式去实现称号解锁模块。该模块的核心类图组织如下:
下面是其中部分核心代码的分析解读:
public interface Handler<T> { /** * handler类型 * * @return */ T type(); }
如上文所说,Handler是策略的顶层抽象,它只定义了type方法,该方法用于获取策略的标识,标识的类型支持子接口定义。
@Slf4j public abstract class HandlerFactory<T, H extends Handler<T>> implements InitializingBean, ApplicationContextAware { private Map<T, H> handlerMap; private ApplicationContext appContext; /** * 根据 type 获得对应的handler * * @param type * @return */ public H getHandler(T type) { return handlerMap.get(type); } /** * 根据 type 获得对应的handler,支持返回默认 * * @param type * @param defaultHandler * @return */ public H getHandlerOrDefault(T type, H defaultHandler) { return handlerMap.getOrDefault(type, defaultHandler); } /** * 反射获取泛型参数handler类型 * * @return handler类型 */ @SuppressWarnings("unchecked") protected Class<H> getHandlerType() { Type type = ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[1]; //策略接口使用了范型参数 if (type instanceof ParameterizedTypeImpl) { return (Class<H>) ((ParameterizedTypeImpl)type).getRawType(); } else { return (Class<H>) type; } } @Override public void afterPropertiesSet() { // 获取所有 H 类型的 handlers Collection<H> handlers = appContext.getBeansOfType(getHandlerType()).values(); handlerMap = Maps.newHashMapWithExpectedSize(handlers.size()); for (final H handler : handlers) { log.info("HandlerFactory {}, {}", this.getClass().getCanonicalName(), handler.type()); handlerMap.put(handler.type(), handler); } log.info("handlerMap:{}", JSON.toJSONString(handlerMap)); } @Override public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException { this.appContext = applicationContext; } }
HandlerFactory在前面也提到过,是策略工厂的抽象实现,封装了反射获取具体场景策略接口类型,并查找策略bean在内存中维护策略与其标识的映射关系,后续可以直接通过标识或者对应的策略实现。这里有二个细节:
- 为什么HandlerFactory是abstract class?其实可以看到该类并没有任何抽象方法,直接将其定义为class也不会有什么问题。这里将其定义为abstract class主要是起到实例创建的约束作用,因为我们对该类的定义是工厂的抽象实现,只希望针对具体场景来创建实例,针对该工厂本身创建实例其实是没有任何实际意义的。
- getHandlerType方法使用了@SuppressWarnings注解并标记了unchecked。这里也确实是存在潜在风险的,因为Type类型转Class类型属于向下类型转换,是存在风险的,可能其实际类型并非Class而是其他类型,那么此处强转就会出错。这里处理了两种最通用的情况:AbstractHandler是带范型的class和最普通的class。
@Component public class TitleUnlockHandlerFactory extends HandlerFactory<String, BaseTitleUnlockHandler<BaseTitleUnlockParams>> {}
TitleUnlockHandlerFactory是策略工厂的具体实现,由于不需要在此定制策略的路由逻辑,所以只声明了相关的参数类型,而没有对父类的方法做什么覆盖。
public abstract class BaseTitleUnlockHandler<T extends BaseTitleUnlockParams> implements Handler<String> { @Resource private UserTitleTairManager userTitleTairManager; @Resource private AchievementCountManager achievementCountManager; @Resource private UserUnreadAchievementTairManager userUnreadAchievementTairManager; ...... /** * 解锁称号 * * @param params * @return */ public @CheckForNull TitleUnlockResult unlockTitles(T params) { TitleUnlockResult titleUnlockResult = this.doUnlock(params); if (null == titleUnlockResult) { return null; } List<TitleAchievementVO> titleAchievements = titleUnlockResult.getUnlockedTitles(); if (CollectionUtils.isEmpty(titleAchievements)) { titleUnlockResult.setUnlockedTitles(new ArrayList<>()); return titleUnlockResult; } //基于注入的bean和计算出的称号列表进行后置操作,如:更新成就计数、更新用户称号缓存、更新用户未读成就等 ...... return titleUnlockResult; } /** * 计算出要解锁的称号 * * @param param * @return */ protected abstract TitleUnlockResult doUnlock(T param); @Override public abstract String type(); }
BaseTitleUnlockHandler定义了称号解锁行为,并且在此确定了策略标识的类型为String。此外,该类是一个abstract class,是因为该类定义了一个模版方法unlockTitles,在该方法里封装了称号解锁所要进行的一些公共操作,比如更新用户的称号计数、用户的称号缓存数据等,这些都依赖于注入的一些外部bean,而interface不支持非静态成员变量,所以该类通过abstract class来定义。具体的称号解锁行为通过doUnlock定义,这也是该策略的具体实现类需要实现的方法。
另外也许你还注意到了doUnlock方法的行参是一个范型参数T,因为我们考虑到了不同类型称号解锁所需要的参数可能是不同的,因此在场景抽象接口侧只依赖于称号解锁的公共参数类型,而在策略接口具体实现侧才与该类型策略的具体参数类型进行耦合。
@Component public class GameplayTitleUnlockHandler extends BaseTitleUnlockHandler<GameplayTitleUnlockParams> { @Resource private BlessTitleAchievementDiamondConfig blessTitleAchievementDiamondConfig; @Resource private UserTitleTairManager userTitleTairManager; @Override protected TitleUnlockResult doUnlock(GameplayTitleUnlockParams params) { //获取称号元数据 List<TitleMetadata> titleMetadata = blessTitleAchievementDiamondConfig.getTitleMetadata(); if (CollectionUtils.isEmpty(titleMetadata)) { return null; } List<TitleAchievementVO> titleAchievements = new ArrayList<>(); Result<DataEntry> result = userTitleTairManager.queryRawCache(params.getUserId()); //用户称号数据查询异常 if (null == result || !result.isSuccess()) { return null; } if (Objects.equals(result.getRc(), ResultCode.SUCCESS)) { //解锁新称号 titleAchievements = unlockNewTitles(params, titleMetadata); } else if (Objects.equals(result.getRc(), ResultCode.DATANOTEXSITS)) { //初始化历史称号 titleAchievements = initHistoricalTitles(params, titleMetadata); } TitleUnlockResult titleUnlockResult = new TitleUnlockResult(); titleUnlockResult.setUserTitleCache(result); titleUnlockResult.setUnlockedTitles(titleAchievements); return titleUnlockResult; } @Override public String type() { return TitleType.GAMEPLAY; } ...... }
上面是一个策略的具体实现类的大致示例,可以看到该实现类核心明确了以下信息:
- 策略标识:给出了type方法的具体实现,返回了一个策略标识的常量
- 策略处理逻辑:此处是玩法类称号解锁的业务逻辑,读者无需关注其细节
- 称号解锁行参:给出了玩法类称号解锁所需的真实参数类型
关于编程模式的总结与思考(中):https://developer.aliyun.com/article/1443300