策略模式说明
在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。
这样,在后续调整渠道时只需要新增/删除一个类,而不会影响整体的逻辑。
改造优化
在类中购物渠道分为拼多多、淘系、京东和当当这4个渠道。以前的做法是根据不同的渠道,if/else判断取调用不同的代码。
还有种情况是这几个渠道消费相同的mq,原本的写法是建立4个消费者对应4个类来消费数据,然后根据其中的某个渠道来过滤和调用对应的实现类。
这种方法不仅类很多,而且会占用mq的信道,在后续维护也会很麻烦。
在这种业务情景下,可以由一个入口,然后根据不同的名称路由到不同的类,那么这就很适合用策略模式了。
策略中new出来的类的问题
在很多文章中Context类中,在具体绑定关系时用不同的名称new出不同的方法。举例如下:
这样做的问题在于,当使用例如下面的CacheClusterService交给Spring管理的类时,new出来的就无法正常使用,会抛空指针的错误。
switch (strategyType) {
case "add":
strategy = new ConcreteStrategyAdd();
break;
case "sub":
strategy = new ConcreteStrategySub();
break;
case "mul":
strategy = new ConcreteStrategyMul();
break;
}
中间件相关的类
那么我们就模拟一个交由Spring管理的类来获取值,看是否能正常获取。
缓存
模拟使用缓存获取值。
@Component
@Slf4j
public class CacheClusterService {
public String getValue(String key){
//模拟获取redis的值
return key;
}
}
策略模式
策略模式仍然是一个接口,然后实现接口,从而达到抽象的目的。
public interface IStrategyHard1 {
enum StrategyType{
PDD,TX,JD
}
String buy();
String getType();
}
以下方法实现这个接口:
京东渠道
@Service
public class JdBuyChannel implements IStrategyHard1{
@Resource
private CacheClusterService cacheClusterService;
@Override
public String buy() {
return cacheClusterService.getValue("jd");
}
@Override
public String getType() {
return StrategyType.JD.name();
}
}
拼多多渠道
@Service
public class PddBuyChannel implements IStrategyHard1{
@Resource
private CacheClusterService cacheClusterService;
@Override
public String buy() {
return cacheClusterService.getValue("pdd");
}
@Override
public String getType() {
return StrategyType.PDD.name();
}
}
淘系渠道
@Service
public class TxBuyChannel implements IStrategyHard1{
@Resource
private CacheClusterService cacheClusterService;
@Override
public String buy() {
return cacheClusterService.getValue("tx");
}
@Override
public String getType() {
return IStrategyHard1.StrategyType.TX.name();
}
}
创建Context类
目前实现的方法有好几种方式,主要在于和工厂模式的结合。
实现方式1:实现ApplicationContextAware接口
用ApplicationContextAware,当一个类实现了这个接口(ApplicationContextAware)之后,这个类就可以方便获得ApplicationContext中的所有bean。换句话说,就是这个类可以直接获取spring配置文件中,所有有引用到的bean对象。
在方法中重写setApplicationContext()方法,用applicationContext获取接口的各个实现类,然后绑定类名别名与类。
@Slf4j
@Component
public class OnlineBuyChannelContext implements ApplicationContextAware {
private Map<String,IStrategyHard1> BUY_CHANNEL_MAP = new ConcurrentHashMap<>();
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
Map<String,IStrategyHard1> beanTypes = applicationContext.getBeansOfType(IStrategyHard1.class);
log.info("beanTypes:{}",beanTypes);
beanTypes.entrySet().forEach(entry->{
BUY_CHANNEL_MAP.put(entry.getValue().getType(),entry.getValue());
});
}
public IStrategyHard1 getBuyChannel(String type){
if (StringUtils.isBlank(type)){
throw new RuntimeException("type is null");
}
return BUY_CHANNEL_MAP.get(type);
}
}
实现方式2:各实现类使用@PostConstruct初始化
用@PostConstruct,实现Spring初始化时将类名别名与类绑定。
@Slf4j
@Component
public class OnlineBuyChannelContext2 {
private Map<String, IStrategyHard2> BUY_CHANNEL_MAP2 = new ConcurrentHashMap<>();
public void register(String type,IStrategyHard2 iStrategyHard2Impl){
if (StringUtils.isBlank(type)){
throw new RuntimeException("type is null");
}
BUY_CHANNEL_MAP2.put(type,iStrategyHard2Impl);
}
public IStrategyHard2 getBuyChannel(String type){
if (StringUtils.isBlank(type)){
throw new RuntimeException("type is null");
}
return BUY_CHANNEL_MAP2.get(type);
}
}
在每个方法中使用@PostConstruct,不再使用IStrategyHard1接口的String getType()方法。
举例:
@Service
public class JdBuyChannel2 implements IStrategyHard2 {
@Resource
private CacheClusterService2 cacheClusterService2;
@Resource
private OnlineBuyChannelContext2 onlineBuyChannelContext2;
@Override
public String buy() {
return cacheClusterService2.getValue("jd");
}
@PostConstruct
public void init(){
onlineBuyChannelContext2.register(IStrategyHard2.StrategyType.JD.name(),this);
}
}
实现方式3:实现InitializingBean接口
此处使用的是实现InitializingBean接口,然后重写afterPropertiesSet()方法。
注意该方法会在spring容器启动初始化bean,即各个策略类完成后调用afterPropertiesSet
方法里调用register
方法。
@Service
public class JdBuyChannel3 implements IStrategyHard3, InitializingBean {
@Resource
private CacheClusterService3 cacheClusterService3;
@Resource
private OnlineBuyChannelContext3 onlineBuyChannelContext3;
@Override
public String buy() {
return cacheClusterService3.getValue("jd");
}
@Override
public void afterPropertiesSet() throws Exception {
onlineBuyChannelContext3.register(StrategyType.JD.name(),this);
}
}
测试类
经过测试,这几种方式都可以实现方法。
@RestController
@Slf4j
public class TestController {
@Autowired
private OnlineBuyChannelContext onlineBuyChannelContext;
@Autowired
private OnlineBuyChannelContext2 onlineBuyChannelContext2;
/**
* 策略模式1:直接用applicationContext注册
* @return String
*/
@GetMapping("/test/getBuy")
public String getBuy(){
IStrategyHard1 iStrategyHard1 = onlineBuyChannelContext.getBuyChannel(IStrategyHard1.StrategyType.DD.name());
String buyChannel = iStrategyHard1.buy();
log.info("buyChannel:{}",buyChannel);
return buyChannel;
}
/**
* 策略模式2:用@PostConstruct注册
* @return String
*/
@GetMapping("/test/getBuy2")
public String getBuy2(){
IStrategyHard2 iStrategyHard2 = onlineBuyChannelContext2.getBuyChannel(IStrategyHard2.StrategyType.DD.name());
String buyChannel = iStrategyHard2.buy();
log.info("buyChannel:{}",buyChannel);
return buyChannel;
}
}
复盘
- 通过以上方法会发现,实现工厂模式都是与Spring结合的,即在初始化时实现绑定关系;
- 实现策略模式,可实现不同的路由,对后续调整业务的影响会很小;