文章目录
- 1、目前登录接口
- 2、问题分析
- 3、解决方案
- 4、使用场景
 
现在有一个场景,就是需要做登录,但是我们不确定现在移动端的同学做几端,可能会有 手机号登录、账号密码登录、qq登录、微信登录、PC端登录、网页端登录。这些登录的具体实现肯定是不一样的,而且我们并不知道到底要做几种策略,并且之后一定会有所拓展
举个栗子:
下图是gitee的登录的入口,其中有多种方式可以进行登录
-  用户名密码登录 
-  短信验证码登录 
-  微信登录 
-  QQ登录 
-  … 

我们来分析一下
1、目前登录接口
| 说明 | |
|---|---|
| 请求方式 | POST | 
| 路径 | /api/user/login | 
| 参数 | LoginReq | 
| 返回值 | LoginResp | 
请求参数:LoginReq
@Data
public class LoginReq {
    private String name;
    private String password;
    private String phone;
    private String validateCode;//手机验证码
    private String wxCode;//用于微信登录
    /**
     * account : 用户名密码登录
     * sms : 手机验证码登录
     * we_chat : 微信登录
     */
    private String type;
}
响应参数:LoginResp
@Data
public class LoginResp{
    private Integer userId;
    private String userName;
    private String roleCode;
    private String token; //jwt令牌
    private boolean success;
}
控制层LoginController
@RestController
@RequestMapping("/api/user")
public class LoginController {
    @Autowired
    private UserService userService;
    @PostMapping("/login")
    public LoginResp login(@RequestBody LoginReq loginReq){
        return userService.login(loginReq);
    }
}
业务层UserService
@Service
public class UserService {
    public LoginResp login(LoginReq loginReq){
        if(loginReq.getType().equals("account")){
            System.out.println("用户名密码登录");
            //执行用户密码登录逻辑
            return new LoginResp();
        }else if(loginReq.getType().equals("sms")){
            System.out.println("手机号验证码登录");
            //执行手机号验证码登录逻辑
            return new LoginResp();
        }else if (loginReq.getType().equals("we_chat")){
            System.out.println("微信登录");
            //执行用户微信登录逻辑
            return new LoginResp();
        }
        LoginResp loginResp = new LoginResp();
        loginResp.setSuccess(false);
        System.out.println("登录失败");
        return loginResp;
    }
}
2、问题分析
- 业务层代码大量使用到了if…else,在后期阅读代码的时候会非常不友好,大量使用if…else性能也不高
- 如果业务发生变更,比如现在新增了QQ登录方式,这个时候需要修改业务层代码,违反了开闭原则
解决方案:
所以我们一般会用IOC容器 + 工厂模式 + 策略模式 来解决
3、解决方案
我们不在service中写业务逻辑,让service调用工厂,然后通过service传递不同的参数来获取不同的登录策略(登录方式)

我们看看具体怎么写
首先定义一个策略接口:UserGranter
/**
 * 抽象策略类
 */
public interface UserGranter{
   /**
    * 获取数据
    * @param loginReq 传入的参数
    * @return map值
    */
   LoginResp login(LoginReq loginReq);
}
接着定义具体的策略:AccountGranter、SmsGranter、WeChatGranter
/**
 * 	策略:账号登录
 **/
@Component
public class AccountGranter implements UserGranter{
	@Override
	public LoginResp login(LoginReq loginReq) {
		System.out.println("登录方式为账号登录" + loginReq);
		// TODO
		// 执行业务操作 
		
		return new LoginResp();
	}
}
/**
 * 策略:短信登录
 */
@Component
public class SmsGranter implements UserGranter{
	@Override
	public LoginResp login(LoginReq loginReq)  {
		System.out.println("登录方式为短信登录" + loginReq);
		// TODO
		// 执行业务操作
		return new LoginResp();
	}
}
/**
 * 策略:微信登录
 */
@Component
public class WeChatGranter implements UserGranter{
	@Override
	public LoginResp login(LoginReq loginReq)  {
		System.out.println("登录方式为微信登录" + loginReq);
		// TODO
		// 执行业务操作
		
		return new LoginResp();
	}
}
接下来定义一个工厂类:UserLoginFactory
/**
 * 操作策略的上下文环境类 工具类
 * 将策略整合起来 方便管理
 */
@Component
public class UserLoginFactory implements ApplicationContextAware {
    private static Map<String, UserGranter> granterPool = new ConcurrentHashMap<>();
    @Autowired
    private LoginTypeConfig loginTypeConfig;
    /**
     * 从配置文件中读取策略信息存储到map中
     * {
     * account:accountGranter,
     * sms:smsGranter,
     * we_chat:weChatGranter
     * }
     *
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        loginTypeConfig.getTypes().forEach((k, y) -> {
            granterPool.put(k, (UserGranter) applicationContext.getBean(y));
        });
    }
    /**
     * 对外提供获取具体策略
     *
     * @param grantType 用户的登录方式,需要跟配置文件中匹配
     * @return 具体策略
     */
    public UserGranter getGranter(String grantType) {
        UserGranter tokenGranter = granterPool.get(grantType);
        return tokenGranter;
    }
}
在application.yml文件中新增自定义配置
login:
  types:
    account: accountGranter
    sms: smsGranter
    we_chat: weChatGranter
新增读取数据配置类
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "login")
public class LoginTypeConfig {
    private Map<String,String> types;
}
改造service代码
@Service
public class UserService {
    @Autowired
    private UserLoginFactory factory;
    public LoginResp login(LoginReq loginReq){
        UserGranter granter = factory.getGranter(loginReq.getType());
        if(granter == null){
            LoginResp loginResp = new LoginResp();
            loginResp.setSuccess(false);
            return loginResp;
        }
        LoginResp loginResp = granter.login(loginReq);
        return loginResp;
    }
}
可以看到我们使用了设计模式之后,业务层的代码就清爽多了,如果后期有新的需求改动,比如加入了QQ登录,我们只需要添加对应的策略就可以,无需再改动业务层代码
4、使用场景
其实像这样的需求,在日常开发中非常常见,场景有很多,以下的情景都可以使用工厂模式+策略模式解决比如:
- 订单的支付策略 
  - 支付宝支付
- 微信支付
- 银行卡支付
- 现金支付
 
- 解析不同类型excel 
  - xls格式
- xlsx格式
 
- 打折促销 
  - 满300元9折
- 满500元8折
- 满1000元7折
 
- 物流运费阶梯计算 
  - 5kg以下
- 5kg-10kg
- 10kg-20kg
- 20kg以上
 
一句话总结:只要代码中有冗长的 if-else 或 switch 分支判断都可以采用策略模式优化












![[MMDetection]COCO数据集可视化验证](https://img-blog.csdnimg.cn/img_convert/ed161824548e0b2b45d0271becde6a45.png)





