《设计模式之策略模式》

一、策略模式简介

1.1 简介

策略模式(Strategy),定义了一组算法,将每个算法都封装起来,并且使它们之间可以互换。从而导致客户端程序独立于算法的改变。

UML结构图如下


image.png

主要解决:在有多种算法相似的情况下,使用 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 中策略模式结构图

Spring 中策略模式结构图

在上面结构图中与标准的策略模式结构稍微有点不同,这里抽象策略是 AopProxy 接口,Cglib2AopProxy 和 JdkDynamicAopProxy 分别代表两种策略的实现方式,ProxyFactoryBean 就是代表 Context 角色,它根据条件选择使用 Jdk 代理方式还是 CGLIB 方式,而另外三个类主要是来负责创建具体策略对象,ProxyFactoryBean 是通过依赖的方法来关联具体策略对象的,它是通过调用策略对象的 getProxy(ClassLoader classLoader) 方法来完成操作。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355