作者|周密(之葉)
政策模式(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 程式設計,太香惹~