淘寶創(chuàng)新業(yè)務(wù)的優(yōu)化迭代是非常高頻且迅速的,在這過程中要求技術(shù)也必須是快且穩(wěn)的,而為了適應(yīng)這種快速變化的節(jié)奏,我們在項目開發(fā)過程中采用了一些面向拓展以及敏捷開發(fā)的設(shè)計,本文旨在總結(jié)并思考其中一些通用的編程模式。
前言
靜心守護業(yè)務(wù)是淘寶今年4月份啟動的創(chuàng)新項目,項目的核心邏輯是通過敲木魚、冥想、盤手串等療愈玩法為用戶帶來內(nèi)心寧靜的同時推動文物的保護與修復(fù),進一步弘揚我們的傳統(tǒng)文化。
作為創(chuàng)新項目,業(yè)務(wù)形態(tài)與產(chǎn)品方案的優(yōu)化迭代是非常高頻且迅速的:項目從4月底投入開發(fā)到7月份最終外灰,整體方案經(jīng)歷過大的推倒重建,也經(jīng)歷過多輪小型重構(gòu)優(yōu)化,項目上線后也在做持續(xù)的迭代優(yōu)化甚至改版升級。
模式清單
基于Spring容器與反射的策略模式
策略模式是一種經(jīng)典的行為設(shè)計模式,它的本質(zhì)是定義一系列算法, 并將每種算法分別放入獨立的類中, 以使算法的對象能夠相互替換,后續(xù)也能根據(jù)需要靈活拓展出新的算法。這里推薦的是一種基于Spring容器和反射結(jié)合的策略模式,這種模式的核心思路是:每個策略模式的實現(xiàn)都是一個bean,在Spring容器啟動時基于反射獲取每個策略場景的接口類型,并基于該接口類型再獲取此類型的所有策略實現(xiàn)bean并記錄到一個map(key為該策略bean的唯一標(biāo)識符,value為bean對象)中,后續(xù)可以自定義路由策略來從該map中獲取bean對象并使用相應(yīng)的策略。
模式解構(gòu)
模式具體實現(xiàn)方式大致如下面的UML類圖所描述的:
其中涉及的各個組件及作用分別為:
Handler(interface):策略的頂層接口,定義的type方法表示策略唯一標(biāo)識的獲取方式。
HandlerFactory(abstract class):策略工廠的抽象實現(xiàn),封裝了反射獲取Spring bean并維護策略與其標(biāo)識映射的邏輯,但不感知策略的真實類型。
AbstractHandler(interface or abstracr class):各個具體場景下的策略接口定義,該接口定義了具體場景下策略所需要完成的行為。如果各個具體策略實現(xiàn)有可復(fù)用的邏輯,可以結(jié)合模版方法模式在該接口內(nèi)定義模版方法,如果模板方法依賴外部bean注入,則該接口的類型需要為abstract class,否則為interface即可。
HandlerImpl(class):各個場景下策略接口的具體實現(xiàn),承載主要的業(yè)務(wù)邏輯,也可以根據(jù)需要橫向拓展。
HandlerFactoryImpl(class):策略工廠的具體實現(xiàn),感知具體場景策略接口的類型,如果有定制的策略路由邏輯也可以在此實現(xiàn)。
這種模式的主要優(yōu)點有:
策略標(biāo)識維護自動化:策略實現(xiàn)與標(biāo)識之間的映射關(guān)系完全委托給Spring容器進行維護(在HandlerFactory中封裝,每個場景的策略工廠直接繼承該類即可,無需重復(fù)實現(xiàn)),后續(xù)新增策略不用再手動修改關(guān)系映射。
場景維度維護標(biāo)識映射:HandlerFactory中在掃描策略bean時是按照AbstractHandler的類型來分類維護的,從而避免了不同場景的同名策略發(fā)生沖突。
策略接口按場景靈活定義:具體場景的策略行為定義在AbstractHandler中,在這里可以根據(jù)真實的業(yè)務(wù)需求靈活定義行為,甚至也可以結(jié)合其他設(shè)計模式做進一步抽象處理,在提供靈活拓展的同時減少重復(fù)代碼。
實踐案例分析
該模式在靜心守護項目中的許多功能模塊都有使用,下面以稱號解鎖模塊為例來介紹其實際應(yīng)用。
我們先簡單了解下該模塊的業(yè)務(wù)背景:靜心守護的成就體系中有一類是稱號,如下圖。用戶可以通過多種行為去解鎖不同類型的稱號,比如說通過參與主玩法(敲木魚、冥想、盤手串),主玩法參與達到一定次數(shù)后即可解鎖特定類型的稱號。當(dāng)然后續(xù)也可能會有其他種類的稱號:比如簽到類(按照用戶簽到天數(shù)解鎖)、捐贈類(按照用戶捐贈項目的行為解鎖),所以對于稱號的解鎖操作應(yīng)該是面向未來可持續(xù)拓展的。
基于這樣的思考,我選擇使用上面的策略模式去實現(xiàn)稱號解鎖模塊。該模塊的核心類圖組織如下:
下面是其中部分核心代碼的分析解讀:
public interface Handler如上文所說,Handler是策略的頂層抽象,它只定義了type方法,該方法用于獲取策略的標(biāo)識,標(biāo)識的類型支持子接口定義。{ /** * handler類型 * * @return */ T type(); }
@Slf4j public abstract class HandlerFactory> implements InitializingBean, ApplicationContextAware { private Map handlerMap; private ApplicationContext appContext; /** * 根據(jù) type 獲得對應(yīng)的handler * * @param type * @return */ public H getHandler(T type) { return handlerMap.get(type); } /** * 根據(jù) type 獲得對應(yīng)的handler,支持返回默認(rèn) * * @param type * @param defaultHandler * @return */ public H getHandlerOrDefault(T type, H defaultHandler) { return handlerMap.getOrDefault(type, defaultHandler); } /** * 反射獲取泛型參數(shù)handler類型 * * @return handler類型 */ @SuppressWarnings("unchecked") protected Class getHandlerType() { Type type = ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[1]; //策略接口使用了范型參數(shù) if (type instanceof ParameterizedTypeImpl) { return (Class ) ((ParameterizedTypeImpl)type).getRawType(); } else { return (Class ) type; } } @Override public void afterPropertiesSet() { // 獲取所有 H 類型的 handlers Collection 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在前面也提到過,是策略工廠的抽象實現(xiàn),封裝了反射獲取具體場景策略接口類型,并查找策略bean在內(nèi)存中維護策略與其標(biāo)識的映射關(guān)系,后續(xù)可以直接通過標(biāo)識或者對應(yīng)的策略實現(xiàn)。這里有二個細節(jié):
為什么HandlerFactory是abstract class?其實可以看到該類并沒有任何抽象方法,直接將其定義為class也不會有什么問題。這里將其定義為abstract class主要是起到實例創(chuàng)建的約束作用,因為我們對該類的定義是工廠的抽象實現(xiàn),只希望針對具體場景來創(chuàng)建實例,針對該工廠本身創(chuàng)建實例其實是沒有任何實際意義的。
getHandlerType方法使用了@SuppressWarnings注解并標(biāo)記了unchecked。這里也確實是存在潛在風(fēng)險的,因為Type類型轉(zhuǎn)Class類型屬于向下類型轉(zhuǎn)換,是存在風(fēng)險的,可能其實際類型并非Class而是其他類型,那么此處強轉(zhuǎn)就會出錯。這里處理了兩種最通用的情況:AbstractHandler是帶范型的class和最普通的class。
@Component public class TitleUnlockHandlerFactory extends HandlerFactoryTitleUnlockHandlerFactory是策略工廠的具體實現(xiàn),由于不需要在此定制策略的路由邏輯,所以只聲明了相關(guān)的參數(shù)類型,而沒有對父類的方法做什么覆蓋。> {}
public abstract class BaseTitleUnlockHandlerimplements Handler { @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 titleAchievements = titleUnlockResult.getUnlockedTitles(); if (CollectionUtils.isEmpty(titleAchievements)) { titleUnlockResult.setUnlockedTitles(new ArrayList<>()); return titleUnlockResult; } //基于注入的bean和計算出的稱號列表進行后置操作,如:更新成就計數(shù)、更新用戶稱號緩存、更新用戶未讀成就等 ...... return titleUnlockResult; } /** * 計算出要解鎖的稱號 * * @param param * @return */ protected abstract TitleUnlockResult doUnlock(T param); @Override public abstract String type(); }
BaseTitleUnlockHandler定義了稱號解鎖行為,并且在此確定了策略標(biāo)識的類型為String。此外,該類是一個abstract class,是因為該類定義了一個模版方法unlockTitles,在該方法里封裝了稱號解鎖所要進行的一些公共操作,比如更新用戶的稱號計數(shù)、用戶的稱號緩存數(shù)據(jù)等,這些都依賴于注入的一些外部bean,而interface不支持非靜態(tài)成員變量,所以該類通過abstract class來定義。具體的稱號解鎖行為通過doUnlock定義,這也是該策略的具體實現(xiàn)類需要實現(xiàn)的方法。
另外也許你還注意到了doUnlock方法的行參是一個范型參數(shù)T,因為我們考慮到了不同類型稱號解鎖所需要的參數(shù)可能是不同的,因此在場景抽象接口側(cè)只依賴于稱號解鎖的公共參數(shù)類型,而在策略接口具體實現(xiàn)側(cè)才與該類型策略的具體參數(shù)類型進行耦合。
@Component public class GameplayTitleUnlockHandler extends BaseTitleUnlockHandler{ @Resource private BlessTitleAchievementDiamondConfig blessTitleAchievementDiamondConfig; @Resource private UserTitleTairManager userTitleTairManager; @Override protected TitleUnlockResult doUnlock(GameplayTitleUnlockParams params) { //獲取稱號元數(shù)據(jù) List titleMetadata = blessTitleAchievementDiamondConfig.getTitleMetadata(); if (CollectionUtils.isEmpty(titleMetadata)) { return null; } List titleAchievements = new ArrayList<>(); Result result = userTitleTairManager.queryRawCache(params.getUserId()); //用戶稱號數(shù)據(jù)查詢異常 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; } ...... }
上面是一個策略的具體實現(xiàn)類的大致示例,可以看到該實現(xiàn)類核心明確了以下信息:
策略標(biāo)識:給出了type方法的具體實現(xiàn),返回了一個策略標(biāo)識的常量
策略處理邏輯:此處是玩法類稱號解鎖的業(yè)務(wù)邏輯,讀者無需關(guān)注其細節(jié)
稱號解鎖行參:給出了玩法類稱號解鎖所需的真實參數(shù)類型
抽象疲勞度管控體系
在我們的業(yè)務(wù)需求中經(jīng)常會遇到涉及疲勞度管控相關(guān)的邏輯,比如每日簽到允許用戶每天完成1次、首頁項目進展彈窗要求對所有用戶只彈1次、首頁限時回訪任務(wù)入口則要對用戶每天都展示一次,但用戶累計完成3次后便不再展示......因此我們設(shè)計了一套疲勞度管控的模式,以降低后續(xù)諸如上述涉及疲勞度管控相關(guān)需求的開發(fā)成本。
自頂向下的視角
這套疲勞度管控體系的類層次大致如下圖: ? 接下來我們自頂向下逐層進行介紹:
FatigueLimiter(interface):FatigueLimiter是最頂層抽象的疲勞度管控接口,它定義了疲勞度管控相關(guān)的行為,比如:疲勞度的查詢、疲勞度清空、疲勞度增加、是否達到疲勞度限制的判斷等。
BaseFatigueLdbLimiter(abstract class):疲勞度數(shù)據(jù)的存儲方案可以是多種多樣的,在我們項目中主要利用ldb進行疲勞度存儲,而BaseFatigueLdbLimiter正是基于ldb【注:阿里內(nèi)部自研的一款持久化k-v數(shù)據(jù)庫,讀者可將其理解為類似level db的項目】對疲勞度數(shù)據(jù)進行管控的抽象實現(xiàn),它封裝了ldb相關(guān)的操作,并基于ldb的數(shù)據(jù)操作實現(xiàn)了FatigueLimiter的疲勞度管控方法。但它并不感知具體業(yè)務(wù)的身份和邏輯,因此定義了幾個業(yè)務(wù)相關(guān)的方法交給下層去實現(xiàn),分別是:
scene:標(biāo)識具體業(yè)務(wù)的場景,會利用該方法返回值去構(gòu)造Ldb存儲的key
buildCustomKey:對Ldb存儲key的定制邏輯
getExpireSeconds:對應(yīng)著Ldb存儲kv失效時間,對應(yīng)著疲勞度的管控周期
Ldb周期性疲勞度管控的解決方案層(abstract class):在這一層提供了多種周期的開箱即用的疲勞度管控實現(xiàn)類,如BaseFatigueDailyLimiter提供的是天級別的疲勞度管控能力,BaseFatigueNoCycleLimiter則表示疲勞度永不過期,而BaseFatigueCycleLimiter則支持用戶實現(xiàn)cycle方法定制疲勞度周期。
業(yè)務(wù)場景層:這一層則是各個業(yè)務(wù)場景對疲勞度管控的具體實現(xiàn),實現(xiàn)類只需要實現(xiàn)scene方法來聲明業(yè)務(wù)場景的身份標(biāo)識,隨后繼承對應(yīng)的解決方案,即可實現(xiàn)快速的疲勞度管控。比如上面的DailyWishSignLimiter就對應(yīng)著本篇開頭我們所說的“每日簽到允許用戶每天完成1次”,這就要求為用戶的簽到行為以天維度構(gòu)建key同時失效時間也為1天,因此直接繼承解決方案層的BaseFatigueDailyLimiter即可。其代碼實現(xiàn)非常簡單,如下:
@Component public class DailyWishSignLimiter extends BaseFatigueLdbDailyLimiter { @Override protected String scene() { return LimiterScene.dailyWish; } }
有一個“異類”
也許你注意到了上面的類層次圖中有一個“異類”——HomeEnterGuideLimiter。它其實就是我們在上文說的“首頁限時回訪任務(wù)入口則要對用戶每天都展示一次,但用戶累計完成3次后便不再展示”,它的邏輯其實也很簡單:因為它有2條管控條件,所以需要繼承2個管控周期的解決方案——天維度和永久維度,最后實際使用的類再聚合了天維度和永久維度的實現(xiàn)類(每個實現(xiàn)類對應(yīng)ldb的一類key)并實現(xiàn)了頂層的疲勞度管控接口,標(biāo)識這也是一個疲勞度管理器。它們的代碼如下:
/** * 首頁入口引導(dǎo)限時任務(wù)-天級疲勞度管控 * */ @Component public class HomeEnterGuideDailyLimiter extends BaseFatigueLdbDailyLimiter { @Override protected String scene() { return LimiterScene.homeEnterGuide; } } /** * 首頁入口引導(dǎo)限時任務(wù)-總次數(shù)疲勞度管控 * */ @Component public class HomeEnterGuideNoCycleLimiter extends BaseFatigueLdbNoCycleLimiter { @Override protected String scene() { return LimiterScene.homeEnterGuide; } @Override protected int maxSize() { return 3; } } /** * 首頁入口引導(dǎo)限時任務(wù)-疲勞度服務(wù) * */ @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 MapbatchQueryLimit(List 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(); } /** * 查詢首頁限時任務(wù)的每日疲勞度 * * @param customKey 用戶自定義key * @return 疲勞度計數(shù) */ public Integer queryDailyLimit(String customKey) { return homeEnterGuideDailyLimiter.queryLimit(customKey); } /** * 查詢首頁限時任務(wù)的全周期疲勞度 * * @param customKey 用戶自定義key * @return 疲勞度計數(shù) */ public Integer queryNoCycleLimit(String customKey) { return homeEnterGuideNoCycleLimiter.queryLimit(customKey); } }
函數(shù)式行為參數(shù)化
Java 21在今年9月份發(fā)布了,而距離Java 8發(fā)布已經(jīng)過去9年多了,但也許,我是說也許......我們有些同學(xué)對Java 8還是不太熟悉......
再談行為參數(shù)化
最早聽到“行為參數(shù)化”這個詞是在經(jīng)典的Java技術(shù)書籍《Java 8實戰(zhàn)》中。在此書中,作者以一個篩選蘋果的案例,基于行為參數(shù)化的思維一步步優(yōu)化重構(gòu)代碼,在提升代碼抽象能力的同時,保證了代碼的簡潔性和可讀性,而其中的秘密武器就是Java 8所引入的Lambda表達式和函數(shù)式接口。Java 8發(fā)布已經(jīng)9年,對于Lambda表達式,大多數(shù)同學(xué)都已經(jīng)耳熟能詳,但函數(shù)式接口也許有同學(xué)不知道代表著什么。簡單來說,如果一個接口,它只有一個沒有被實現(xiàn)的方法,那它就是函數(shù)式接口。java.lang.function包下定義JDK提供的一系列函數(shù)式接口。如果一個接口是函數(shù)式接口,推薦用@FunctionalInterface注解來顯式標(biāo)明。那函數(shù)式接口有什么用呢?如果一個方法的行參里有函數(shù)式接口,那么函數(shù)式接口對應(yīng)的參數(shù)可以支持傳遞Lambda表達式或者方法引用。 那何為“行為參數(shù)化”?直觀地來說就是將行為作為方法/函數(shù)的參數(shù)來進行傳遞。在Java 8之前,這可以通過匿名類實現(xiàn),而在Java 8以后,可以基于函數(shù)式特性來實現(xiàn)行為參數(shù)化,即方法參數(shù)定義為函數(shù)式接口,在具體傳參時使用Lambda表達式/方法。相比匿名類,后者在簡潔性上有極大的提升。 在我們的日常開發(fā)中,如果我們看到兩個方法的結(jié)構(gòu)十分相似,只有其中部分行為存在差別,那么就可以考慮采用函數(shù)式的行為參數(shù)化來重構(gòu)優(yōu)化這段代碼,將其中存在差異的行為抽象成參數(shù),從而減少重復(fù)代碼。
從實踐中來,到代碼中去
下面給出一個例子。在靜心守護項目中,我們基于ldb維護了用戶未讀成就的列表,在用戶進入到個人成就頁時,會查詢未讀成就數(shù)據(jù),并對未讀的成就在成就列表進行置頂以及加紅點展示。下面是對用戶未讀成就列表進行新增和清除的兩個方法:
/** * 清除未讀成就 * * @param uid 用戶ID * @param achievementType 需要清除未讀成就列表的成就類型 * @return */ public boolean clearUnreadAchievements(long uid, SetachievementTypes) { if (CollectionUtils.isEmpty(achievementTypes)) { return true; } Result ldbRes = super.rawGet(buildKey(uid), false); //用戶稱號數(shù)據(jù)查詢失敗 if (Objects.isNull(ldbRes)) { recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build()); return false; } boolean success = false; ResultCode resultCode = ldbRes.getRc(); //不存在用戶稱號數(shù)據(jù)則進行初始化 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(); //存在新數(shù)據(jù)則對其進行更新 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> achievementTypeIdMap) { if (MapUtils.isEmpty(achievementTypeIdMap)) { return true; } Result ldbRes = super.rawGet(buildKey(uid), false); //用戶稱號數(shù)據(jù)查詢失敗 if (Objects.isNull(ldbRes)) { recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build()); return false; } boolean success = false; ResultCode resultCode = ldbRes.getRc(); //不存在用戶稱號數(shù)據(jù)則進行初始化 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(); //存在新數(shù)據(jù)則對其進行更新 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; }
從結(jié)構(gòu)上看,上面兩段代碼其實是非常類似的:整個結(jié)構(gòu)都是先判空,然后查詢歷史的未讀成就數(shù)據(jù),如果數(shù)據(jù)未初始化,則進行初始化,如果已經(jīng)初始化,則對數(shù)據(jù)進行更新。只不過寫入/清除對數(shù)據(jù)的初始化和更新邏輯并不相同。因此可以將數(shù)據(jù)初始化和更新抽象為行為參數(shù),將剩余部分提取為公共方法,基于這樣的思路重構(gòu)后的代碼如下:
/** * 創(chuàng)建or更新緩存 * * @param uid 用戶ID * @param initCacheSupplier 緩存初始化策略 * @param updater 緩存更新策略 * @return */ private boolean upsertCache(long uid, SupplierinitCacheSupplier, Function updater) { Result ldbRes = super.rawGet(buildKey(uid), false); //用戶稱號數(shù)據(jù)查詢失敗 if (Objects.isNull(ldbRes)) { recordErrorCode(InteractErrorCode.UNREAD_ACHIEVEMENT_UPSERT_ERROR, ExceptionBizParams.builder().uid(uid).build()); return false; } boolean success = false; ResultCode resultCode = ldbRes.getRc(); //不存在用戶稱號數(shù)據(jù)則進行初始化 if (Objects.equals(resultCode, ResultCode.DATANOTEXSITS)) { UserUnreadAchievementsCache userUnreadAchievementsCache = initCacheSupplier.get(); success = putCache(uid, userUnreadAchievementsCache, DEFAULT_VERSION); } else if (Objects.equals(resultCode, ResultCode.SUCCESS)) { DataEntry ldbEntry = ldbRes.getValue(); //存在新數(shù)據(jù)則對其進行更新 if (Objects.nonNull(ldbEntry)) { Object data = ldbEntry.getValue(); if (data instanceof String) { UserUnreadAchievementsCache userUnreadAchievementsCache = JSON.parseObject(String.valueOf(data), UserUnreadAchievementsCache.class); userUnreadAchievementsCache = updater.apply(userUnreadAchievementsCache); 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 > achievementTypeIdMap) { if (MapUtils.isEmpty(achievementTypeIdMap)) { return true; } return upsertCache(uid, () -> { UserUnreadAchievementsCache userUnreadAchievementsCache = new UserUnreadAchievementsCache(); achievementTypeIdMap.forEach((key, value) -> updateCertainTypeIds(userUnreadAchievementsCache, key, value)); return userUnreadAchievementsCache; }, oldCache -> { achievementTypeIdMap.forEach((key, value) -> updateCertainTypeIds(oldCache, key, value)); return oldCache; } ); } /** * 清除未讀成就 * * @param uid 用戶ID * @param achievementType 需要清除未讀成就列表的成就類型 * @return */ public boolean clearUnreadAchievements(long uid, Set achievementTypes) { if (CollectionUtils.isEmpty(achievementTypes)) { return true; } return upsertCache(uid, () -> { UserUnreadAchievementsCache userUnreadAchievementsCache = new UserUnreadAchievementsCache(); achievementTypes.forEach(type -> clearCertainTypeIds(userUnreadAchievementsCache, type)); return userUnreadAchievementsCache; }, oldCache -> { achievementTypes.forEach(type -> clearCertainTypeIds(oldCache, type)); return oldCache; } ); }
重構(gòu)的核心是提取了upsert方法,該方法將緩存數(shù)據(jù)的初始化和更新策略以函數(shù)式接口進行定義,從而支持從調(diào)用側(cè)進行透傳,避免了模板方法的重復(fù)編寫。這是一個拋磚引玉的例子,在日常開發(fā)中,我們可以更多地嘗試用函數(shù)式編程的思維去思考和重構(gòu)代碼,也許會發(fā)現(xiàn)另一個神奇的編程世界。
切面編程的一些實踐
AOP想必大家都已經(jīng)十分熟悉了,在此便不再贅述其基本概念,而是開門見山直接分享一些AOP在靜心守護項目中的實際應(yīng)用。
服務(wù)層異常統(tǒng)一收口
靜心守護項目采用了在阿里系統(tǒng)中常用的service-manager-dao的分層模式,其中service層是距離終端最近的一層。為了防止下層預(yù)期外的異常拋到終端,我們需要在service層對異常進行統(tǒng)一攔截并且記錄,同時最好將相關(guān)的錯誤碼、請求參數(shù)以及traceId都一并記下,便于問題排查。這個場景就非常適合使用AOP。在引入AOP之前,我們需要對每個service中面向終端的方法都進行異常攔截和監(jiān)控日志打印的操作。比方說下面這個類,它有3個面向終端mtop【注:阿里內(nèi)部自研的API網(wǎng)關(guān)平臺】服務(wù)的方法(api具體參數(shù)和名稱做了模糊化處理),這3個方法都采用了同樣的try-catch結(jié)構(gòu)來進行異常捕捉和監(jiān)控日志打印,其中存在大量的重復(fù)代碼,而更糟糕的事,如果后續(xù)增加新的方法,這樣的重復(fù)代碼還會不斷增加。
@Slf4j @HSFProvider(serviceInterface = MtopBlessHomeService.class) public class MtopBlessHomeServiceImpl implements MtopBlessHomeService { //依賴的bean注入 ...... @Override public MtopResult看到這樣重復(fù)的代碼結(jié)構(gòu)而只是局部行為的不同,也許我們可以考慮著用上一節(jié)的函數(shù)式行為參數(shù)化進行重構(gòu):將重復(fù)的代碼結(jié)構(gòu)抽取為公共的工具方法,將對manager層的調(diào)用抽象為行為參數(shù)。但在上述場景下,這種做法還是存在一些弊端:entranceA(EntranceARequest request) { try { startDiagnose(request.getUserId()); //該入口下的業(yè)務(wù)邏輯 ...... } catch (InteractBizException e) { log.error("Service invoke fail. Method name:{}, params:{}, errorCode:{}, trace:{}", "MtopBlessHomeServiceImpl.entranceA", buildMethodParamsStr(request), e.getErrCode(), EagleEye.getTraceId()); recordErrorCode(e); return MtopUtils.errMtopResult(e.getErrCode(), e.getErrMsg()); } catch (Exception e) { log.error("Service invoke fail. Method name:{}, params:{}, trace:{}", "MtopBlessHomeServiceImpl.entranceA", buildMethodParamsStr(request), EagleEye.getTraceId(), e); recordErrorCode(InteractErrorCode.SYSTEM_ERROR, ExceptionBizParams.builder().build()); return MtopUtils.sysErrMtopResult(); } finally { DiagnoseClient.end(); } } @Override public MtopResult entranceB(EntranceBRequest request) { try { startDiagnose(request.getUserId()); //該入口下的業(yè)務(wù)邏輯 ...... } catch (InteractBizException e) { log.error("Service invoke fail. Method name:{}, params:{}, errorCode:{}, trace:{}", "MtopBlessHomeServiceImpl.entranceB", buildMethodParamsStr(request), e.getErrCode(), EagleEye.getTraceId()); recordErrorCode(e); return MtopUtils.errMtopResult(e.getErrCode(), e.getErrMsg()); } catch (Exception e) { log.error("Service invoke fail. Method name:{}, params:{}, trace:{}", "MtopBlessHomeServiceImpl.entranceB", buildMethodParamsStr(request), EagleEye.getTraceId(), e); recordErrorCode(InteractErrorCode.SYSTEM_ERROR, ExceptionBizParams.builder().build()); return MtopUtils.sysErrMtopResult(); } finally { DiagnoseClient.end(); } } @Override public MtopResult entranceC(EntranceCRequest request) { try { startDiagnose(query.getUserId()); //該入口下的業(yè)務(wù)邏輯 ...... } catch (InteractBizException e) { log.error("Service invoke fail. Method name:{}, params:{}, errorCode:{}, trace:{}", "MtopBlessHomeServiceImpl.entranceC", buildMethodParamsStr(request), e.getErrCode(), EagleEye.getTraceId()); recordErrorCode(e); return MtopUtils.errMtopResult(e.getErrCode(), e.getErrMsg()); } catch (Exception e) { log.error("Service invoke fail. Method name:{}, params:{}, trace:{}", "MtopBlessHomeServiceImpl.entranceC", buildMethodParamsStr(request), EagleEye.getTraceId(), e); recordErrorCode(InteractErrorCode.SYSTEM_ERROR, ExceptionBizParams.builder().build()); return MtopUtils.sysErrMtopResult(); } finally { DiagnoseClient.end(); } } }
每個服務(wù)的方法還是需要顯式調(diào)用工具類方法
為了保證監(jiān)控信息的齊全,還需要在參數(shù)里手動透傳一些監(jiān)控相關(guān)的信息
而AOP則不存在這些問題:AOP基于動態(tài)代理實現(xiàn),在實現(xiàn)上述邏輯時對服務(wù)層的代碼編寫完全透明。此外,AOP還封裝了調(diào)用端方法的各種元信息,可以輕松實現(xiàn)各種監(jiān)控信息的自動化打印。下面是我們提供的AOP切面。其中值得注意的點是切點的選擇要盡量準(zhǔn)確,避免增強了不必要的方法。下面我們選擇的切點是mtop包下所有Impl結(jié)尾類的public方法。
@Aspect @Component @Slf4j public class MtopServiceAspect { /** * MtopService層服務(wù) */ @Pointcut("execution(public com.taobao.mtop.common.MtopResult com.taobao.gaia.veyron.bless.service.mtop.*Impl.*(..))") public void mtopService(){} /** * 對mtop服務(wù)進行增強 * * @param pjp 接入點 * @return * @throws Throwable */ @Around("com.taobao.gaia.veyron.bless.aspect.MtopServiceAspect.mtopService()") public Object enhanceService(ProceedingJoinPoint pjp) throws Throwable { try { startDiagnose(pjp); return pjp.proceed(); } catch (InteractBizException e) { log.error("Service invoke fail. Method name:{}, params:{}, errorCode:{}, trace:{}", AspectUtils.extractMethodName(pjp), buildMethodParamsStr(pjp), e.getErrCode(), EagleEye.getTraceId()); recordErrorCode(e); return MtopUtils.errMtopResult(e.getErrCode(), e.getErrMsg()); } catch (Exception e) { log.error("Service invoke fail. Method name:{}, params:{}, trace:{}", AspectUtils.extractMethodName(pjp), buildMethodParamsStr(pjp), EagleEye.getTraceId(), e); recordErrorCode(InteractErrorCode.SYSTEM_ERROR, ExceptionBizParams.builder().build()); return MtopUtils.sysErrMtopResult(); } finally { DiagnoseClient.end(); } } }
存在這樣一個切面后,service層的代碼就可以變得非常簡潔:只需要純粹專注于業(yè)務(wù)邏輯。同樣以剛才的MtopBlessHomeServiceImpl類為例,在AOP改寫后的代碼里可以去除掉原先異常收口和監(jiān)控相關(guān)的內(nèi)容,而僅保留業(yè)務(wù)邏輯部分,代碼簡潔性大大提升。
@Slf4j @HSFProvider(serviceInterface = MtopBlessHomeService.class) public class MtopBlessHomeServiceImpl implements MtopBlessHomeService { //依賴的bean注入 ...... @Override public MtopResultentranceA(EntranceARequest request) { //業(yè)務(wù)邏輯 ...... } @Override public MtopResult entranceB(EntranceBRequest request) { //業(yè)務(wù)邏輯 ...... } @Override public MtopResult entranceC(EntranceCRequest request) { //業(yè)務(wù)邏輯 ...... } }
切點選擇的策略
除了服務(wù)層以外,我們還想對數(shù)據(jù)訪問層進行監(jiān)控,監(jiān)控項目中各種數(shù)據(jù)存儲工具的RT以及成功率相關(guān)指標(biāo),并且監(jiān)控粒度要盡可能地貼近業(yè)務(wù)維度(整體的數(shù)據(jù)訪問監(jiān)控直接通過eagleeye查看即可),便于具體問題的定位排查。這種面向?qū)蛹墑e的邏輯定制,我們很自然而然地想到了AOP,這也正是它可以大顯身手的場景。 這節(jié)核心想要分享的則是切點的選擇。靜心守護項目的數(shù)據(jù)存儲主要依賴于Tair【注:阿里內(nèi)部自研的高性能K-V存儲系統(tǒng)。根據(jù)存儲介質(zhì)和使用場景不同又分為LDB、MDB、RDB】、Lindorm【注:阿里內(nèi)部自研的大規(guī)模云原生多模數(shù)據(jù)庫服務(wù)】和Mysql,這三種存儲工具在代碼中的使用各不相同,導(dǎo)致切點的選擇策略也大相徑庭。
目標(biāo)對象規(guī)律分布
如果我們要選擇增強的對象在項目中分布的非常規(guī)律,那么我們往往可以直接利用Spring AOP的PointCut語法來選擇切點。以靜心守護項目中的Mysql數(shù)據(jù)訪問對象為例:我們使用的ORM框架是mybatis,并且主要的用法是注解模式,所有的SQL邏輯都放在一個DAO包下,每個業(yè)務(wù)場景定義一個DAO結(jié)尾的Mapper接口,接口下的每個方法都對應(yīng)著一種數(shù)據(jù)訪問的方式。因此在切點選擇時,我們可以直接選擇DAO包下以DAO結(jié)尾的類,并選擇其中public方法即可準(zhǔn)確織入所有滿足條件的切點。
@Pointcut("execution(public * com.taobao.gaia.serverless.veyron.bless.dao.*DAO.*(..))") public void charityProjectDataAccess() { }
這樣實現(xiàn)的監(jiān)控粒度是具體到每個DAO對象-方法級別的粒度,監(jiān)控效果如下:
一個失效案例
靜心守護項目中對tair的使用方式是:通過一個抽象類對tair的各種基礎(chǔ)操作進行封裝(包括參數(shù)校驗、響應(yīng)判空、異常處理等),但將具體tair實例相關(guān)的參數(shù)設(shè)置行為抽象化,由實現(xiàn)類決定。各個業(yè)務(wù)場景的tair管理類最終會基于抽象類封裝的基礎(chǔ)操作來對tair進行數(shù)據(jù)訪問。 如下圖,AbstractLdbManager是封裝
由于各個業(yè)務(wù)場景的tair管理實現(xiàn)類分散在各個業(yè)務(wù)包下,想要對它們進行統(tǒng)一切入比較困難。因此我們選擇對抽象類進行切入。但這樣就會遇到一個同類調(diào)用導(dǎo)致AOP失效的問題:抽象類本身不會有實例對象,因此基于CGLIB創(chuàng)建代理對象后,代理對象本質(zhì)上調(diào)用的還是各個業(yè)務(wù)場景tair管理類的對象,而在使用這些對象時,我們不會直接調(diào)用tair抽象類封裝的數(shù)據(jù)訪問方法,而是調(diào)用這些業(yè)務(wù)tair管理對象進一步封裝的帶業(yè)務(wù)語義的方法,基于這些方法再去調(diào)用tair抽象類的數(shù)據(jù)訪問方法。這種同類方法間接調(diào)用最終就導(dǎo)致了抽象類的方法沒有如期被增強。文字描述興許有些繞,可以參考下面的圖:
我們選擇的解決方法則是從上面的MultiClusterTairManager入手,這個類是tair為我們提供的TairManger的一種默認(rèn)實現(xiàn),我們之前的做法是為該類實例化一個bean,然后提供給所有業(yè)務(wù)Tair管理類使用,也就是說所有業(yè)務(wù)Tair管理類使用的TairManager都是同一個bean實例(因為業(yè)務(wù)流量沒那么大,一個tair實例暫時綽綽有余)。那么我們可以自己提供一個TairManager的實現(xiàn),基于繼承+組合MultiClusterTairManager的方式,只對我們項目內(nèi)用到數(shù)據(jù)訪問操作進行重寫,并委托給原先的MultiClusterTairManager bean進行處理。這樣我們可以在設(shè)置AOP切點時選擇對自己實現(xiàn)的TairManager的所有方法做增強,進而避開上面的問題。經(jīng)過這樣改寫后,上面的兩張圖會演變成下面這樣:
基于注解切入
還有一種場景是我們要增強的方法分布毫無規(guī)律,可能都在同一個類中,但方法的名稱毫無規(guī)律,也無法簡單通過private或者public來區(qū)別。針對這樣的場景,我們的做法是自定義注解,專門用于標(biāo)識需要做增強的方法。比如靜心守護項目中l(wèi)indorm相關(guān)的數(shù)據(jù)操作就是這樣。我們定義注解:
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface VeyronJoinPoint {}
并將該注解標(biāo)識在需要增強的方法上,隨后通過下面的方式描述切點,即可獲取到所有需要增強的方法。
@Pointcut("@annotation(com.taobao.gaia.serverless.veyron.aspect.VeyronJoinPoint)") public void lindormDataAccess() {}
上面的方法也有進一步改良的空間:在注解內(nèi)增加屬性來描述具體的業(yè)務(wù)場景,不同的切面根據(jù)業(yè)務(wù)場景來對捕獲的方法進行過濾,只留下當(dāng)前業(yè)務(wù)場景所需要的方法。不然按照現(xiàn)有的做法,如果新的切面也要基于注解來尋找切點,那只能定義新的注解,否則會與原先注解產(chǎn)生沖突。
總結(jié)
業(yè)務(wù)需求千變?nèi)f化,對應(yīng)的解法也見仁見智。在研發(fā)過程中對各種變化中不變的部分進行總結(jié),從中提取出自己的模式與方法論進行整理沉淀,會讓我們以后跑的更快。也正應(yīng)了學(xué)生時期,老師常說的那句話:“我們要把厚厚的書本讀薄才能裝進腦子里?!?/p>
審核編輯:湯梓紅
-
編程
+關(guān)注
關(guān)注
88文章
3671瀏覽量
94675 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4364瀏覽量
63814 -
容器
+關(guān)注
關(guān)注
0文章
503瀏覽量
22313 -
spring
+關(guān)注
關(guān)注
0文章
340瀏覽量
14780
原文標(biāo)題:關(guān)于編程模式的總結(jié)與思考
文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
編程:思考還是打字
關(guān)于USB的知識總結(jié)
ARM嵌入式系統(tǒng)的問題總結(jié)分析
DXP關(guān)于板層說明及總結(jié)
Linux下的網(wǎng)絡(luò)編程總結(jié)
關(guān)于Linux下多線程編程技術(shù)學(xué)習(xí)總結(jié)

AT燒錄軟件Progisp和使用手冊和對于ISP編程進入不了編程模式的總結(jié)

事件總線模式知識總結(jié)
關(guān)于risc-v啟動部分的思考

C 語言編程習(xí)慣總結(jié)

評論