策略模式
策略模式是很常见的一种设计模式,将具体的算法封装起来,可以平等替换,避免客户端过多的涉及业务逻辑。网上有很多讲策略模式的文章,都很好。但也有一些文章强行将策略模式、状态模式和消除if-else联系起来,这是不对的。策略模式只是封装了各自算法逻辑,使其可以自由替换,如何选择仍然需要客户端来决定。
举个简单的会员充值的例子,初级会员充值100得100,中级会员充值100得150,高级会员充值100得200。
public interface IMember {
/**充值*/
public void charge(double money);
}
@Slf4j
public class PrimitiveMember implements IMember {
@Override
public void charge(double money) {
log.info("初级会员充值不享受优惠,充值{}",money);
}
}
@Slf4j
public class MiddleMember implements IMember{
@Override
public void charge(double money) {
log.info("高级会员充值享受1.5倍,充值{}",money * 1.5);
}
}
@Slf4j
public class HighMember implements IMember {
@Override
public void charge(double money) {
log.info("高级会员充值享受2倍,充值{}",money * 2);
}
}
我们将不同的充值策略进行了封装,通过接口开放出来。为了简单起见,策略的上下文类就省略了,直接在客户端类调用
public class Client {
public static void main(String[] args) {
IMember member = new HighMember();
member.charge(100);
}
}
这是一个比较经典的策略模式的例子,网上大部分的例子都是这样及其变种,号称解决了if-else问题。其实在客户端生成策略的时候,这里已经定死了是高级会员,高级会员如何得来的呢?肯定还是需要if-else作出选择的,例如:
public class Client {
public static void main(String[] args) {
IMember member = null;
double store = 1000; // 储值
double money = 100;
if(store < 500){// 低于500,是初级会员
member = new PrimitiveMember();
}else if(store < 1000){// 500~1000中级会员
member = new MiddleMember();
}else if(store >= 1000){// 1000以上高级会员
member = new HighMember();
}
member.charge(money);
}
}
有的文章然后用工厂模式优化了一下策略获取:
public class MemberFactory {
public IMember getMember(double store){
IMember member = null;
if(store < 500){
member = new PrimitiveMember();
}else if(store < 1000){
member = new MiddleMember();
}else if(store >= 1000){
member = new HighMember();
}
return member;
}
}
public class Client {
public static void main(String[] args) {
IMember member = null;
double store = 1000;
double money = 100;
member = new MemberFactory().getMember(store); // 通过工厂类获取具体的策略
member.charge(money);
}
}
这样看起来客户端代码似乎消除了if-else,但实际上它并没有消失,只是转移到了工厂类里面。策略模式天生没有选择算法的功能,它只是封装了算法而已。因此if-else仍然需要想办法解决,这里我们可以通过注解来实现。
消除if-else
通过施加在策略类的上注解,我们可以很轻松的区别不同的策略,这样要采用怎样的策略类,就可以通过操作注解来实现。例如,我们加密算法有BASE64,MD5和RSA三种不同的加密策略。
/**
* 业务策略接口
* Created by gameloft9 on 2019/8/5.
*/
public interface ISigner{
/**
* 计算签名
* */
String doSign(String input);
}
@Slf4j
@StrategyComponent // 通过该注解标记该类是策略类
@SignType(type = SignTypeName.BASE64)
@Component
public class BASE64Signer implements ISigner {
public String doSign(String input) {
log.info("进行BASE64加密");
return "";
}
}
@Slf4j
@StrategyComponent
@SignType(type = SignTypeName.MD5)
@Component
public class MD5Signer implements ISigner {
public String doSign(String input) {
log.info("进行MD5加密");
return "";
}
}
@Slf4j
@StrategyComponent
@SignType(type = SignTypeName.RSA)
@Component
public class RSASigner implements ISigner {
public String doSign(String input) {
log.info("进行RSA加密");
return "";
}
}
我们通过SignType注解来区分不同的加密算法,SignType注解代码如下:
/**
* 1、策略分类注解,根据它区分不同的策略实现类,他是实现if-else消除的关键
* 2、如果策略不稳定,数目较多,可能变化。那么建议将策略的定义配置放入数据字典,apllo或者配置文件中
* 然后注解仅提供一个key值,在工厂类的accept方法里读取数据字典,再执行选择逻辑。
* Created by gameloft9 on 2019/8/5.
*/
@Target(ElementType.TYPE)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface SignType {
public SignTypeName type();
}
/**
* 签名类型枚举
* Created by gameloft9 on 2019/8/5.
*/
public enum SignTypeName {
BASE64("BASE64"),
MD5("MD5"),
RSA("RSA");
private String name;
private SignTypeName(String name){
this.name = name;
}
}
注解的定义需要根据业务来定制,例如这里只需要提供签名类型名称即可。
@StrategyComponent注解表明该类是一个策略实现类,后面会扫描所有有该注解的类,并放入策略类的集合中。这样获取具体策略的时候,就扫描策略类集合,根据注解判断是否是符合条件的策略类,如果符合则返回该类的一个实例。策略的获取通过工厂类实现:
/**
* 具体的业务策略工厂类
* 1、添加@StrategyScan注解指定策略扫描包路径
* 2、注入业务分类的策略注解class
* 3、指定从何处获取策略类实例,spring容器或者反射实例化。默认反射实例化。
* 3、实现具体策略的匹配逻辑
* Created by gameloft9 on 2019/8/5.
*/
// 需要通过StrategyScan指定扫描策略包路径,包含子目录
@StrategyScan(value = "com.gameloft9.demo.sign")
@Component
public class SignerStrategyFactory extends StrategyFactory {
@Override
public Class<?> strategyAnnotationClass(){
return SignType.class;// 注入一个策略分类注解
}
@Override
public boolean getStrategyFromSpringContext(){
return true; // 从spring context中获取策略实例bean
}
@Override
public boolean accept(Annotation annotation,Object... params) {
String type = params[0].toString();
SignType signType = (SignType)annotation;
return StringUtils.equals(signType.type().name(),type); // 匹配策略
}
}
策略工厂类首先需要通过@StrategyScan(value = “com.gameloft9.demo.sign”)注解指定具体策略类的包位置,这样工厂类会自动扫描该包,并获取所有的策略类。
具体的策略注解是根据业务变化的,因此需要向工厂类提供注解的类型。
如果具体策略类是spring容器托管的,那么需要告知工厂类从spring容器中获取策略实例。
具体的选择逻辑通过accept方法实现,annotation是该策略类上的策略注解,params是传入参数。如果匹配则返回true,那么就会获取该策略类的实例。
下面我们来测试下:
/**
* 测试代码,通过简单的反射获取实例
* Created by gameloft9 on 2019/8/5.
*/
public class TestSiger {
public static void main(String[] args) {
String input = "GAMELOFT9";
String type = "RSA";
ISigner signer = (ISigner) new SignerStrategyFactory().createStrategy(type);
signer.doSign(input);
}
}
下面测试通过spring容器获取实例:
/**
* 测试代码,通过spring容器获取策略实例
* Created by gameloft9 on 2018/8/7.
*/
@Controller
@EnableAutoConfiguration
@Slf4j
@ComponentScan(basePackages="com.gameloft9.demo")
public class TestStrategy {
public static void main(String[] args) throws Exception {
SpringApplication.run(TestStrategy.class, args);
String input = "GAMELOFT9";
String type = "RSA";
ISigner signer = (ISigner) ContextUtil.getBean(SignerStrategyFactory.class).createStrategy(type);
signer.doSign(input);
}
}
这样我们就真正的消除了if-else,如果我们将策略类的定义存入配置文件、apollo配置中或者数据库中,我们还可以做到策略类的定义可配置化。例如:
@Override
public boolean accept(Annotation annotation,Object... params) {
String type = params[0].toString();
SignType signType = (SignType)annotation;
// 这里添加配置的获取逻辑,从数据库、配置文件或者apollo中获取策略类的定义
// 此时策略注解只需提供一个key即可。
......
return StringUtils.equals(signType.type().name(),type); // 匹配策略
}
后记
消除if-else,方法有不少,下面总结三种:
1-先判断不符合,然后直接返回,这样可以少嵌套一层if。例如:
if(条件满足){
if(条件满足){
}else{
}
}
// 条件不满足提前返回
if(条件不满足){
return ;
}
if(条件满足){
}else{
}
2-如果if内部的逻辑比较复杂,可以抽取成方法出来。例如:
if(){
if(){}
if(){
if(){
}else{
}
}
}
// 转换成如下调用方式
if(){
method()
}
// 将内部调用抽取成方法
public void method(){
if(){}
if(){
if(){
}else{
}
}
}
3-如果业务层面上有很明显的if-else区分,那么可以考虑策略模式+注解,或者状态模式+注解的方式,例如本文所讲的方法。
最后放上工程地址,喜欢的麻烦点个star~
策略模式消除if-else