关于编程模式的总结与思考(上)

简介: 关于编程模式的总结与思考(上)




淘宝创新业务的优化迭代是非常高频且迅速的,在这过程中要求技术也必须是快且稳的,而为了适应这种快速变化的节奏,我们在项目开发过程中采用了一些面向拓展以及敏捷开发的设计,本文旨在总结并思考其中一些通用的编程模式。


前言


静心守护业务是淘宝今年4月份启动的创新项目,项目的核心逻辑是通过敲木鱼、冥想、盘手串等疗愈玩法为用户带来内心宁静的同时推动文物的保护与修复,进一步弘扬我们的传统文化。


作为创新项目,业务形态与产品方案的优化迭代是非常高频且迅速的:项目从4月底投入开发到7月份最终外灰,整体方案经历过大的推倒重建,也经历过多轮小型重构优化,项目上线后也在做持续的迭代优化甚至改版升级。


模式清单

 基于Spring容器与反射的策略模式


策略模式是一种经典的行为设计模式,它的本质是定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换,后续也能根据需要灵活拓展出新的算法。这里推荐的是一种基于Spring容器和反射结合的策略模式,这种模式的核心思路是:每个策略模式的实现都是一个bean,在Spring容器启动时基于反射获取每个策略场景的接口类型,并基于该接口类型再获取此类型的所有策略实现bean并记录到一个map(key为该策略bean的唯一标识符,value为bean对象)中,后续可以自定义路由策略来从该map中获取bean对象并使用相应的策略。


  • 模式解构


模式具体实现方式大致如下面的UML类图所描述的:


其中涉及的各个组件及作用分别为:

  1. Handlerinterface):策略的顶层接口,定义的type方法表示策略唯一标识的获取方式。
  2. HandlerFactoryabstract class):策略工厂的抽象实现,封装了反射获取Spring bean并维护策略与其标识映射的逻辑,但不感知策略的真实类型。
  3. AbstractHandlerinterface or abstracr class):各个具体场景下的策略接口定义,该接口定义了具体场景下策略所需要完成的行为。如果各个具体策略实现有可复用的逻辑,可以结合模版方法模式在该接口内定义模版方法,如果模板方法依赖外部bean注入,则该接口的类型需要为abstract class,否则为interface即可。
  4. HandlerImplclass):各个场景下策略接口的具体实现,承载主要的业务逻辑,也可以根据需要横向拓展。
  5. HandlerFactoryImplclass):策略工厂的具体实现,感知具体场景策略接口的类型,如果有定制的策略路由逻辑也可以在此实现。


这种模式的主要优点有:

  1. 策略标识维护自动化:策略实现与标识之间的映射关系完全委托给Spring容器进行维护(在HandlerFactory中封装,每个场景的策略工厂直接继承该类即可,无需重复实现),后续新增策略不用再手动修改关系映射。
  2. 场景维度维护标识映射HandlerFactory中在扫描策略bean时是按照AbstractHandler的类型来分类维护的,从而避免了不同场景的同名策略发生冲突。
  3. 策略接口按场景灵活定义:具体场景的策略行为定义在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在内存中维护策略与其标识的映射关系,后续可以直接通过标识或者对应的策略实现。这里有二个细节:

  1. 为什么HandlerFactory是abstract class?其实可以看到该类并没有任何抽象方法,直接将其定义为class也不会有什么问题。这里将其定义为abstract class主要是起到实例创建的约束作用,因为我们对该类的定义是工厂的抽象实现,只希望针对具体场景来创建实例,针对该工厂本身创建实例其实是没有任何实际意义的。
  2. 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;
    }

    ......
}


上面是一个策略的具体实现类的大致示例,可以看到该实现类核心明确了以下信息:

  1. 策略标识:给出了type方法的具体实现,返回了一个策略标识的常量
  2. 策略处理逻辑:此处是玩法类称号解锁的业务逻辑,读者无需关注其细节
  3. 称号解锁行参:给出了玩法类称号解锁所需的真实参数类型


关于编程模式的总结与思考(中):https://developer.aliyun.com/article/1443300

目录
相关文章
|
11月前
|
分布式计算 前端开发 JavaScript
程范式解析:面向对象、函数式与声明式编程
程范式解析:面向对象、函数式与声明式编程
109 0
编程问题之响应式编程使用了哪些技术
编程问题之响应式编程使用了哪些技术
|
4月前
|
存储 NoSQL Java
关于编程模式的总结与思考(中)
关于编程模式的总结与思考(中)
40 1
|
4月前
|
存储 监控 NoSQL
关于编程模式的总结与思考(下)
关于编程模式的总结与思考(下)
51 0
|
缓存 Java 程序员
函数式编程的Java编码实践:利用惰性写出高性能且抽象的代码
本文会以惰性加载为例一步步介绍函数式编程中各种概念,所以读者不需要任何函数式编程的基础,只需要对 Java 8 有些许了解即可。
函数式编程的Java编码实践:利用惰性写出高性能且抽象的代码
|
消息中间件 Java 数据库
实现异步编程的方式
实现异步编程的方式
|
Scala 开发工具 git
剖析响应式编程的本质
剖析响应式编程的本质
剖析响应式编程的本质
事件驱动式编程
事件驱动式编程
136 1
|
JavaScript Java API
反应式编程探索与总结
1.什么是反应式编程 Reactive Programming 一种以异步处理数据流为中心思想的编程范式,这个范式存在已久,不是新概念,就像面向过程、面向对象编程、函数式编程等范式。 对比一下,Reactive streams指的是一套规范,对于Java开发者来讲,Reactive Streams就是一套API,使我们可以进行Reactive programming。 Reacti
2762 0
|
存储 算法 前端开发
一文了解异步编程基础
异步编程是指并发编程的范式,其中除了单个主应用程序线程之外,工作可以委托给一个或多个并行工作线程。这被称为非阻塞系统,其中整体系统速度不受订单执行的影响,并且多个进程可以同时发生。