基于注解的策略模式实际使用
前言
在我们实际业务场景中,往往对于一个流程会有多种不同的实现,在了解策略模式前,很多地方采用了很多类似面向过程编程的方法,导致编程异常复杂,往往一个小的改动会影响很多地方,这种实现方式是非常不对的,而了解了策略模式之后并没有在实际业务场景中使用过,今天我们结合虚拟的业务应用来一步步实现策略模式,相信看过之后大家就会明白为什么要使用策略模式以及如何在我们实际项目中更好的使用策略模式。
策略模式
策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。(摘自百度以及菜鸟教程)
第一次业务需求
假设我们要开一家餐厅,笔者负责厨师工作,因为业务不熟练只做拿手菜红烧肉,由于刚开业基于成本原因考虑,洗菜、配菜、炒菜、装盘工作都由厨师来负责。那么初版的代码应该是这样的。
public Food handle(Order order){
List<RawFood> RawFoods = wish(order);
Material material = pick(order);
Food food = cook(material);
decorate(food);
return food;
}
首先我们根据客户的订单进行清洗猪肉处理,洗完的肉再拿点白糖生姜大蒜葱花放在盆里,然后大厨将这些菜烹饪,最后装饰一下就可以喊服务员来拿菜了~。
问题所在
由于这个开业前期只做红烧肉,所有流程都是一样的,那么大厨处理的很快,突然老板娘说咱家特色菜应该再来一个小鸡炖蘑菇,这是我们是不是要在炒菜方法里来加上一个判断呢,如果订单是红烧肉就按照红烧肉来炒,如果订单是小鸡炖蘑菇那么就按照小鸡炖蘑菇来炒呢?
新增小鸡炖蘑菇后的烹饪方法
public Food cook(Material material){
if (MenuEnum.HONGSHAO_MEAT.getName().equals(material.getName())){
return new Food("红烧肉");
} else if (MenuEnum.COOK_CHICKEN.equals(material.getName())){
return new Food("小鸡炖蘑菇");
}
return null;
}
现在我们已经愉快的完成了老板娘的无理需求了,在老板娘的英明领导以及两道硬菜的吸引下,餐厅的生意蒸蒸日上,贪心的老板娘从网上打印了100道网红菜谱非要大厨学会,难道我们的大厨cook方法里面要写100个if-else吗?这时候,我们的策略方法就显示出了它的优越性了。
简单的策略模式
首先我们可以抽象cook方法为一个Cook接口,然后针对不同的菜单,实现cook方法。
public interface Cook{
public Food cook(Material material);
}
public class RedMeatCookImpl implements Cook{
public Food cook(Material material){
return new Food("红烧肉");
}
}
public class ChickenCookImpl implements Cook{
public Food cook(Material material){
return new Food("小鸡炖蘑菇");
}
}
然后修改我们的handle方法。
public Food handle(Order order){
List<RawFood> RawFoods = wish(order);
Material material = pick(order);
Cook cook = getCook(order);
Food food = cook.cook(material);
decorate(food);
return food;
}
public Cook getCook(Order order){
if (MenuEnum.HONGSHAO_MEAT.getName().equals(material.getName())){
return new RedMeatCookImpl();
} else if (MenuEnum.COOK_CHICKEN.equals(material.getName())){
return new ChickenCookImpl();
}
return null;
}
如果我们需要修改红烧肉的做菜,就不用去cook方法中根据一群if-else找到红烧肉的做法进行更改了,每一个菜的做法已经单独拿到了实现类里,与此同时我们也不用每次费劲心思在冗长的cook方法中新增else{}跟做法了,简单的策略模式已经成型了。
问题
在更改后的demo中,策略模式其实依然存在两个问题:
1. 我们新增的getCook方法需要知道每一个实现类。
2. 我们写的if-else跟之前一样多,我们只是将之前if-else里面的具体实现封装到一个类中,而没有减少我们if-else分支的数量。
另外,英明的老板娘也提出了一个业务需求:
1. wish和pick方法也需要根据不同菜品进行不同逻辑处理了,老板娘总是嫌弃厨师用洗土豆的粗暴方式洗豆腐,结果碎成豆腐沫导致客户给差评。
那么我们应该怎么办呢?
基于注解的策略模式实现
首先我们可以认为洗菜、配菜、烹饪都是针对订单的一个流程,诚然这三个流程的入参跟返回值可能是不同的,但是他们策略模式的筛选条件都是一样的:针对不同菜名有不同的行为。
对流程的枚举
首先我们定义一个流程的枚举类,将洗菜配菜烹饪都放入其中,这样做的好处是以后老板娘不清楚我们到底有什么流程时可以一目了然的知道各个流程。
public enum ProcessEnum {
WISH(101,"洗菜","洗菜"),
PICK(102,"配菜","配菜"),
COOK(103,"烹饪","烹饪"),
;
private Integer code;
private String name;
private String desc;
ProcessEnum(Integer code,String name,String desc){
this.code=code;
this.name=name;
this.desc=desc;
}
}
创建一个注解
注解的作用是为了:
1.找到对应的流程
2.方便执行筛选条件(if-else选择)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface choose {
ProcessEnum process();//对应流程
MenuEnum menu();//客户菜品
}
定义一个接口
在洗菜、配菜、烹饪流程中,我们可以把流程抽象为一个接口,让每个流程实现接口。
public interface Process {
public void excute(ParamContext context);
}
具体流程
@Slf4j
@Choose(value = ProcessEnum.COOK,menu = MenuEnum.HONGSHAO_MEAT)
public class RedMeatCook implements Process {
private final static String FOOD_NAME="红烧肉";
@Override
public void excute(ParamContext context) {
log.info("当前处理FOOD_NAME:{}",FOOD_NAME);
ResultContext resultContext=new ResultContext();
resultContext.setFood(new Food(FOOD_NAME));
log.info("当前处理FOOD_NAME:{},处理完成",FOOD_NAME);
}
}
@Slf4j
@Choose(value = ProcessEnum.COOK,menu= MenuEnum.COOK_CHICKEN)
public class ChickenCook implements Process {
private final static String FOOD_NAME="小鸡炖蘑菇";
@Override
public void excute(ParamContext context) {
log.info("当前处理FOOD_NAME:{}",FOOD_NAME);
ResultContext resultContext=new ResultContext();
resultContext.setFood(new Food(FOOD_NAME));
log.info("当前处理FOOD_NAME:{},处理完成",FOOD_NAME);
}
}
以红烧肉的烹饪方法为例,我们的choose注解指定了使用条件:流程为cook,并且菜单位红烧肉时使用,而我们的ChickenCook类则指定了菜单为小鸡炖蘑菇时使用,那么下面让我们来看下如何利用spring拿到这两个实现并根据不同入参执行实际excute方法。
利用spring获得每个具体类的引用
public class ProcessFactory implements ApplicationContextAware {
//TODO PostConstruct注解跟afterPropertiesSet InitializingBean, ApplicationContextAware 方法区别?
/**
* ApplicationContextAware接口实现了 setApplicationContext 方法
* PostConstruct注解跟afterPropertiesSet在此时可以被替代
* 但是afterPropertiesSet是接口调用 效率好一点 PostConstruct是反射
* 构造方法>PostConstruct>afterPropertiesSet>init-method
* Constructor > @Autowired >@PostConstruct > InitializingBean > init-method
*/
private ApplicationContext applicationContext;//spring容器上下文 继承ApplicationContextAware接口后注入
private final static Map<String, Process> holder = new HashMap<>();
private static final String UNDER_LINE = "_";
@PostConstruct
private void initData() {
Map<String, Object> processsMap = applicationContext.getBeansWithAnnotation(Choose.class);
for (Map.Entry entry : processsMap.entrySet()) {
Object process = entry.getValue();
if (process instanceof Process) {
loadHolder((Process) process);
} else {
throw new RuntimeException(String.format("注解使用异常,类:%s使用注解:%s错误!", entry.getKey(), process.getClass().getAnnotation(Choose.class)));
}
}
}
private void loadHolder(Process process) {
Choose choose = process.getClass().getAnnotation(Choose.class);
ProcessEnum processEnum = choose.value();
MenuEnum menuEnum = choose.menu();
//TODO 添加空值校验以及默认值支持
holder.put(processEnum.getCode() + UNDER_LINE + menuEnum.getCode(), process);
}
public static Process getProcess(ProcessEnum process, MenuEnum menu) {
//TODO 添加空值校验以及默认值支持
return holder.get(process.getCode() + UNDER_LINE + menu.getCode());
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
}
在这里我们使用@PostConstruct注解,在spring加载bean完成之后,执行我们的initData方法,获得每个被choose注解的具体类,然后利用map将 processEnum以及MenuEnum的名字拼接起来作为key,具体类作为value来保存对应关系,然后后续使用的时候,执行getProcess方法获取具体实现类并调用excute方法就可以实现实际调用了。
写一个Excuter
在我们业务代码中如果调用getProcess以及excute方法,其实对于代码控制来说并不优雅,因为我们可能有好多业务代码都需要这样调用,如果我们需要在调用时加日志或者是监控,那么好多业务代码都会非常冗杂,我们可以写一个Excuter将调用封装起来。
@Slf4j
public abstract class Excutor {
public static void excute(ProcessEnum processEnum,ParamContext paramContext){
Process realProcess = ProcessFactory.getProcess(processEnum,paramContext.getMenu());
log.info("流程:{},处理菜名:{},当前处理的process类是:{},",processEnum.getName(),paramContext.getMenu().getName(),realProcess.getClass());
realProcess.excute(paramContext);
}
}
业务调用
public class Restaurant{
public Food handle(Order order){
ParamContext context = createContext();
excuteProcess(ProcessEnum.WISH,context);
excuteProcess(ProcessEnum.PICK,context);
excuteProcess(ProcessEnum.COOK,context);
return context.getFood();
}
private void excuteProcess(ProcessEnum processEnum,ParamContext context){
Excutor.excute(processEnum,context);
}
}
这样看来是不是就业务代码是不是就非常简单了呢,并且在后期新加流程实现时,我们只需要写一个实现了process接口并且用choose注解的类就可以了,在后期维护时是非常方便的。