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

Createsequence 發表於 2020-11-21

一、背景

之前接手了一個 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()方法就不需要再做修改了。