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

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

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


 抽象疲劳度管控体系


在我们的业务需求中经常会遇到涉及疲劳度管控相关的逻辑,比如每日签到允许用户每天完成1次、首页项目进展弹窗要求对所有用户只弹1次、首页限时回访任务入口则要对用户每天都展示一次,但用户累计完成3次后便不再展示......因此我们设计了一套疲劳度管控的模式,以降低后续诸如上述涉及疲劳度管控相关需求的开发成本。


  • 自顶向下的视角


这套疲劳度管控体系的类层次大致如下图:

接下来我们自顶向下逐层进行介绍:

  1. FatigueLimiter(interface):FatigueLimiter是最顶层抽象的疲劳度管控接口,它定义了疲劳度管控相关的行为,比如:疲劳度的查询、疲劳度清空、疲劳度增加、是否达到疲劳度限制的判断等。
  2. BaseFatigueLdbLimiter(abstract class):疲劳度数据的存储方案可以是多种多样的,在我们项目中主要利用ldb进行疲劳度存储,而BaseFatigueLdbLimiter正是基于ldb【注:阿里内部自研的一款持久化k-v数据库,读者可将其理解为类似level db的项目】对疲劳度数据进行管控的抽象实现,它封装了ldb相关的操作,并基于ldb的数据操作实现了FatigueLimiter的疲劳度管控方法。但它并不感知具体业务的身份和逻辑,因此定义了几个业务相关的方法交给下层去实现,分别是:
  • scene:标识具体业务的场景,会利用该方法返回值去构造Ldb存储的key
  • buildCustomKey:对Ldb存储key的定制逻辑
  • getExpireSeconds:对应着Ldb存储kv失效时间,对应着疲劳度的管控周期
  1. Ldb周期性疲劳度管控的解决方案层(abstract class):在这一层提供了多种周期的开箱即用的疲劳度管控实现类,如BaseFatigueDailyLimiter提供的是天级别的疲劳度管控能力,BaseFatigueNoCycleLimiter则表示疲劳度永不过期,而BaseFatigueCycleLimiter则支持用户实现cycle方法定制疲劳度周期。
  2. 业务场景层:这一层则是各个业务场景对疲劳度管控的具体实现,实现类只需要实现scene方法来声明业务场景的身份标识,随后继承对应的解决方案,即可实现快速的疲劳度管控。比如上面的DailyWishSignLimiter就对应着本篇开头我们所说的“每日签到允许用户每天完成1次”,这就要求为用户的签到行为以天维度构建key同时失效时间也为1天,因此直接继承解决方案层的BaseFatigueDailyLimiter即可。其代码实现非常简单,如下:


@Component
public class DailyWishSignLimiter extends BaseFatigueLdbDailyLimiter {

    @Override
    protected String scene() {
        return LimiterScene.dailyWish;
    }
}


  • 有一个“异类”


也许你注意到了上面的类层次图中有一个“异类”——HomeEnterGuideLimiter。它其实就是我们在上文说的“首页限时回访任务入口则要对用户每天都展示一次,但用户累计完成3次后便不再展示”,它的逻辑其实也很简单:因为它有2条管控条件,所以需要继承2个管控周期的解决方案——天维度和永久维度,最后实际使用的类再聚合了天维度和永久维度的实现类(每个实现类对应ldb的一类key)并实现了顶层的疲劳度管控接口,标识这也是一个疲劳度管理器。它们的代码如下:



/**
 * 首页入口引导限时任务-天级疲劳度管控
 *
 */
@Component
public class HomeEnterGuideDailyLimiter extends BaseFatigueLdbDailyLimiter {

    @Override
    protected String scene() {
        return LimiterScene.homeEnterGuide;
    }
}

/**
 * 首页入口引导限时任务-总次数疲劳度管控
 *
 */
@Component
public class HomeEnterGuideNoCycleLimiter extends BaseFatigueLdbNoCycleLimiter {

    @Override
    protected String scene() {
        return LimiterScene.homeEnterGuide;
    }

    @Override
    protected int maxSize() {
        return 3;
    }
}

/**
 * 首页入口引导限时任务-疲劳度服务
 *
 */
@Component
public class HomeEnterGuideLimiter implements FatigueLimiter {

    @Resource
    private FatigueLimiter homeEnterGuideDailyLimiter;

    @Resource
    private FatigueLimiter homeEnterGuideNoCycleLimiter;

    @Override
    public boolean isLimit(String customKey) {
        return homeEnterGuideNoCycleLimiter.isLimit(customKey) || homeEnterGuideDailyLimiter.isLimit(customKey);
    }

    @Override
    public Integer incrLimit(String customKey) {
        homeEnterGuideDailyLimiter.incrLimit(customKey);
        return homeEnterGuideNoCycleLimiter.incrLimit(customKey);
    }

    @Override
    public boolean isLimit(Integer fatigue) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Map<String, Integer> batchQueryLimit(List<String> keys) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void removeLimit(String customKey) {
        homeEnterGuideDailyLimiter.removeLimit(customKey);
        homeEnterGuideNoCycleLimiter.removeLimit(customKey);
    }

    @Override
    public Integer queryLimit(String customKey) {
        throw new UnsupportedOperationException();
    }

    /**
     * 查询首页限时任务的每日疲劳度
     *
     * @param customKey 用户自定义key
     * @return 疲劳度计数
     */
    public Integer queryDailyLimit(String customKey) {
        return homeEnterGuideDailyLimiter.queryLimit(customKey);
    }

    /**
     * 查询首页限时任务的全周期疲劳度
     *
     * @param customKey 用户自定义key
     * @return 疲劳度计数
     */
    public Integer queryNoCycleLimit(String customKey) {
        return homeEnterGuideNoCycleLimiter.queryLimit(customKey);
    }
}


 函数式行为参数化


Java 21在今年9月份发布了,而距离Java 8发布已经过去9年多了,但也许,我是说也许......我们有些同学对Java 8还是不太熟悉......

  • 再谈行为参数化


最早听到“行为参数化”这个词是在经典的Java技术书籍《Java 8实战》中。在此书中,作者以一个筛选苹果的案例,基于行为参数化的思维一步步优化重构代码,在提升代码抽象能力的同时,保证了代码的简洁性和可读性,而其中的秘密武器就是Java 8所引入的Lambda表达式和函数式接口。Java 8发布已经9年,对于Lambda表达式,大多数同学都已经耳熟能详,但函数式接口也许有同学不知道代表着什么。简单来说,如果一个接口,它只有一个没有被实现的方法,那它就是函数式接口。java.lang.function包下定义JDK提供的一系列函数式接口。如果一个接口是函数式接口,推荐用@FunctionalInterface注解来显式标明。那函数式接口有什么用呢?如果一个方法的行参里有函数式接口,那么函数式接口对应的参数可以支持传递Lambda表达式或者方法引用。
那何为“行为参数化”?直观地来说就是将行为作为方法/函数的参数来进行传递。在Java 8之前,这可以通过匿名类实现,而在Java 8以后,可以基于函数式特性来实现行为参数化,即方法参数定义为函数式接口,在具体传参时使用Lambda表达式/方法。相比匿名类,后者在简洁性上有极大的提升。
在我们的日常开发中,如果我们看到两个方法的结构十分相似,只有其中部分行为存在差别,那么就可以考虑采用函数式的行为参数化来重构优化这段代码,将其中存在差异的行为抽象成参数,从而减少重复代码。


  • 从实践中来,到代码中去


下面给出一个例子。在静心守护项目中,我们基于ldb维护了用户未读成就的列表,在用户进入到个人成就页时,会查询未读成就数据,并对未读的成就在成就列表进行置顶以及加红点展示。下面是对用户未读成就列表进行新增和清除的两个方法:



/**
 * 清除未读成就
 *
 * @param uid             用户ID
 * @param achievementType 需要清除未读成就列表的成就类型
 * @return
 */
public boolean clearUnreadAchievements(long uid, Set<String> achievementTypes) {

    if (CollectionUtils.isEmpty(achievementTypes)) {
        return true;
    }

    Result<DataEntry> ldbRes = super.rawGet(buildKey(uid), false);

    //用户称号数据查询失败
    if (Objects.isNull(ldbRes)) {
        recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
        return false;
    }

    boolean success = false;

    ResultCode resultCode = ldbRes.getRc();

    //不存在用户称号数据则进行初始化
    if (Objects.equals(resultCode, ResultCode.DATANOTEXSITS)) {
    UserUnreadAchievementsCache userUnreadAchievementsCache = new UserUnreadAchievementsCache();
        achievementTypes.forEach(type -> clearCertainTypeIds(userUnreadAchievementsCache, type));
        success = putCache(uid, userUnreadAchievementsCache, DEFAULT_VERSION);

    } else if (Objects.equals(resultCode, ResultCode.SUCCESS)) {

        DataEntry ldbEntry = ldbRes.getValue();

        //存在新数据则对其进行更新
        if (Objects.nonNull(ldbEntry)) {
            Object data = ldbEntry.getValue();

            if (data instanceof String) {
                UserUnreadAchievementsCache userUnreadAchievementsCache = JSON.parseObject(String.valueOf(data), UserUnreadAchievementsCache.class);
                achievementTypes.forEach(type -> clearCertainTypeIds(userUnreadAchievementsCache, type))
                success = putCache(uid, userUnreadAchievementsCache, ldbEntry.getVersion());
            }
        }
    }
    //缓存解锁的称号失败
    if (!success) {
        recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
    }
    return success;
}



/**
 * 写入新的未读成就
 *
 * @param uid                  用户ID
 * @param achievementTypeIdMap 需要新增的成就类型和成就ID列表的映射
 * @return
 */
public boolean writeUnreadAchievements(long uid, Map<String, List<String>> achievementTypeIdMap) {

    if (MapUtils.isEmpty(achievementTypeIdMap)) {
        return true;
    }

    Result<DataEntry> ldbRes = super.rawGet(buildKey(uid), false);

    //用户称号数据查询失败
    if (Objects.isNull(ldbRes)) {
        recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
        return false;
    }

    boolean success = false;

    ResultCode resultCode = ldbRes.getRc();

    //不存在用户称号数据则进行初始化
    if (Objects.equals(resultCode, ResultCode.DATANOTEXSITS)) {
    UserUnreadAchievementsCache userUnreadAchievementsCache = new UserUnreadAchievementsCache();
        achievementTypeIdMap.forEach((key, value) -> updateCertainTypeIds(userUnreadAchievementsCache, key, value));
        success = putCache(uid, userUnreadAchievementsCache, DEFAULT_VERSION);

    } else if (Objects.equals(resultCode, ResultCode.SUCCESS)) {

        DataEntry ldbEntry = ldbRes.getValue();

        //存在新数据则对其进行更新
        if (Objects.nonNull(ldbEntry)) {
            Object data = ldbEntry.getValue();

            if (data instanceof String) {
                UserUnreadAchievementsCache userUnreadAchievementsCache = JSON.parseObject(String.valueOf(data), UserUnreadAchievementsCache.class);
                achievementTypeIdMap.forEach((key, value) -> updateCertainTypeIds(oldCache, key, value));
                success = putCache(uid, userUnreadAchievementsCache, ldbEntry.getVersion());
            }
        }
    }
    //缓存解锁的称号失败
    if (!success) {
        recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build());
    }
    return success;
}


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

目录
相关文章
|
6月前
|
安全 Java 数据安全/隐私保护
|
6月前
|
设计模式 算法 Java
关于编程模式的总结与思考(上)
关于编程模式的总结与思考(上)
80 0
|
分布式计算 前端开发 JavaScript
程范式解析:面向对象、函数式与声明式编程
程范式解析:面向对象、函数式与声明式编程
117 0
|
27天前
|
消息中间件 监控 测试技术
事件驱动架构是一种编程范式
【10月更文挑战第7天】事件驱动架构是一种编程范式
105 65
编程问题之响应式编程使用了哪些技术
编程问题之响应式编程使用了哪些技术
|
4月前
|
自然语言处理 开发者
编程问题之函数式编程有什么优点
编程问题之函数式编程有什么优点
|
6月前
|
存储 监控 NoSQL
关于编程模式的总结与思考(下)
关于编程模式的总结与思考(下)
56 0
|
缓存 Java 程序员
函数式编程的Java编码实践:利用惰性写出高性能且抽象的代码
本文会以惰性加载为例一步步介绍函数式编程中各种概念,所以读者不需要任何函数式编程的基础,只需要对 Java 8 有些许了解即可。
函数式编程的Java编码实践:利用惰性写出高性能且抽象的代码
|
Scala 开发工具 git
剖析响应式编程的本质
剖析响应式编程的本质
剖析响应式编程的本质
|
JSON 监控 安全
关于不同编程语言相互调用的思考
关于不同编程语言相互调用的思考
关于不同编程语言相互调用的思考