1. 程式人生 > >記一次使用策略模式優化程式碼的經歷

記一次使用策略模式優化程式碼的經歷

## 一、背景 之前接手了一個 springboot 專案。在我負責的模組中,有一塊使用者註冊的功能,但是比較特別的是這個註冊並不是重新註冊,而是從以前的舊系統的資料庫中同步舊資料到新系統的資料庫中。由於這些使用者角色來自於不同的系統,所以我需要在註冊的時候先判斷型別(這個型別由一個專門的列舉類提供),再去呼叫已經寫好的同步方法同步資料。 虛擬碼大概是這樣的: ~~~java public void register(String type, String userId, String projectId, String declareId){ // 判斷使用者型別 if (UserSynchronizeTyeEnum.A.type.equals(type)) { // 同步A型別的資料 } else if (UserSynchronizeTyeEnum.A.type.equals(type)) { // 同步B型別的資料 } else { throw new RuntimeException("不存在的使用者型別"); } ... ... } ~~~ 由於使用者的型別比較多,所以當我接手的時候已經有8個 if-esle 了,由於這個專案會逐步的跟其他平臺對接,要同步的使用者型別會越來越多,而且也不能排除什麼時候不新增,反而要取消一部分型別的同步情況。 就這個情況來說,一方面每一次新增型別都會讓 if-else 串越來越長,取消一些型別的同步還要直接刪除 if-else 裡的對應程式碼;另一方面,這個業務的需求相對穩定,同步方法會不一樣,但是一定會根據型別來判斷。出於以上考慮,我決定趁現在牽扯範圍不大的時候重構一下。 ## 二、思路 ### 1.抽取策略介面和策略類 首先,由於每種使用者型別的同步方法是由各模組自己提供的,其實已經抽出了策略,只是沒有實現一個統一的策略介面。 但是我在這一步遇上了問題: - 各模組的同步方法的名稱不全部一樣; - 由於年代久遠,舊程式碼是不允許改的。 程式碼不讓改,就沒法通過為舊實現類新增介面實現多型,方法名不一樣,那麼反射這條路子也走不通。我想到了裝飾器,為每個實現類新增一個裝飾器類,註冊的時候通過裝飾器去呼叫同步方法,但是這樣缺點很明顯,會引入一個裝飾器介面+n多個裝飾器類,為了優化這一個方法,反而要引入十幾個類,實在是脫褲子放屁。 但是好在天無絕人之路,他們並不是完全沒有相同點: - 雖然引數名不一樣,但是**每個同步方法都需要的引數數量和型別都是一樣**的; - **他們都返回一個布林值** 這讓我想起了 JDK8 的函式式介面,將策略介面改造為函式式介面,由於同步方法的引數和返回值型別都是一樣的,就可以直接以 Lambda 表示式的形式將各個模組的同步方法放進去,這樣就不需要改動模組的程式碼了。 新增的介面如下: ~~~java @FunctionalInterface public interface IUserSynchronizeSerivice { /** * 同步方法 */ public boolean sync(String userId, String projectId, String declareId); } ~~~ ### 2.策略池的實現 接著,為了實現原本 if-else 的邏輯,我需要一個策略池,能夠建立起一個使用者型別跟對應的同步策略的對映關係,一開始,我打算直接寫在 `register()`方法所在的類中加入以下程式碼: ~~~java @Autowired private AUserService aUserService; @Autowired private BUserService bUserService; private static final Map synchronizeServiceStrategy = new HashMap<>(); @PostConstruct private void strategyInit(){ // spring容器啟動後將策略裝入策略池 synchronizeServiceStrategy.put(UserSynchronizeTyeEnum.A.type, aUserService::synchronization); synchronizeServiceStrategy.put(UserSynchronizeTyeEnum.B.type, bUserService::sync); } ~~~ 但是這樣在新增新的使用者型別時,需要先去列舉類新增新列舉,然後再回到`register()`所在的類為策略池新增策略,這個兩個邏輯上相連的過程被分散到了兩個地方,而且仍然要修改`register()`所在類的程式碼。所以決定不用上述的程式碼,而是去對列舉類下手。 原本的列舉類是這樣的: ~~~java /** * 老系統使用者註冊,使用者型別與同步方法的列舉類 */ public enum UserSynchronizeTyeEnum { /** * 型別A的使用者 */ A("a"), /** * 型別B的使用者 */ B("b"); /** * 使用者型別 */ private final String type; UserSynchronizeTyeEnum(String type) { this.type = type; } public String getType() { return type; } } ~~~ 為了保證邏輯能夠集中,我決定將新增策略這一過程一起放到到列舉類裡,在新增列舉的時候就把策略一起放進去: > 注:下文的 SpringUtils 實現了 BeanFactoryPostProcessor 介面,是一個用於從 ConfigurableListableBeanFactory 獲取物件的工具類。 ~~~java /** * 老系統使用者註冊,使用者型別與同步方法的列舉類 * 新增新型別時,需要將模組對應的同步方法一併放入 */ public enum UserSynchronizeTyeEnum { /** * 型別A的使用者 */ A("a", (userId, projectId, declareId) -> { return SpringUtils.getBean(AUserService.class).synchronization(userId, projectId, declareId); }), /** * 型別B的使用者 */ B("b", (userId, projectId, declareId) -> { return SpringUtils.getBean(BUserService.class).sync(userId, projectId, declareId); }); /** * 使用者型別 */ private final String type; /** * 同步方法 */ private final IUserSynchronizeService synchronizeService; UserSynchronizeTyeEnum(String type, IUserSynchronizeService synchronizeService) { this.type = type; this.synchronizeService = synchronizeService; } } ~~~ 由於由於列舉類已經相當於之前策略池的 Map 集合了,所以我們直接在裡面新增一個 `getSynchronizeService()`方法,用於直接獲取同步方法: ~~~java /** * 根據列舉值獲取對應同步方法 */ public Optional getSynchronizeService(String type) { for (UserSynchronizeTyeEnum tyeEnum : UserSynchronizeTyeEnum.values()) { if (tyeEnum.type.equals(type)) { return Optional.of(tyeEnum.synchronizeService); } } return Optional.empty(); } ~~~ 到目前為止,策略池已經基本完成了,但是我們不難發現,現在為策略介面新增實現的地方也變成了列舉類中,策略介面 `IUserSynchronizeService` 一般也不會被用在其他地方,因此不妨**把策略介面也一併引入列舉類中,讓他成為一個列舉類的內部介面**。 現在,列舉類是這樣的: ![策略模式的列舉類](http://img.xiajibagao.top/image-20201121161017904.png) 列舉類堆外只暴露根據型別獲取方法的`IUserSynchronizeService()` 方法,以及 A 和 B 兩個列舉。 完整的 `UserSynchronizeTyeEnum`列舉類程式碼如下: ~~~java /** * 老系統使用者註冊,使用者型別與同步方法的列舉類 * 新增新型別時,需要將模組對應的同步方法一併放入。待使用者註冊時,會遍歷列舉物件並根據型別獲取對應的同步方法執行。 */ public enum UserSynchronizeTyeEnum { /** * 型別A的使用者 */ A("a", (userId, projectId, declareId) -> { return SpringUtils.getBean(AUserService.class).synchronization(userId, projectId, declareId); }), /** * 型別B的使用者 */ B("b", (userId, projectId, declareId) -> { return SpringUtils.getBean(BUserService.class).sync(userId, projectId, declareId); }); /** * 使用者型別 */ public String type; /** * 同步方法 */ public IUserSynchronizeService synchronizeService; UserSynchronizeTyeEnum(String type, IUserSynchronizeService synchronizeService) { this.type = type; this.synchronizeService = synchronizeService; } /** * 根據列舉值獲取對應同步方法 */ public static Optional getSynchronizeService(String type) { for (UserSynchronizeTyeEnum tyeEnum : UserSynchronizeTyeEnum.values()) { if (tyeEnum.type.equals(type)) { return Optional.of(tyeEnum.synchronizeService); } } return Optional.empty(); } /** * 同步方法需要符合函式式介面 */ @FunctionalInterface public interface IUserSynchronizeService { boolean sync(List> dateList, String userId, String projectId, String declareId); } } ~~~ ## 三、使用 現在,改造完畢,可以開始使用了,對於原先的 `register()`方法,現在改為: ~~~java public void register(String type, String userId, String projectId, String declareId){ // 獲取同步方法,沒有就拋異常 UserSynchronizeTyeEnum.IUserSynchronizeService synchronizeService = UserSynchronizeTyeEnum.getSynchronizeService(type) .orElseThrow(() -> new RuntimeException("型別不存在")); // 同步使用者資料 synchronizeService.sync(userId, projectId, declareId); } ~~~ 當我們需要再新增一個 C 類使用者的同步註冊的時候,只需要前往列舉類新增: ~~~java /** * 型別C的使用者 */ C("c", (userId, projectId, declareId) -> { return SpringUtils.getBean(CUserService.class).sync(userId, projectId, declareId); }); ~~~ 即可,`register()`方法就不需要再做修