设计模式之策略模式

源码

java程序的设计原则

6大原则:

单一职责:一个类和方法只做一件事。
开闭原则:对修改关闭,对扩展开发。
里氏替换原则:任意替换子类行为不变,这里的行为是,如某些哪种情况下抛出哪种异常,返回的参数值等。
依赖倒置:依赖于抽象,而非具体实现,即面向接口编程(如方法参数,类属性使用接口声明,这样可接收任何子类)。
接口隔离:使用多个隔离的接口定义抽象,降低耦合。
最少知道/迪米特原则:降低类之间的依赖,聚合,组合等。

1:策略设计模式

策略设计模式一般使用的场景是,多种可互相替代的同类行为,在具体的运行过程中根据不同的情况,选择其中一种行为来执行,比如支付,有微信支付,支付宝支付,银行卡支付,那么到底使用哪种支付方式,这是由用户来决定的,再比如购物优惠,用户可以选择使用优惠券,可以选择满减优惠,以及其他优惠方式,到底是使用优惠券,还是满减,或者其他优惠方式,还是由用户来决定,类似的场景我们都可以考虑使用策略设计模式,可能对于类似的场景我们最常用的还是ifelse,ifelse的缺点是缺少扩展性,从6大原则来说不符合开闭原则,下面我们通过一个实际的场景来看下策略设计模式如何使用。

策略模式的UML图如下:

在这里插入图片描述

1.1:场景

在这里插入图片描述

在我们购物时经常会有如图中的优惠活动:

直减:比如在双十一等时间,商家会选择这种方式进行促销,如原价999的商品,直接降价到899。
满减:一般以券的方式发放给用户,当用户消费金额达到一定数目时,直接使用券来抵扣一定额度的现在,如图中“满2000减1000”,总价2999,用券后需要2899。
折扣:商家直接打折出售商品,如原价1000元的商品,直接八折出售,实际价格为800元。
N元购:另外一种营销手段,比如1元购1999元的手机,但是并非百分百可购得,而是有一定概率,类似于买彩票。

了解了以上的几种优惠活动,

下面我们先来看下通过常规的if-else如何实现,具体参考1.2:if-else实现

1.2:if-else实现

示例代码如下,用于计算用户实际需要支付的金额(仅仅示例,并没有提供真正实现)

class FakeCls {
    // type: 优惠方式
    // 1:直减 2:满减:3:折扣:4:N元购
    double needPayAmount(int type, String otherParams) {
        if (type == 1) {
            // 直减相关逻辑
        } else if (type == 2) {
            // 满减相关逻辑
        } else if (type == 3) {
            // 折扣相关逻辑
        } else if (type == 4) {
            // N元购相关逻辑
        }
    }   
}

以上的代码,很明显不符合6大涉及原则中的单一职责开闭原则(注意并不是默认其他原则都符合),这样写代码扩展性很弱,修改代码的成本高,对现有功能的影响大(说不定你一行代码的修改,老功能都不好用了,你说是让测试测还是不测,不测吧,很明显被影响了,测吧,又会增加人家的工作量),接下来我们看下如何使用策略设计模式来优化代码。

1.3:策略设计模式实现

首先根据依赖倒置原则,我们需要一个接口,如下:

public interface PayAmountStrategy {
    double payAmount(Object param);
}

再根据单一职责原则,我们分别为每种优惠策略提供一个具体实现类。

  • 直减
// 直减
public class DecreasePayAmountStrategy implements PayAmountStrategy {
    @Override
    public double payAmount(Object param) {
        System.out.println("使用直减方式支付");
        return 0;
    }
}
  • 满减
// 满减
public class CouponPayAmountStrategy implements PayAmountStrategy {
    @Override
    public double payAmount(Object param) {
        System.out.println("使用满减支付");
        return 0;
    }
}
  • 折扣
// 折扣
public class DiscountPayAmountStrategy implements PayAmountStrategy {
    @Override
    public double payAmount(Object param) {
        System.out.println("使用折扣方式支付");
        return 0;
    }
}
  • N元购
// N元购
public class NYuanPayAmountStrategy implements PayAmountStrategy {

    @Override
    public double payAmount(Object param) {
        System.out.println("使用N元购支付");
        return 0;
    }
}

接下来定义Context类:

public class PayAmountContext {
    // 依赖倒置原则,面向接口编程
    // 客户端需要设置自己想要使用的具体策略类,因此需要客户端对策略类有具体的了解,这点也是策略设计模式的不足之处
    private PayAmountStrategy payAmountStrategy;

    public PayAmountContext(/*PayAmount payAmount*/) {
        /*this.payAmount = payAmount;*/
    }

    public void setPayAmount(PayAmountStrategy payAmountStrategy) {
        this.payAmountStrategy = payAmountStrategy;
    }

    public double payAmount(Object param) {
        return this.payAmountStrategy.payAmount(param);
    }
}

此时将我们的类映射到策略设计模式UML图如下:

在这里插入图片描述

  • 测试
public class StrategyClient {
    public static void main(String[] args) {
        PayAmountContext payAmountContext = new PayAmountContext();
        // 使用直减策略
        payAmountContext.setPayAmount(new DecreasePayAmountStrategy());
        payAmountContext.payAmount(null);
        // 使用N元购策略
        payAmountContext.setPayAmount(new NYuanPayAmountStrategy());
        payAmountContext.payAmount(null);
        // 使用满减策略
        payAmountContext.setPayAmount(new CouponPayAmountStrategy());
        payAmountContext.payAmount(null);
        // 使用折扣策略
        payAmountContext.setPayAmount(new DiscountPayAmountStrategy());
        payAmountContext.payAmount(null);
    }
}

运行:

使用直减方式支付
使用N元购支付
使用满减支付
使用折扣方式支付

在实际工作中,不一定非得按照这种方式来做,也可以根据实际的情况进行变化和调整,但是是不变的。另外我们看到具体策略类还需要交给客户端来实例化,如果具体实例也能够做到对客户端透明就更好了,接下来我们一起看下如何实现。

1.4:基于注解和约定标记优化

在实际开发过程中,对于不同的支付方式,我们肯定都是会和客户端来约定标记来标示当前用户选择的是哪种支付方式的,那么就可以在标记上来做文章,做法具体是,首先定义一个注解,然后将该注解定义在具体的实现类上来和前端的标记对应起来,这样就能知道哪种支付方式方式对应的具体实现类是哪个了,因此我们先来定义这个注解,如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Key {
    String payType();
}

假定约定的标记和对应的支付方式如下:

满减 -> couponPay
直减 -> decreasePay
折扣 -> discountPay
N元购 -> nYuanPay

然后将注解是用在具体实现类上,修改如下:

// 满减
@Key(payType = "couponPay")
public class CouponPayAmountStrategy implements PayAmountStrategy {
    @Override
    public double payAmount(Object param) {
        System.out.println("使用满减支付V2");
        return 0;
    }
}
// 直减
@Key(payType = "decreasePay")
public class DecreasePayAmountStrategy implements PayAmountStrategy {
    @Override
    public double payAmount(Object param) {
        System.out.println("使用直减方式支付V2");
        return 0;
    }
}
// 折扣
@Key(payType = "discountPay")
public class DiscountPayAmountStrategy implements PayAmountStrategy {
    @Override
    public double payAmount(Object param) {
        System.out.println("使用折扣方式支付V2");
        return 0;
    }
}
// N元购
@Key(payType = "nYuanPay")
public class NYuanPayAmountStrategy implements PayAmountStrategy {

    @Override
    public double payAmount(Object param) {
        System.out.println("使用N元购支付V2");
        return 0;
    }
}

接下来使用SPI 来定义实现类:
src/main/resources/META-INF/services/dongshi.daddy.strategy.v2.PayAmountStrategy:

dongshi.daddy.strategy.v2.CouponPayAmountStrategy
dongshi.daddy.strategy.v2.DecreasePayAmountStrategy
dongshi.daddy.strategy.v2.DiscountPayAmountStrategy
dongshi.daddy.strategy.v2.NYuanPayAmountStrategy

定义简单工厂类:

public class PayTypeFactory {
    //    private static Map<Integer, PayAmountStrategy> productMap = new HashMap<>();
    private static Map<String, PayAmountStrategy> productMap = new HashMap<>();

    static {
        ServiceLoader<PayAmountStrategy> load = ServiceLoader.load(PayAmountStrategy.class);
        Iterator<PayAmountStrategy> iterator = load.iterator();
        while (iterator.hasNext()) {
            PayAmountStrategy next = iterator.next();
            Class<? extends PayAmountStrategy> aClass = next.getClass();
            if (!aClass.isAnnotationPresent(dongshi.daddy.strategy.v2.Key.class))
                throw new IllegalStateException("class: " + aClass + " expect @dongshi.daddy.strategy.v2.Key, but not found!");
//            String payType = aClass.getAnnotation(Key.class).payType();
            productMap.put(aClass.getAnnotation(Key.class).payType(), next);
        }
    }

    public static PayAmountStrategy makeProduct(String payType) {
        return productMap.get(payType);
    }
}

客户端测试类:

public class StrategyClient {
    public static void main(String[] args) {
        PayAmountContext payAmountContext = new PayAmountContext();
        /*// 使用直减策略
        payAmountContext.setPayAmount(new DecreasePayAmountStrategy());
        payAmountContext.payAmount(null);
        // 使用N元购策略
        payAmountContext.setPayAmount(new NYuanPayAmountStrategy());
        payAmountContext.payAmount(null);
        // 使用满减策略
        payAmountContext.setPayAmount(new CouponPayAmountStrategy());
        payAmountContext.payAmount(null);
        // 使用折扣策略
        payAmountContext.setPayAmount(new DiscountPayAmountStrategy());
        payAmountContext.payAmount(null);*/
        // 使用直减策略
        payAmountContext.setPayAmount(PayTypeFactory.makeProduct("decreasePay"));
        payAmountContext.payAmount(null);
        // 使用N元购策略
        payAmountContext.setPayAmount(PayTypeFactory.makeProduct("nYuanPay"));
        payAmountContext.payAmount(null);
        // 使用满减策略
        payAmountContext.setPayAmount(PayTypeFactory.makeProduct("couponPay"));
        payAmountContext.payAmount(null);
        // 使用折扣策略
        payAmountContext.setPayAmount(PayTypeFactory.makeProduct("discountPay"));
        payAmountContext.payAmount(null);

    }
}

运行:

使用直减方式支付V2
使用N元购支付V2
使用满减支付V2
使用折扣方式支付V2

这样子,当我们系统增加了一种新的支付方式时,只需要提供具体的实现类,然后使用注解设置其标记,并将实现类定义到SPI文件中,在客户端就可以通过PayTypeFactory.makeProduct("新支付方式标记")来使用新支付方式了,符合开闭原则。

2:实际应用

2.1:处理微信公众号消息

微信公众号发送消息分为两类,第一种是普通消息,消息类型为text,如下:
在这里插入图片描述
还有一种是事件,消息类型为event,如下:
在这里插入图片描述
此时对于不同的消息类型,就可以定义不同的策略类,然后根据消息类型动态执行不同的策略。

策略类的创建使用了工厂模式,不同策略类因为具有相同的逻辑,所以也使用了模板方法设计模式。一般的策略设计模式+工厂设计模式+模板方法设计模式就是用的最多的黄金搭档了。

2.2:大模型问答敏感词过滤,限次等

在这里插入图片描述
在公司的大模型问答产品(文本机器人)中,对于用户录入的内容需要做敏感词过滤,限次等功能。对于这些功能都是属于产品的规则范畴的,变化的频率比较高,所以应该和主功能解耦,因为不同的规则,其实就是不同的策略类,所以这里就可以使用策略设计模式来实现功能,首先定义策略接口:

/**
 * 规则过滤接口(即策略设计模式中的策略接口)
 */
public interface ILogicFilter {
    /**
     * 对封装了用户请求信息的类ChatProcessAggregate进行规则过滤,即应用规则,并返回一个
     * 包装了处理后的请求对象的规则处理结果对象,这样可以方便的从规则处理结果对象中获取处理结果,
     * 并且获取业务上需要的用户信息对象
     * @param chatProcess
     * @return
     * @throws Exception
     */
//    RuleLogicEntity<ChatProcessAggregate> filter(ChatProcessAggregate chatProcess) throws Exception;
    RuleLogicEntity<ChatProcessAggregates> filter(ChatProcessAggregates chatProcess) throws Exception;
}

敏感词策略子类:

@Slf4j
@Component
@LogicStrategy(logicMode = DefaultLogicFactory.LogicMode.SENSITIVE_WORD)
public class SensitiveWordFilter implements ILogicFilter {

    @Resource
    private SensitiveWordBs words;

    @Value("${app.config.white-list}")
    private String whiteListStr;

    @Override
    public RuleLogicEntity<ChatProcessAggregates> filter(ChatProcessAggregates chatProcess) throws Exception {
        // 白名单用户不做敏感词处理
        if (chatProcess.isWhiteList(whiteListStr)) {
            return RuleLogicEntity.<ChatProcessAggregates>builder()
                    .type(LogicCheckTypeVO.SUCCESS)
                    .data(chatProcess)
                    .build();
        }

        /*
        ChatProcessAggregates chatProcessAggregates = ChatProcessAggregates.builder()
        .token(token)
        .model(request.getModel())
        .questionList(request.getMessages().stream()
                .map(zhiPuAiMessageEntity -> MessageEntity.builder()
                        .question(zhiPuAiMessageEntity.getContent())
                        .role(zhiPuAiMessageEntity.getRole())
                        .build())
                .collect(Collectors.toList()))
        .build();

         */
        ChatProcessAggregates newChatProcessAggregate = new ChatProcessAggregates();
        newChatProcessAggregate.setModel(chatProcess.getModel());
        newChatProcessAggregate.setOpenid(chatProcess.getOpenid());
        newChatProcessAggregate.setToken(chatProcess.getToken());

        // 下面做关键词处理
        List<MessageEntity> newQuestionList = chatProcess.getQuestionList().stream()
                .map(message -> {
                    String content = message.getQuestion();
                    // 替换或者直接删除掉敏感词,等!!!
                    String replace = words.replace(content);
                    return MessageEntity.builder()
                            .role(message.getRole())
//                            .name(message.getName())
                            .question(replace)
                            .build();
                })
                .collect(Collectors.toList());

        newChatProcessAggregate.setQuestionList(newQuestionList);

        return RuleLogicEntity.<ChatProcessAggregates>builder()
                .type(LogicCheckTypeVO.SUCCESS)
                .data(newChatProcessAggregate)
                .build();
    }

}

限次策略子类:

@Slf4j
@Component
@LogicStrategy(logicMode = DefaultLogicFactory.LogicMode.ACCESS_LIMIT)
public class AccessLimitFilter implements ILogicFilter {
    @Value("${app.config.limit-count:10}")
    private Integer limitCount;
    @Value("${app.config.white-list}")
    private String whiteListStr;
    @Resource
    private Cache<String, Integer> visitCache;

    @Override
    public RuleLogicEntity<ChatProcessAggregates> filter(ChatProcessAggregates chatProcess) throws Exception {
        // 1. 白名单用户直接放行
        if (chatProcess.isWhiteList(whiteListStr)) {
            return RuleLogicEntity.<ChatProcessAggregates>builder()
                    .type(LogicCheckTypeVO.SUCCESS).data(chatProcess).build();
        }
        String openid = chatProcess.getOpenid();

        // 2. 访问次数判断
        int visitCount = visitCache.get(openid, () -> 0);
        if (visitCount < limitCount) {
            visitCache.put(openid, visitCount + 1);
            log.info(openid + ", 剩余次数为:" + (limitCount - (visitCount + 1)));
            return RuleLogicEntity.<ChatProcessAggregates>builder()
                    .type(LogicCheckTypeVO.SUCCESS).data(chatProcess).build();
        } else {
        }

        return RuleLogicEntity.<ChatProcessAggregates>builder()
                .info("您今日的免费" + limitCount + "次,已耗尽!")
                .type(LogicCheckTypeVO.REFUSE).data(chatProcess).build();

    }
}

这里我没有定义context类来获取具体的策略(这也是设计模式需要注意的一个点,不要生搬硬套,核心是思想),而是直接从工厂类中来获取了,如下:

// 交给spring管理起来
@Service
public class DefaultLogicFactory {

    // 规则类型->规则处理类 字段
    public Map<String, ILogicFilter> logicFilterMap = new ConcurrentHashMap<>();

    /**
     * 在构造函数中构造对象(工厂核心所在)
     *
     * 需要注意:设计模式准确来说只是一种变成思想,不一定非得按照标准的来,只要符合思想,稍微变种也是可以的
     *
     * 比如这里就是在构造函数中创建了对象
     * 有多个spring 的bean,所以以list的形式接收???自动传进来???
     */
    public DefaultLogicFactory(List<ILogicFilter> logicFilters) {
        logicFilters.forEach(logic -> {
            LogicStrategy strategy = AnnotationUtils.findAnnotation(logic.getClass(), LogicStrategy.class);
            if (null != strategy) {
                logicFilterMap.put(strategy.logicMode().getCode(), logic);
            }
        });
    }

    // 创建对象,返回对象是创建对象,返回一批对象也是创建对象啊,至于这一个对象,或者是一批对象
    // 是通过哪种形式返回的,其实无所谓啊,都是返回对象嘛,就看你业务上咋方便了,这里就是返回了一个map
    public Map<String, ILogicFilter> openLogicFilter() {
        return logicFilterMap;
    }

    public enum LogicMode {
        AA("AAA", "vvv"), // 测试用!!!
        // 敏感词过滤 规则
        SENSITIVE_WORD("SENSITIVE_WORD", "敏感词过滤"),
        ACCESS_LIMIT("ACCESS_LIMIT", "访问次数限制")
        ;

        private String code;
        private String info;

        LogicMode(String code, String info) {
            this.code = code;
            this.info = info;
        }

        public String getCode() {
            return code;
        }

        public void setCode(String code) {
            this.code = code;
        }

        public String getInfo() {
            return info;
        }

        public void setInfo(String info) {
            this.info = info;
        }

    }
}

通过openLogicFilter就获取了所有的策略对象了。设计模式的应用是灵活多变的,这里就是一个很好的例子,是吧?

参考文章列表

策略模式(策略设计模式)详解

秒懂设计模式之策略模式(Strategy Pattern)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值