開發與維運

設計模式最佳套路—— 愉快地使用策略模式

作者|周密(之葉)

策略模式(Strategy Pattern)定義了一組策略,分別在不同類中封裝起來,每種策略都可以根據當前場景相互替換,從而使策略的變化可以獨立於操作者。比如我們要去某個地方,會根據距離的不同(或者是根據手頭經濟狀況)來選擇不同的出行方式(共享單車、坐公交、滴滴打車等等),這些出行方式即不同的策略。

何時使用策略模式

阿里開發規約-編程規約-控制語句-第六條 :超過 3 層的 if-else 的邏輯判斷代碼可以使用衛語句、策略模式、狀態模式等來實現。相信大家都見過這種代碼:

if (conditionA) {
    邏輯1
} else if (conditionB) {
    邏輯2
} else if (conditionC) {
    邏輯3
} else {
    邏輯4
}

這種代碼雖然寫起來簡單,但是很明顯違反了面向對象的 2 個基本原則:

  • 單一職責原則(一個類應該只有一個發生變化的原因):因為之後修改任何一個邏輯,當前類都會被修改
  • 開閉原則(對擴展開放,對修改關閉):如果此時需要添加(刪除)某個邏輯,那麼不可避免的要修改原來的代碼

因為違反了以上兩個原則,尤其是當 if-else 塊中的代碼量比較大時,後續代碼的擴展和維護就會逐漸變得非常困難且容易出錯,使用衛語句也同樣避免不了以上兩個問題。因此根據我的經驗,得出一個我個人認為比較好的實踐:

  • if-else 不超過 2 層,塊中代碼 1~5 行,直接寫到塊中,否則封裝為方法
  • if-else 超過 2 層,但塊中的代碼不超過 3 行,儘量使用衛語句
  • if-else 超過 2 層,且塊中代碼超過 3 行,儘量使用策略模式

愉快地使用策略模式

在 Spring 中,實現策略模式的方法多種多樣,下面我分享一下我目前實現策略模式的 “最佳套路”(如果你有更好的套路,歡迎賜教,一起討論哦)。

沒時間解釋了快上車

image.png

▐ 需求背景

我們平臺的動態表單,之前專門用於模型輸入的提交。現在業務方希望對錶單能力進行開放,除了可用於模型提交,還可以用於業務方指定功能的提交(方式設計為綁定一個 HSF 泛化服務,HSF 即淘系內部的 RPC 框架)。加上我們在配置表單時的 “預覽模式” 下的提交,那麼表單目前便有以下三種提交類型:

  • 預覽表單時的提交
  • 模型輸入時的提交
  • 綁定 HSF 時的提交

現在,有請我的 “最佳套路” 上場。

▐ 第一步,定義策略接口

首先定義策略的接口,包括兩個方法:

1、獲取策略類型的方法

2、處理策略邏輯的方法

/**
 * 表單提交處理器
 */
public interface FormSubmitHandler<R extends Serializable> {

    /**
     * 獲得提交類型(返回值也可以使用已經存在的枚舉類)
     *
     * @return 提交類型
     */
    String getSubmitType();

    /**
     * 處理表單提交請求
     *
     * @param request 請求
     * @return 響應,left 為返回給前端的提示信息,right 為業務值
     */
    CommonPairResponse<String, R> handleSubmit(FormSubmitRequest request);
}
/**
 * 表單提交的請求
 */
@Getter
@Setter
public class FormSubmitRequest {

    /**
     * 提交類型
     *
     * @see FormSubmitHandler#getSubmitType()
     */
    private String submitType;

    /**
     * 用戶 id
     */
    private Long userId;

    /**
     * 表單提交的值
     */
    private Map<String, Object> formInput;

    // 其他屬性
}

其中,FormSubmitHandler 的 getSubmitType 方法用來獲取表單的提交類型(即策略類型),用於根據客戶端傳遞的參數直接獲取到對應的策略實現;客戶端傳遞的相關參數都被封裝為 FormSubmitRequest,傳遞給 handleSubmit 進行處理。

▐ 第二步,相關策略實現

預覽表單時的提交

@Component
public class FormPreviewSubmitHandler implements FormSubmitHandler<Serializable> {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public String getSubmitType() { return "preview"; }

    @Override
    public CommonPairResponse<String, Serializable> handleSubmit(FormSubmitRequest request) {
        logger.info("預覽模式提交:userId={}, formInput={}", request.getUserId(), request.getFormInput());

        return CommonPairResponse.success("預覽模式提交數據成功!", null);
    }
}

模型輸入時的提交

@Component
public class FormModelSubmitHandler implements FormSubmitHandler<Long> {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public String getSubmitType() { return "model"; }

    @Override
    public CommonPairResponse<String, Long> handleSubmit(FormSubmitRequest request) {
        logger.info("模型提交:userId={}, formInput={}", request.getUserId(), request.getFormInput());

        // 模型創建成功後獲得模型的 id
        Long modelId = createModel(request);

        return CommonPairResponse.success("模型提交成功!", modelId);
    }

    private Long createModel(FormSubmitRequest request) {
        // 創建模型的邏輯
        return 123L;
    }
}

HSF 模式的提交

@Component
public class FormHsfSubmitHandler implements FormSubmitHandler<Serializable> {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public String getSubmitType() { return "hsf"; }

    @Override
    public CommonPairResponse<String, Serializable> handleSubmit(FormSubmitRequest request) {
        logger.info("HSF 模式提交:userId={}, formInput={}", request.getUserId(), request.getFormInput());

        // 進行 HSF 泛化調用,獲得業務方返回的提示信息和業務數據
        CommonPairResponse<String, Serializable> response = hsfSubmitData(request);

        return response;
    }

    ...
}

▐ 第三步,建立策略的簡單工廠

@Component
public class FormSubmitHandlerFactory implements InitializingBean, ApplicationContextAware {

    private static final
    Map<String, FormSubmitHandler<Serializable>> FORM_SUBMIT_HANDLER_MAP = new HashMap<>(8);

    private ApplicationContext appContext;

    /**
     * 根據提交類型獲取對應的處理器
     *
     * @param submitType 提交類型
     * @return 提交類型對應的處理器
     */
    public FormSubmitHandler<Serializable> getHandler(String submitType) {
        return FORM_SUBMIT_HANDLER_MAP.get(submitType);
    }

    @Override
    public void afterPropertiesSet() {
        // 將 Spring 容器中所有的 FormSubmitHandler 註冊到 FORM_SUBMIT_HANDLER_MAP
        appContext.getBeansOfType(FormSubmitHandler.class)
                  .values()
                  .forEach(handler -> FORM_SUBMIT_HANDLER_MAP.put(handler.getSubmitType(), handler));
    }

    @Override
    public void setApplicationContext(@NonNull ApplicationContext applicationContext) {
        appContext = applicationContext;
    }
}

我們讓 FormSubmitHandlerFactory 實現 InitializingBean 接口,在 afterPropertiesSet 方法中,基於 Spring 容器將所有 FormSubmitHandler 自動註冊到 FORM_SUBMIT_HANDLER_MAP,從而 Spring 容器啟動完成後, getHandler 方法可以直接通過 submitType 來獲取對應的表單提交處理器。

▐ 第四步,使用 & 測試

在表單服務中,我們通過 FormSubmitHandlerFactory 來獲取對應的表單提交處理器,從而處理不同類型的提交:

@Service
public class FormServiceImpl implements FormService {

    @Autowired
    private FormSubmitHandlerFactory submitHandlerFactory;

    public CommonPairResponse<String, Serializable> submitForm(@NonNull FormSubmitRequest request) {
        String submitType = request.getSubmitType();

        // 根據 submitType 找到對應的提交處理器
        FormSubmitHandler<Serializable> submitHandler = submitHandlerFactory.getHandler(submitType);

        // 判斷 submitType 對應的 handler 是否存在
        if (submitHandler == null) {
            return CommonPairResponse.failure("非法的提交類型: " + submitType);
        }

        // 處理提交
        return submitHandler.handleSubmit(request);
    }
}

Factory 只負責獲取 Handler,Handler 只負責處理具體的提交,Service 只負責邏輯編排,從而達到功能上的 “低耦合高內聚”。

ac8edc1f57e833d4c5a907b379244a44.gif

寫一個簡單的 Controller:

@RestController
public class SimpleController {

    @Autowired
    private FormService formService;

    @PostMapping("/form/submit")
    public CommonPairResponse<String, Serializable> submitForm(@RequestParam String submitType,
                                                               @RequestParam String formInputJson) {
        JSONObject formInput = JSON.parseObject(formInputJson);

        FormSubmitRequest request = new FormSubmitRequest();
        request.setUserId(123456L);
        request.setSubmitType(submitType);
        request.setFormInput(formInput);

        return formService.submitForm(request);
    }
}

最後來個簡單的測試:

image.png
image.png
image.png

我感覺到了,這就是非常流暢的感覺~

d9b9d8f0a77ce6f127e6ff6a384a8e4d.gif

▐ 設想一次擴展

如果我們需要加入一個新的策略,比如綁定 FaaS 函數的提交,我們只需要添加一個新的策略實現即可:

@Component
public class FormFaasSubmitHandler implements FormSubmitHandler<Serializable> {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public String getSubmitType() { return "faas"; }

    @Override
    public CommonPairResponse<String, Serializable> handleSubmit(FormSubmitRequest request) {
        logger.info("FaaS 模式的提交:userId={}, formInput={}", request.getUserId(), request.getFormInput());

        // 進行 FaaS 函數調用,並獲得業務方返回的提示信息和業務數據
        CommonPairResponse<String, Serializable> response = faasSubmitData(request);

        return response;
    }

    ... 
}

此時不需要修改任何代碼,因為 Spring 容器重啟時會自動將 FormFaasSubmitHandler 註冊到 FormSubmitHandlerFactory 中 —— 面向 Spring 編程,太香惹~

Leave a Reply

Your email address will not be published. Required fields are marked *