策略模式作為一種軟體設計模式,指物件有某個行為,但是在不同的場景中,該行為有不同的實現演算法,可以替代程式碼中大量的 if-else。
那麼在什麼時候使用策略模式呢?
在《阿里巴巴Java開發手冊》中有提到當超過 3 層的 if-else 的邏輯判斷程式碼可以使用策略模式來實現。
在 Spring 中實現策略模式的方式有很多種,下面透過一個案例來演示下,比如有個需求需要實現支援第三方登入,目前需要支援以下三種登入方式:
策略模式結構如下圖所示:
策略模式結構
主要包括一個登入介面類和幾種登入方式的實現方式,並利用簡單工廠來獲取對應的處理器。
定義策略介面首先定義一個登入的策略介面 LoginHandler,其中包括兩個方法:
獲取策略型別的方法處理策略邏輯的方法public interface LoginHandler<T extends Serializable> { /** * 獲取登入型別 * * @return */ LoginType getLoginType(); /** * 登入 * * @param request * @return */ LoginResponse<String, T> handleLogin(LoginRequest request);}
其中,LoginHandler 的 getLoginType 方法用來獲取登入的型別(即策略型別),用於根據客戶端傳遞的引數直接獲取到對應的策略實現。
客戶端傳遞的相關引數都被封裝為 LoginRequest,傳遞給 handleLogin 進行處理。
@Datapublic class LoginRequest { private LoginType loginType; private Long userId;}
其中,根據需求定義登入型別列舉如下:
public enum LoginType { QQ, WE_CHAT, WEI_BO;}
實現策略介面
在定義好策略介面後,我們就需要根據各種第三方登入來實現對應的處理邏輯就可以了。
微信登入@Componentpublic class WeChatLoginHandler implements LoginHandler<String> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); /** * 獲取登入型別 * * @return */ @Override public LoginType getLoginType() { return LoginType.WE_CHAT; } /** * 登入 * * @param request * @return */ @Override public LoginResponse<String, String> handleLogin(LoginRequest request) { logger.info("微信登入:userId:{}", request.getUserId()); String weChatName = getWeChatName(request); return LoginResponse.success("微信登入成功", weChatName); } private String getWeChatName(LoginRequest request) { return "wupx"; }}
QQ 登入@Componentpublic class QQLoginHandler implements LoginHandler<Serializable> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); /** * 獲取登入型別 * * @return */ @Override public LoginType getLoginType() { return LoginType.QQ; } /** * 登入 * * @param request * @return */ @Override public LoginResponse<String, Serializable> handleLogin(LoginRequest request) { logger.info("QQ登入:userId:{}", request.getUserId()); return LoginResponse.success("QQ登入成功", null); }}
微博登入@Componentpublic class WeiBoLoginHandler implements LoginHandler<Serializable> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); /** * 獲取登入型別 * * @return */ @Override public LoginType getLoginType() { return LoginType.WEI_BO; } /** * 登入 * * @param request * @return */ @Override public LoginResponse<String, Serializable> handleLogin(LoginRequest request) { logger.info("微博登入:userId:{}", request.getUserId()); return LoginResponse.success("微博登入成功", null); }}
建立策略的簡單工廠@Componentpublic class LoginHandlerFactory implements InitializingBean, ApplicationContextAware { private static final Map<LoginType, LoginHandler<Serializable>> LOGIN_HANDLER_MAP = new EnumMap<>(LoginType.class); private ApplicationContext appContext; /** * 根據登入型別獲取對應的處理器 * * @param loginType 登入型別 * @return 登入型別對應的處理器 */ public LoginHandler<Serializable> getHandler(LoginType loginType) { return LOGIN_HANDLER_MAP.get(loginType); } @Override public void afterPropertiesSet() throws Exception { // 將 Spring 容器中所有的 LoginHandler 註冊到 LOGIN_HANDLER_MAP appContext.getBeansOfType(LoginHandler.class) .values() .forEach(handler -> LOGIN_HANDLER_MAP.put(handler.getLoginType(), handler)); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { appContext = applicationContext; }}
我們讓 LoginHandlerFactory實現 InitializingBean 介面,在 afterPropertiesSet 方法中,基於 Spring 容器將所有 LoginHandler 自動註冊到 LOGIN_HANDLER_MAP,從而 Spring 容器啟動完成後, getHandler 方法可以直接透過 loginType 來獲取對應的登入處理器。
建立登入服務在登入服務中,我們透過 LoginHandlerFactory 來獲取對應的登入處理器,從而處理不同型別的第三方登入:
@Servicepublic class LoginServiceImpl implements LoginService { @Autowired private LoginHandlerFactory loginHandlerFactory; @Override public LoginResponse<String, Serializable> login(LoginRequest request) { LoginType loginType = request.getLoginType(); // 根據 loginType 找到對應的登入處理器 LoginHandler<Serializable> loginHandler = loginHandlerFactory.getHandler(loginType); // 處理登入 return loginHandler.handleLogin(request); }}
Factory 只負責獲取 Handler,Handler 只負責處理具體的登入,Service 只負責邏輯編排,從而達到功能上的低耦合高內聚。
測試寫一個 Controller:
@RestControllerpublic class LoginController { @Autowired private LoginService loginService; /** * 登入 */ @PostMapping("/login") public LoginResponse<String, Serializable> login(@RequestParam LoginType loginType, @RequestParam Long userId) { LoginRequest loginRequest = new LoginRequest(); loginRequest.setLoginType(loginType); loginRequest.setUserId(userId); return loginService.login(loginRequest); }}
然後用 Postman 測下下:
QQ登入
是不是很簡單呢?如果需求又要加需求,需要支援 GitHub 第三方登入。
此時我們只需要新增一個新的策略實現,然後在登入列舉中加入對應的型別即可:
@Componentpublic class GitHubLoginHandler implements LoginHandler<Serializable> { private final Logger logger = LoggerFactory.getLogger(this.getClass()); /** * 獲取登入型別 * * @return */ @Override public LoginType getLoginType() { return LoginType.GIT_HUB; } /** * 登入 * * @param request * @return */ @Override public LoginResponse<String, Serializable> handleLogin(LoginRequest request) { logger.info("GitHub登入:userId:{}", request.getUserId()); return LoginResponse.success("GitHub登入成功", null); }}
此時不需要修改任何程式碼 ,因為 Spring 容器重啟時會自動將 GitHubLoginHandler註冊到 LoginHandlerFactory 中,使用 Spring 實現策略模式就是這麼簡單,還不快學起來!