一尉桩、策略模式簡介
1.1 簡介
策略模式(Strategy)蜘犁,定義了一組算法屈扎,將每個算法都封裝起來墨叛,并且使它們之間可以互換漠趁。從而導(dǎo)致客戶端程序獨(dú)立于算法的改變甥绿。
UML結(jié)構(gòu)圖如下
主要解決:在有多種算法相似的情況下妹窖,使用 if...else 所帶來的復(fù)雜和難以維護(hù)蜓萄。
使用場景:一個系統(tǒng)有許多許多類绸硕,而區(qū)分它們的只是他們直接的行為出嘹。
如何解決:將這些算法封裝成一個一個的類,任意地替換。
關(guān)鍵代碼:實(shí)現(xiàn)同一個接口。
設(shè)計(jì)原則:找出應(yīng)用中需要變化的部分游桩,把他們獨(dú)立出來雀彼,不要和那些不需要變化的代碼混合在一起。
1.2 使用場景
使用用場景:
a) 如果在一個系統(tǒng)里面有許多類,它們之間的區(qū)別僅在于它們的行為,那么使用策略模式可以動態(tài)地讓一個對象在許多行為中選擇一種行為。
b) 一個系統(tǒng)需要動態(tài)地在幾種算法中選擇一種扰肌。
c) 如果一個對象有很多的行為,如果不用恰當(dāng)?shù)哪J轿保@些行為就只好使用多重的條件選擇語句來實(shí)現(xiàn)狡耻。
如:
a) 諸葛亮的錦囊妙計(jì),每一個錦囊就是一個策略猴凹。
b) 出門選擇不同的交通工具夷狰。
c) 支付時選擇不同的支付通道。
d) JAVA AWT 中的 LayoutManager郊霎,選用不同的模板或皮膚進(jìn)行展示沼头。
- 融山系統(tǒng)使用場景:
a) 鑒權(quán)中心根據(jù)不同的token類型進(jìn)行 生成token, 校驗(yàn)token, 獲取token參數(shù)。
b) 數(shù)據(jù)門戶數(shù)據(jù)填報, 三大業(yè)務(wù)線的數(shù)據(jù)結(jié)構(gòu)基本相同,都需要進(jìn)行5級菜單聯(lián)動进倍,數(shù)據(jù)過濾土至,操作權(quán)限檢查等。
c) 合同參數(shù)校驗(yàn)服務(wù)猾昆,不同的合同都要進(jìn)行通用參數(shù)檢查和特殊參數(shù)檢查陶因。
注意事項(xiàng):如果一個系統(tǒng)的策略多于四個,就需要考慮使用混合模式垂蜗,解決策略類膨脹的問題楷扬。
二、策略模式如何實(shí)現(xiàn)
2.1 通過分離變化得出的策略接口Strategy
public interface TokenStrategy {
String creatToken(BaseToken token);
Boolean checkToken(String token);
Map<String, Object> getParms(String token);
}
2.2 Strategy的實(shí)現(xiàn)類
- OperationAdminUserToken
@Slf4j
@Component
public class OperationAdminUserToken implements TokenStrategy {
@Override
public String creatToken(BaseToken token) {
log.info("adminUserToken生成成功");
return "adminUserToken 生成成功";
}
@Override
public Boolean checkToken(String token) {
log.info("token ={}, adminUserToken 檢驗(yàn)成功", token);
return true;
}
@Override
public Map<String, Object> getParms(String token) {
log.info("adminUserToken 獲取參數(shù)");
HashMap<String , Object> hashMap = new HashMap<>();
hashMap.put("tokenType", "adminUserToken");
return hashMap;
}
}
- OperationApplicationToken
@Slf4j
@Component
public class OperationApplicationToken implements TokenStrategy{
@Override
public String creatToken(BaseToken token) {
log.info("applicationToken生成成功");
return "applicationToken 生成成功";
}
@Override
public Boolean checkToken(String token) {
log.info("token ={}, applicationToken 校驗(yàn)成功", token);
return true;
}
@Override
public Map<String, Object> getParms(String token) {
log.info("applicationToken 參數(shù)獲取成功");
HashMap<String , Object> hashMap = new HashMap<>();
hashMap.put("tokenType", "application token");
return hashMap;
}
}
2.3 定義一個封裝了策略的Context
public class TokenContext {
private TokenStrategy tokenStrategy;
public TokenContext(TokenStrategy tokenStrategy) {
this.tokenStrategy = tokenStrategy;
}
public String creatToken(BaseToken token) {
return tokenStrategy.creatToken(token);
}
;
public Boolean checkToken(String token) {
return tokenStrategy.checkToken(token);
}
;
public Map<String, Object> getParms(String token) {
return tokenStrategy.getParms(token);
}
;
}
2.4 在客戶程序中選擇
@Slf4j
public class MainTest {
public static void main(String[] args) {
TokenContext tokenContext = new TokenContext(new OperationApplicationToken());
ApplicationToken applicationToken = new ApplicationToken();
applicationToken.setApplicationCode("fyfq");
applicationToken.setApplicationIp("0.0.0.1");
applicationToken.setEnv("test");
applicationToken.setTokenId(UUID.randomUUID().toString());
tokenContext.creatToken(applicationToken);
tokenContext.checkToken("applicationTokn.....");
Map<String, Object> hashMap = tokenContext.getParms("applicationTokn.....");
log.info("hashMap ={}", hashMap);
tokenContext = new TokenContext(new OperationAdminUserToken());
AdminUserToken adminUserToken = new AdminUserToken();
adminUserToken.setAdminUserId("123456");
adminUserToken.setVersion("v1");
adminUserToken.setEnv("test");
adminUserToken.setTokenId(UUID.randomUUID().toString());
tokenContext.creatToken(applicationToken);
tokenContext.checkToken("adminUserToken.....");
hashMap = tokenContext.getParms("adminUserToken.....");
log.info("hashMap ={}", hashMap);
tokenContext = new TokenContext(new OperationCustomerToken());
CustomerToken customerToken = new CustomerToken();
customerToken.setCustomerId("customerId");
customerToken.setNewTokenIntervalHours(9);
customerToken.setTokenRefreshIntervalHours(10);
customerToken.setCustomerId("123");
customerToken.setEnv("prd");
customerToken.setTokenId(UUID.randomUUID().toString());
tokenContext.creatToken(applicationToken);
tokenContext.checkToken("customerUserToken.....");
hashMap = tokenContext.getParms("customerUserToken.....");
log.info("hashMap ={}", hashMap);
tokenContext = new TokenContext(new OperationOtherToken());
OtherToken otherToken = new OtherToken();
otherToken.setInfo("other token ");
otherToken.setEnv("prd");
otherToken.setTokenId(UUID.randomUUID().toString());
tokenContext.creatToken(applicationToken);
tokenContext.checkToken("other token.....");
hashMap = tokenContext.getParms("other token.....");
log.info("hashMap ={}", hashMap);
}
}
三贴见、策略模式優(yōu)缺點(diǎn)
3.1 與繼承相比(在父類中提供實(shí)現(xiàn)方法烘苹,子類通過繼承獲取父類的某種功能)
繼承優(yōu)點(diǎn):簡單易用,已有應(yīng)用可以快速添加額外功能片部。
繼承缺點(diǎn):不具備靈活性镣衡,對未來變化支持差。
3.2 與抽象方法相比 (在父類中提供抽象方法档悠,強(qiáng)迫子類實(shí)現(xiàn)自己的某種功能)
抽象方法優(yōu)點(diǎn):足夠靈活
缺點(diǎn):每一個子類都要實(shí)現(xiàn)一遍代碼廊鸥,即使是相同的也不例外。
3.3 綜合對比
優(yōu)點(diǎn): 1站粟、算法可以自由切換黍图。 2、避免使用多重條件判斷奴烙。 3、擴(kuò)展性良好剖张。
缺點(diǎn): 1切诀、策略類會增多。 2搔弄、所有策略類都需要對外暴露幅虑。
- 繼承是重用代碼的利器,但是繼承并不總是最好的工具顾犹。
- Favor composition over inheritance. 復(fù)合優(yōu)先于繼承倒庵,多用組合,少用繼承炫刷。
What is Composition?
在類中增加一個私有域擎宝,引用另一個已有類的實(shí)例,通過調(diào)用引用實(shí)例的方法從而獲得新的功能浑玛,這種設(shè)計(jì)被稱作組合(復(fù)合)绍申。
四、進(jìn)一步優(yōu)化
問題描述:
在選擇策略的時候,通常需要根據(jù)不同的條件判斷對策略進(jìn)行選擇极阅,會產(chǎn)生多個if ......else胃碾,使用動態(tài)選擇執(zhí)行bean 的方式來選擇策略可進(jìn)一步優(yōu)化代碼
4.1 定義相關(guān)的枚舉映射
/**
* @author: lipei
* @Date:2020/7/2 2:43 下午
*/
public enum StrategyClassEnum {
APPLICATION_TOKEN("app", OperationApplicationToken.class),
ADMIN_USER_TOKEN("adminuser", OperationAdminUserToken.class),
CUSTOMER_TOKEN("customer", OperationCustomerToken.class);
private String tokenType;
private Class clazz;
StrategyClassEnum(String tokenType, Class clazz) {
this.tokenType = tokenType;
this.clazz = clazz;
}
public String getTokenType() {
return tokenType;
}
public Class getClazz() {
return clazz;
}
public static Class getClazz(String tokenType) {
for (StrategyClassEnum ele : values()) {
if(ele.getTokenType().equals(tokenType)) return ele.getClazz();
}
return null;
}
}
4.2 定義相關(guān)的工具類,可以通過策略類的名稱從容器中獲取相關(guān)的bean
@Component
@Slf4j
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
//獲取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通過name獲取 Bean.
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
//通過class獲取Bean.
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
//通過name,以及Clazz返回指定的Bean
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
//通過name,以及Clazz返回指定的Bean
public static String[] getAllBens( ){
return getApplicationContext().getBeanDefinitionNames();
}
}
4.3 定義相關(guān)的接口并實(shí)現(xiàn)相關(guān)功能
- IStrategyTokenService
public interface IStrategyTokenService {
/**
* 檢查token是否合法
* @param token
* @return
*/
Boolean checkToken(String tokenType, String token);
}
- StrategyTokenServiceImpl
@Slf4j
@Component
public class StrategyTokenServiceImpl implements IStrategyTokenService {
@Override
public Boolean checkToken( String tokenType, String token) {
Class strategyClass = StrategyClassEnum.getClazz(tokenType);
log.info("strategyClass ={}", strategyClass);
TokenContext tokenContext = null;
try {
tokenContext = new TokenContext((TokenStrategy)strategyClass.newInstance());
return tokenContext.checkToken(token);
} catch (Exception e) {
log.error("check token error , e={}", e);
return false;
}
}
}
4.4 使用
@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class GofStrategyTests {
@Autowired
IStrategyTokenService tokenService;
@Test
public void test01() {
TestImplBeans testImplBeans = SpringUtil.getBean(TestImplBeans.class);
testImplBeans.print();
}
@Test
public void test02() {
String tokenType = "app";
String token = "客戶隨機(jī)輸入的token";
Class strategyClass = StrategyClassEnum.getClazz(tokenType);
log.info("strategyClass ={}", strategyClass);
TokenContext tokenContext = null;
try {
tokenContext = new TokenContext((TokenStrategy)strategyClass.newInstance());
ApplicationToken applicationToken = new ApplicationToken();
applicationToken.setApplicationCode("fyfq");
applicationToken.setApplicationIp("0.0.0.1");
applicationToken.setEnv("test");
applicationToken.setTokenId(UUID.randomUUID().toString());
tokenContext.creatToken(applicationToken);
tokenContext.checkToken("applicationTokn.....");
Map<String, Object> hashMap = tokenContext.getParms("applicationTokn.....");
log.info("hashMap ={}", hashMap);
} catch (Exception e) {
e.printStackTrace();
}
}
@Test
public void test03(){
String tokenType01 = "app";
String token01 = "應(yīng)用所攜帶的token";
tokenService.checkToken(tokenType01, token01);
String tokenType02 = "adminuser";
String token02 = "用戶登陸時所攜帶的token";
tokenService.checkToken(tokenType02, token02);
}
}
五筋搏、Spring 中的應(yīng)用
Spring 中策略模式使用有多個地方仆百,如 Bean 定義對象的創(chuàng)建以及代理對象的創(chuàng)建等。這里主要看一下代理對象創(chuàng)建的策略模式的實(shí)現(xiàn)奔脐。
Spring 中策略模式的實(shí)現(xiàn)
Spring 的代理方式有兩個 Jdk 動態(tài)代理和 CGLIB 代理俄周。這兩個代理方式的使用正是使用了策略模式。它的結(jié)構(gòu)圖如下所示:
Spring 中策略模式結(jié)構(gòu)圖
在上面結(jié)構(gòu)圖中與標(biāo)準(zhǔn)的策略模式結(jié)構(gòu)稍微有點(diǎn)不同帖族,這里抽象策略是 AopProxy 接口栈源,Cglib2AopProxy 和 JdkDynamicAopProxy 分別代表兩種策略的實(shí)現(xiàn)方式,ProxyFactoryBean 就是代表 Context 角色竖般,它根據(jù)條件選擇使用 Jdk 代理方式還是 CGLIB 方式甚垦,而另外三個類主要是來負(fù)責(zé)創(chuàng)建具體策略對象,ProxyFactoryBean 是通過依賴的方法來關(guān)聯(lián)具體策略對象的涣雕,它是通過調(diào)用策略對象的 getProxy(ClassLoader classLoader) 方法來完成操作艰亮。