一、背景
之前接手了一個 springboot 專案。在我負責的模組中,有一塊使用者註冊的功能,但是比較特別的是這個註冊並不是重新註冊,而是從以前的舊系統的資料庫中同步舊資料到新系統的資料庫中。由於這些使用者角色來自於不同的系統,所以我需要在註冊的時候先判斷型別(這個型別由一個專門的列舉類提供),再去呼叫已經寫好的同步方法同步資料。
虛擬碼大概是這樣的:
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 表示式的形式將各個模組的同步方法放進去,這樣就不需要改動模組的程式碼了。
新增的介面如下:
@FunctionalInterface
public interface IUserSynchronizeSerivice {
/**
* 同步方法
*/
public boolean sync(String userId, String projectId, String declareId);
}
2.策略池的實現
接著,為了實現原本 if-else 的邏輯,我需要一個策略池,能夠建立起一個使用者型別跟對應的同步策略的對映關係,一開始,我打算直接寫在 register()
方法所在的類中加入以下程式碼:
@Autowired
private AUserService aUserService;
@Autowired
private BUserService bUserService;
private static final Map<String, UserSynchronizeTyeEnum.IUserSynchronizeService> synchronizeServiceStrategy = new HashMap<>();
@PostConstruct
private void strategyInit(){
// spring容器啟動後將策略裝入策略池
synchronizeServiceStrategy.put(UserSynchronizeTyeEnum.A.type, aUserService::synchronization);
synchronizeServiceStrategy.put(UserSynchronizeTyeEnum.B.type, bUserService::sync);
}
但是這樣在新增新的使用者型別時,需要先去列舉類新增新列舉,然後再回到register()
所在的類為策略池新增策略,這個兩個邏輯上相連的過程被分散到了兩個地方,而且仍然要修改register()
所在類的程式碼。所以決定不用上述的程式碼,而是去對列舉類下手。
原本的列舉類是這樣的:
/**
* 老系統使用者註冊,使用者型別與同步方法的列舉類
*/
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 獲取物件的工具類。
/**
* 老系統使用者註冊,使用者型別與同步方法的列舉類
* 新增新型別時,需要將模組對應的同步方法一併放入
*/
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()
方法,用於直接獲取同步方法:
/**
* 根據列舉值獲取對應同步方法
*/
public Optional<IUserSynchronizeService> getSynchronizeService(String type) {
for (UserSynchronizeTyeEnum tyeEnum : UserSynchronizeTyeEnum.values()) {
if (tyeEnum.type.equals(type)) {
return Optional.of(tyeEnum.synchronizeService);
}
}
return Optional.empty();
}
到目前為止,策略池已經基本完成了,但是我們不難發現,現在為策略介面新增實現的地方也變成了列舉類中,策略介面 IUserSynchronizeService
一般也不會被用在其他地方,因此不妨把策略介面也一併引入列舉類中,讓他成為一個列舉類的內部介面。
現在,列舉類是這樣的:
列舉類堆外只暴露根據型別獲取方法的IUserSynchronizeService()
方法,以及 A 和 B 兩個列舉。
完整的 UserSynchronizeTyeEnum
列舉類程式碼如下:
/**
* 老系統使用者註冊,使用者型別與同步方法的列舉類
* 新增新型別時,需要將模組對應的同步方法一併放入。待使用者註冊時,會遍歷列舉物件並根據型別獲取對應的同步方法執行。
*/
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<IUserSynchronizeService> 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<Map<String, Object>> dateList, String userId, String projectId, String declareId);
}
}
三、使用
現在,改造完畢,可以開始使用了,對於原先的 register()
方法,現在改為:
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 類使用者的同步註冊的時候,只需要前往列舉類新增:
/**
* 型別C的使用者
*/
C("c", (userId, projectId, declareId) -> {
return SpringUtils.getBean(CUserService.class).sync(userId, projectId, declareId);
});
即可,register()
方法就不需要再做修改了。