使用策略模式優化代碼實踐,如何讓項目快速起飛

推薦閱讀:

一、背景

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章