拜托,别再用 if-else 了可以吗?

1,175 阅读2分钟

1. 什么是策略模式?

策略模式其实也是在解耦,把策略的定义、创建、使用这三个部分解耦开来,因为本身策略模式也是基于接口编程,这样其实可以简单的理解客户端调用使用接口进行编程,可以通过工厂方法创建对应的策略模式,进而完成对应的程序功能。这么说起来有点拗口,我们直接看程序吧。


public interface Strategy {
  void alg();
}

public class ConcreteStrategyA implements Strategy {
  @Override
  public void  alg() {
    //具体的算法...
  }
}

public class ConcreteStrategyB implements Strategy {
  @Override
  public void  alg() {
    //具体的算法...
  }
}

策略类的定义比较简单,仅仅包含一个策略接口和一组实现这个接口的策略类。这样客户端就可以根据不同的策略灵活的编程了,通常我们是使用工厂方法根据条件创建不同的策略模式进行使用。


public class StrategyFactory {
  private static final Map<String, Strategy> strategies = new HashMap<>();

  static {
    strategies.put("A", new ConcreteStrategyA());
    strategies.put("B", new ConcreteStrategyB());
  }

  public static Strategy getStrategy(String type) {
    return strategies.get(type);
  }
}

这样就完成了初版的策略模式,其实我们只是使用的 map 进行了策略的路由,如果考虑到开放封闭原则,我们的程序还是存在很大的问题,这里留下一个悬念,下文中的真实例子会解开这个问题。

public class Main {
  public static void main(String[] args) {
    //...
    StrategyFactory factory = new StrategyFactory();
    factory.getStrategy("A").alg();
    //...
  }
}

说到这里其实大家已经看明白了策略模式就是通过接口和一组实现类的方式去掉了大量使用 if-else 的逻辑,那么我们接下来针对实际的例子做一下吧,这样更有体感。

2. 实际例子

小编开发的码问(mawen.co)社区目前只支持了 Github 登录,因为 Github 在国外的原因登录起来比较容易超时,于是小编打算增加 Gitee 的登录,这样无论登录还是学习成本都会降低,原来只有 Github 登录的代码逻辑如下,主要逻辑就是通过 Github 的接口获取 Github 用户信息:

AccessTokenDTO accessTokenDTO = new AccessTokenDTO();
String accessToken = githubProvider.getAccessToken(accessTokenDTO);
GithubUser githubUser = githubProvider.getUser(accessToken);
if (githubUser != null && githubUser.getId() != null) {
    //……登录成功逻辑
    return "redirect:/";
} else {
    //……登录失败逻辑
    return "redirect:/";
}

如果简单粗暴的使用 if-else 完成 Gitee 的逻辑接入,那么代码会是如下的样子。

AccessTokenDTO accessTokenDTO = new AccessTokenDTO();
User user;
if(type == "github") {
  String accessToken = githubProvider.getAccessToken(accessTokenDTO);
  user = githubProvider.getUser(accessToken);
} else if(type == "gitee") {
  String accessToken = giteeProvider.getAccessToken(accessTokenDTO);
  user = giteeProvider.getUser(accessToken);
}
if (user != null && user.getId() != null) {
    //……登录成功逻辑
    return "redirect:/";
} else {
    //……登录失败逻辑
    return "redirect:/";
}

那么如果增加 10 个平台呢?代码会越来越多,而且完全违背的开放封闭原则,那么是时候展示策略模式真正的实力了。

首先我们创建一个策略接口和两个策略实现,这里有一个约定,我们平时开发使用了什么设计模式就要以这个设计模式名称结尾,所以具体的改造代码如下。

public interface UserStrategy {
    // 通过 code 和 state 获取对应平台的用户信息
    User getUser(String code,String state);
}

public class GithubUserStrategy implements UserStrategy {
    // 通过 code 和 state 获取对应平台的用户信息
    public User getUser(String code,String state) {
      // ……根据 Github 信息获取用户资料
    }
}
public class GiteeUserStrategy implements UserStrategy {
    // 通过 code 和 state 获取对应平台的用户信息
    public User getUser(String code,String state) {
      // ……根据 Gitee 信息获取用户资料
    }
}

同时我们也创建了一个工厂类,把创建具体的策略的实例的逻辑内聚起来。

public class UserStrategyFactory {
   private static final Map<String, UserStrategy> strategies = new HashMap<>();

  static {
    strategies.put("github", new GithubUserStrategy());
    strategies.put("gitee", new GiteeUserStrategy());
  }

  public static UserStrategy getStrategy(String type) {
    return strategies.get(type);
  }
}

我们重新回到增加 Gitee 的代码逻辑,修改成如下样子,是不是清爽了很多?

//从 url 上下文中获取 type
String type = "";
UserStrategy userStrategy = userStrategyFactory.getStrategy(type);
User user = userStrategy.getUser(code,state);
if (user != null && user.getId() != null) {
    //……登录成功逻辑
    return "redirect:/";
} else {
    //……登录失败逻辑
    return "redirect:/";
}

这时候我们细细品味还是有瑕疵的,因为 UserStrategyFactory 中并没有解决开放封闭原则的根本问题,每次增加新的 UserStrategy 实现类还是需要修改代码的,那么我们继续往前走一步,把具体的判断是否匹配策略的逻辑内聚到策略里面来解决这个问题。

我们给 UserStrategy 增加一个方法,叫做 getSupportedType,用于返回当前策略支持的 type 类型,我们直接用代码说话。

public interface UserStrategy {
    // 通过 code 和 state 获取对应平台的用户信息
    User getUser(String code,String state);
    String getSupportedType();
}

public class GithubUserStrategy implements UserStrategy {
    // 通过 code 和 state 获取对应平台的用户信息
    public User getUser(String code,String state) {
      // ……根据 Github 信息获取用户资料
    }
    
    public String getSupportedType(){
      return "github";
    }
}
public class GiteeUserStrategy implements UserStrategy {
    // 通过 code 和 state 获取对应平台的用户信息
    public User getUser(String code,String state) {
      // ……根据 Gitee 信息获取用户资料
    }
    
    public String getSupportedType(){
      return "gitee";
    }
}

然后简单修改一下工厂类

public class UserStrategyFactory {
   List<UserStrategy> strategies = new ArrayList(){{
     this.add(new GithubUserStrategy());
     this.add(new GiteeUserStrategy());
   }};
   
  public static UserStrategy getStrategy(String type) {
    for (UserStrategy userStrategy : strategies) {
      if(type == userStrategy.getSupportedType()) {
        return userStrategy;
      }
    }
  }
}

这样以后,如果增加了新的策略实现,我们只需要增加到 strategies 里面就可以解决问题了,但是如何动态的增加进去不需要修改代码呢?我们可以使用自定义注解,把所有标记自定义注解的类都加载进来或者使用 Spring 的 @Autowired 也会自动的把所有的子类注入到对应的父类集合里面,那么这样是不是就大功告成了呢?

如果觉得语言有一些晦涩,可以看视频,小编专门录制了一个策略模式的实战视频,关注微信订阅号码匠笔记,回复“策略模式”即可获取观看链接。