关于编程模式的总结与思考(上):https://developer.aliyun.com/article/1443301
▐ 抽象疲劳度管控体系
在我们的业务需求中经常会遇到涉及疲劳度管控相关的逻辑,比如每日签到允许用户每天完成1次、首页项目进展弹窗要求对所有用户只弹1次、首页限时回访任务入口则要对用户每天都展示一次,但用户累计完成3次后便不再展示......因此我们设计了一套疲劳度管控的模式,以降低后续诸如上述涉及疲劳度管控相关需求的开发成本。
- 自顶向下的视角
这套疲劳度管控体系的类层次大致如下图:
接下来我们自顶向下逐层进行介绍:
- FatigueLimiter(interface):FatigueLimiter是最顶层抽象的疲劳度管控接口,它定义了疲劳度管控相关的行为,比如:疲劳度的查询、疲劳度清空、疲劳度增加、是否达到疲劳度限制的判断等。
- BaseFatigueLdbLimiter(abstract class):疲劳度数据的存储方案可以是多种多样的,在我们项目中主要利用ldb进行疲劳度存储,而BaseFatigueLdbLimiter正是基于ldb【注:阿里内部自研的一款持久化k-v数据库,读者可将其理解为类似level db的项目】对疲劳度数据进行管控的抽象实现,它封装了ldb相关的操作,并基于ldb的数据操作实现了FatigueLimiter的疲劳度管控方法。但它并不感知具体业务的身份和逻辑,因此定义了几个业务相关的方法交给下层去实现,分别是:
- scene:标识具体业务的场景,会利用该方法返回值去构造Ldb存储的key
- buildCustomKey:对Ldb存储key的定制逻辑
- getExpireSeconds:对应着Ldb存储kv失效时间,对应着疲劳度的管控周期
- Ldb周期性疲劳度管控的解决方案层(abstract class):在这一层提供了多种周期的开箱即用的疲劳度管控实现类,如BaseFatigueDailyLimiter提供的是天级别的疲劳度管控能力,BaseFatigueNoCycleLimiter则表示疲劳度永不过期,而BaseFatigueCycleLimiter则支持用户实现cycle方法定制疲劳度周期。
- 业务场景层:这一层则是各个业务场景对疲劳度管控的具体实现,实现类只需要实现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