天天看點

設計模式最佳套路—— 愉快地使用政策模式何時使用政策模式愉快地使用政策模式

作者|周密(之葉)

政策模式(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 中,實作政策模式的方法多種多樣,下面我分享一下我目前實作政策模式的 “最佳套路”(如果你有更好的套路,歡迎賜教,一起讨論哦)。

沒時間解釋了快上車

設計模式最佳套路—— 愉快地使用政策模式何時使用政策模式愉快地使用政策模式

▐ 需求背景

我們平台的動态表單,之前專門用于模型輸入的送出。現在業務方希望對表單能力進行開放,除了可用于模型送出,還可以用于業務方指定功能的送出(方式設計為綁定一個 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 隻負責邏輯編排,進而達到功能上的 “低耦合高内聚”。

設計模式最佳套路—— 愉快地使用政策模式何時使用政策模式愉快地使用政策模式

寫一個簡單的 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);
    }
}           

最後來個簡單的測試:

設計模式最佳套路—— 愉快地使用政策模式何時使用政策模式愉快地使用政策模式
設計模式最佳套路—— 愉快地使用政策模式何時使用政策模式愉快地使用政策模式
設計模式最佳套路—— 愉快地使用政策模式何時使用政策模式愉快地使用政策模式

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

設計模式最佳套路—— 愉快地使用政策模式何時使用政策模式愉快地使用政策模式

▐ 設想一次擴充

如果我們需要加入一個新的政策,比如綁定 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 程式設計,太香惹~