从初学者的角度一起谈谈设计模式之策略模式
疑问一:什么是策略模式
先看看大佬们怎末说
策略模式是一种行为型模式,它将对象和行为分开,将行为定义为 一个行为接口 和 具体行为的实现。策略模式最大的特点是行为的变化,行为之间可以相互替换。每个if判断都可以理解为就是一个策略。本模式使得算法可独立于使用它的用户而变化
策略模式是顾名思义就是指对象具有某个行为,但是在不同的业务场景下,这个行为应该有不同的表现形式,也就是有了不同的策略。让对象能再不同的场景下对同一行为有不同的实现,这就是策略模式。
策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数.
我自己浅薄的理解
策略就是,我想达到某件事,但是会面对不同的环境,所以要对每个环境提出针对性的策略, 来达到我的目的。
具体的代码实现的逻辑:
定义一个接口 把所有的策略实现都抽象出来
然后根据不同的业务场景对这个接口进下多实现
这个时候我们就可以用到多态
通过接口提供统一的方法 指向不同的决策实现类
疑问二:策略模式运用场景
先看看大佬们怎末说
登录类型,支付类型,供应商渠道,不同等级会员享受的优惠券价格不一样,等等业务判断,大量if else导致拓展(侧重新增)极其困难,维护(侧重修改)自然是改起来头痛(其实一个类型的增加[拓展一个类型]往往对应这个类型的增删改查CRUD[维护]),比如业务一开始一个简单的登录,往往做一个电话号码和验证码登录的方式或者账号密码登录方式,后来随着业务的增加或者提高用户体验,那么需要拓展(新增一种)三方登录,比如新增微信登录,支付宝登录,甚至抖音登录,一大堆,你要根据登录方式来处理,那就有点恼火吧,支付方式也是同样的问题,我们可以发现一个规律,凡是可以枚举的业务,往往都需要使用设计模式才能更好的解决,比如策略模式(往往搭配工厂模式使用更配哦),水来土掩,兵来将挡,这思想和高中数学中的分类讨论思想一模一样.遇事不要慌,因为还有dang中央.所以,我们就打个栗子,举个比方,更加形象一点
上面的举例很生动我们再业务中可能登录会有很多第三方登录方式和支付类型 这个情况用if else 代码会又臭又长 以后维护起来肯定比较麻烦
想了下 其实在很多项目中好像也又用到策略模式的 只是那时不知道策略模式是啥
实际项目运用一:日志记录
比如之前写的这篇文章
简单描述下
上面文章是实现了一个切面日志记录的功能,
但是有这么一种业务场景,就是可能他入参或者结果是不同的DTO,
而且可能字段属性还不一样,但是你要获取不同DTO指定的不同属性,
显然这个你去切面里面用 if 去判断DTO的类型 然后来获取字段属性,
这样是很糟糕的
所有可以把这样转换的方法 抽象出来写个接口 参数进行泛化
然后根据参数类型进行多实现 每个DTO 建一个转换的实现类
传不同的参数就可以实现不同的转换逻辑
很好的解耦和实现了开闭原则 这也是策略模式的一种表现嘛
实际项目运用二:报表的数据源接口
公司真实项目代码就不贴出来了
业务逻辑:
我们会有很多报表 提供相应的 插入 查询等接口
可以先定义一个抽象类接口 定义相关好方法
然后每个报表的去实现这个接口和方法
这时候我们前端会把数据库配置的 bean名称 传给我们
我们拿到实现类的bean名称就可以获取到bean对象,从而调用对应报表的实现类方法
因为每个报表的数据组装代码里还是挺多的 所以主要的好处是 每张报表的实现都写在一个类里面 而且这里很好的利用了多态
疑问三:具体实现案例
案例来自:
文章:
代码地址:
看代码前需要清楚
之(后置处理器)介绍
代码很简单就是通过实现接口 在对象初始化之前 把类型key和bean对象放到map里面 然后根据传入的类型key获取指定实现类bean对象,最后执行策略方法。
1.层
@Slf4j
@RestController
@RequestMapping("/strategy")
@RequiredArgsConstructor
public class StrategyController extends BaseController {@Resourceprivate BusinessService businessService;@GetMapping("/{key}")public Object doLoginWithOutStrategy(@PathVariable("key") Integer key) {// 这里顺带使用REST 风格接口 没有处理异常的和校验这些,因为这里重点是展示策略模式log.info("未使用策略模式控制层");return businessService.doLoginWithOutStrategy(key);}@GetMapping("/v2/{key}")public Object doLoginWithStrategy(@PathVariable("key") Integer key) {log.info("******使用策略模式控制层******");return businessService.doLoginWithStrategy(key);}
}
2.层
/*** BusinessService:业务服务层*/
public interface BusinessService {/*** 未使用策略模式的登录** @param key 登录类型* @return 登录结果*/Object doLoginWithOutStrategy(Integer key);/*** 使用策略模式登录[请求参数和返回参数自己根据具体业务设计就行,这里只是就请求参数传个枚举key,返回参数返回个Object为例子哈]** @param key 枚举的key* @return 返回值[可以为void, 根据你的业务设计就行]*/Object doLoginWithStrategy(Integer key);
}
/*** BusinessServiceImpl:模拟业务遇到的实现类(控制层controller->service层->dao层)*/
@Service
@Slf4j
@RequiredArgsConstructor
public class BusinessServiceImpl implements BusinessService {@Resourceprivate StrategyHandler strategyhandler;@Overridepublic Object doLoginWithOutStrategy(Integer key) {if (Objects.equals(LoginEnum.ACCOUNT.getKey(), key)) {log.info("用户根据[账号密码登录],执行相关逻辑===>未使用策略模式");return "[账号密码登录]";} else if (Objects.equals(LoginEnum.QQ.getKey(), key)) {log.info("用户根据[QQ登录],执行相关逻辑===>未使用策略模式");return "[QQ登录]";} else if (Objects.equals(LoginEnum.WECHAT.getKey(), key)) {log.info("用户根据[微信登录],执行相关逻辑===>未使用策略模式");return "[微信登录]";} else {log.info("用户根据[默认登录策略],执行相关逻辑===>未使用策略模式");return "[默认登录策略]";}}@Overridepublic Object doLoginWithStrategy(Integer key) {// 满足开闭原则,这里不管新增登录类型key还是修改类型key内部逻辑,这里都不需动,也不需要动它,闭就是禁止在这里修改,维护就很棒了return strategyhandler.handler(key);}
}
3.策略实现类
/*** AccountStrategyImpl:账户登录** @author zhangxiaoxiang* @date 2021/07/18*/
@Slf4j
@Service
public class AccountStrategyImpl implements StrategyService {@Overridepublic Integer matchKey() {return LoginEnum.ACCOUNT.getKey();}@Overridepublic Object handler(Integer key) {log.info("用户根据[账号密码登录],执行相关逻辑");return "[账号密码登录]";}}
/*** DefaultStrategyImpl:默认策略(相当于没有匹配请求参数返回的默认策略),一般策略模式都会带上默认策略,类似if else最后都有个else 或者switch语句的默认分支*/
@Service
@Slf4j
public class DefaultStrategyImpl implements StrategyService {@Overridepublic Integer matchKey() {return LoginEnum.DEFAULT.getKey();}@Overridepublic Object handler(Integer key) {log.info("用户根据[默认登录策略],执行相关逻辑");return "[默认登录策略]";}
}
/*** QQLoginStrategyImpl:QQ登录*/
@Slf4j
@Service
public class QQLoginStrategyImpl implements StrategyService {@Overridepublic Integer matchKey() {return LoginEnum.QQ.getKey();}@Overridepublic Object handler(Integer key) {log.info("用户根据[QQ登录],执行相关逻辑");return "[QQ登录]";}
}
/*** WechatStrategyImpl:微信登录逻辑*/
@Slf4j
@Service
public class WechatStrategyImpl implements StrategyService {@Overridepublic Integer matchKey() {return LoginEnum.WECHAT.getKey();}@Overridepublic Object handler(Integer key) {log.info("用户根据[微信登录],执行相关逻辑");return "[微信登录]";}
}
4.类型枚举类
/*** MyEnum:登录类型枚举[其它业务 比如支付类型,物流类型,运营商类型类比即可]*/
@AllArgsConstructor
public enum LoginEnum {/*** 账号*/ACCOUNT(1, "账号登录"),/*** QQ登录*/QQ(2, "QQ登录"),/*** 微信登录*/WECHAT(3, "微信登录"),/*** 暂不支持登录方式*/DEFAULT(0, "暂不支持登录方式");/*** 标识key*/@Getter@Setterprivate Integer key;/*** 标识value值*/@Getter@Setterprivate String value;
}
4.核心策略处理类
/*** StrategyHandler:策略处理类[可以理解为策略工厂类],Map也可以使用private static final 修饰*/
@Component
@Slf4j
public class StrategyHandler implements BeanPostProcessor {Map<Integer, StrategyService> handlers = new ConcurrentHashMap<>();public Object handler(Integer key) {// 如果没有找到正常业务策略实现类处理方式1:写一个默认策略,比如HANDLERS.get(0)表示找到默认策略[兜底保留策略]Object result = Optional.ofNullable(handlers.get(key)).orElse(handlers.get(LoginEnum.DEFAULT.getKey())).handler(key);// 如果没有找到正常业务策略实现类处理方式2:直接抛业务异常,调用方处理,两种方式根据业务情况自行选择就行// Object result = Optional.ofNullable(HANDLERS.get(type)).orElseThrow(() -> new RuntimeException("未找到定义的策略实现类[抛出的业务异常]")).handler(o);log.info("从容器获取到:key={},对应的StrategyService处理结果:{}", key, result);return result;}@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {if (bean instanceof StrategyService) {StrategyService service = (StrategyService) bean;handlers.put(service.matchKey(), service);}return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);}
}
5.核心策略处理抽象接口
/*** 登录策略*/
public interface StrategyService {/*** 需要返回枚举的key,用于处理注册对应逻辑** @return 枚举的key*/Integer matchKey();/*** 执行具体策略,这里写业务代码[拓展一种登录模式,只需要新增一个一个枚举值和对应的实现类就行]** @param key 具有逻辑请求需要的参数* @return 具体逻辑执行完毕返回的参数*/Object handler(Integer key);
}