一、策略模式简介
1.1 简介
策略模式(Strategy),定义了一组算法,将每个算法都封装起来,并且使它们之间可以互换。从而导致客户端程序独立于算法的改变。
UML结构图如下
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
使用场景:一个系统有许多许多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。
关键代码:实现同一个接口。
设计原则:找出应用中需要变化的部分,把他们独立出来,不要和那些不需要变化的代码混合在一起。
1.2 使用场景
使用用场景:
a) 如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
b) 一个系统需要动态地在几种算法中选择一种。
c) 如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
如:
a) 诸葛亮的锦囊妙计,每一个锦囊就是一个策略。
b) 出门选择不同的交通工具。
c) 支付时选择不同的支付通道。
d) JAVA AWT 中的 LayoutManager,选用不同的模板或皮肤进行展示。
- 融山系统使用场景:
a) 鉴权中心根据不同的token类型进行 生成token, 校验token, 获取token参数。
b) 数据门户数据填报, 三大业务线的数据结构基本相同,都需要进行5级菜单联动,数据过滤,操作权限检查等。
c) 合同参数校验服务,不同的合同都要进行通用参数检查和特殊参数检查。
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
二、策略模式如何实现
2.1 通过分离变化得出的策略接口Strategy
public interface TokenStrategy {
String creatToken(BaseToken token);
Boolean checkToken(String token);
Map<String, Object> getParms(String token);
}
2.2 Strategy的实现类
- 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 检验成功", token);
return true;
}
@Override
public Map<String, Object> getParms(String token) {
log.info("adminUserToken 获取参数");
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 校验成功", token);
return true;
}
@Override
public Map<String, Object> getParms(String token) {
log.info("applicationToken 参数获取成功");
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);
}
}
三、策略模式优缺点
3.1 与继承相比(在父类中提供实现方法,子类通过继承获取父类的某种功能)
继承优点:简单易用,已有应用可以快速添加额外功能。
继承缺点:不具备灵活性,对未来变化支持差。
3.2 与抽象方法相比 (在父类中提供抽象方法,强迫子类实现自己的某种功能)
抽象方法优点:足够灵活
缺点:每一个子类都要实现一遍代码,即使是相同的也不例外。
3.3 综合对比
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。
- 继承是重用代码的利器,但是继承并不总是最好的工具。
- Favor composition over inheritance. 复合优先于继承,多用组合,少用继承。
What is Composition?
在类中增加一个私有域,引用另一个已有类的实例,通过调用引用实例的方法从而获得新的功能,这种设计被称作组合(复合)。
四、进一步优化
问题描述:
在选择策略的时候,通常需要根据不同的条件判断对策略进行选择,会产生多个if ......else,使用动态选择执行bean 的方式来选择策略可进一步优化代码
4.1 定义相关的枚举映射
/**
* @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 定义相关的工具类,可以通过策略类的名称从容器中获取相关的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 定义相关的接口并实现相关功能
- 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 = "客户随机输入的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 = "应用所携带的token";
tokenService.checkToken(tokenType01, token01);
String tokenType02 = "adminuser";
String token02 = "用户登陆时所携带的token";
tokenService.checkToken(tokenType02, token02);
}
}
五、Spring 中的应用
Spring 中策略模式使用有多个地方,如 Bean 定义对象的创建以及代理对象的创建等。这里主要看一下代理对象创建的策略模式的实现。
Spring 中策略模式的实现
Spring 的代理方式有两个 Jdk 动态代理和 CGLIB 代理。这两个代理方式的使用正是使用了策略模式。它的结构图如下所示:
Spring 中策略模式结构图
在上面结构图中与标准的策略模式结构稍微有点不同,这里抽象策略是 AopProxy 接口,Cglib2AopProxy 和 JdkDynamicAopProxy 分别代表两种策略的实现方式,ProxyFactoryBean 就是代表 Context 角色,它根据条件选择使用 Jdk 代理方式还是 CGLIB 方式,而另外三个类主要是来负责创建具体策略对象,ProxyFactoryBean 是通过依赖的方法来关联具体策略对象的,它是通过调用策略对象的 getProxy(ClassLoader classLoader) 方法来完成操作。