策略模式是GoF23种设计模式中比较简单的了,也是常用的设计模式之一,今天我们就来看看策略模式。
我工作第三年的时候,重构旅游路线的机票查询模块,旅游路线分为四种情况:
在我重构前,代码差不多是这样的:
int type = 1; // 往返都可以直达 if (type == 1) { // 查询出两张机票 return; } // 去程无法直达,需要中转,但是返程可以直达 if (type == 2) { // 查询出三张机票(去程两张,返程一张) return; } // 去程可以直达,但是返程需要中转 if (type == 3) { // 查询出三张机票(去程一张,返程两张) return; } // 往返都无法直达 else{ // 查询出四张机票(去程两张,返程两张) return; } 复制代码
当时我还是菜鸡(现在也是),也不懂什么设计模式,就是感觉代码都写在一个类中,实在是太长了,不够清爽,不管是哪种类型的线路,最终都是返回机票集合,只是处理逻辑不同,可以提取一个接口出来,再开四个类去实现此接口,最后定义一个Map,Key是Type,Value是接口(实现类),根据Type决定调用哪个实现类,就像下面的酱紫:
public class Ticket { private String desc; public Ticket(String desc) { this.desc = desc; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } @Override public String toString() { return "Ticket{" + "desc='" + desc + '/'' + '}'; } } 复制代码
public interface QueryTicketService { List<Ticket> getTicketList(); } 复制代码
public class QueryTicketAService implements QueryTicketService { @Override public List<Ticket> getTicketList() { List<Ticket> list = new ArrayList<>(); list.add(new Ticket("去程机票")); list.add(new Ticket("返程机票")); return list; } } 复制代码
public class QueryTicketBService implements QueryTicketService { @Override public List<Ticket> getTicketList() { List<Ticket> list = new ArrayList<>(); list.add(new Ticket("去程第一张机票")); list.add(new Ticket("去程第二张机票")); list.add(new Ticket("返程机票")); return list; } } 复制代码
public class QueryTicketCService implements QueryTicketService { @Override public List<Ticket> getTicketList() { List<Ticket> list = new ArrayList<>(); list.add(new Ticket("去程机票")); list.add(new Ticket("返程第一张机票")); list.add(new Ticket("返程第二张机票")); return list; } } 复制代码
public class QueryTicketDService implements QueryTicketService { @Override public List<Ticket> getTicketList() { List<Ticket> list = new ArrayList<>(); list.add(new Ticket("去程第一张机票")); list.add(new Ticket("去程第二张机票")); list.add(new Ticket("返程第一张机票")); list.add(new Ticket("返程第二张机票")); return list; } } 复制代码
public class Main { static Map<Integer, QueryTicketService> map = new HashMap<>(); static { map.put(1, new QueryTicketAService()); map.put(2, new QueryTicketBService()); map.put(3, new QueryTicketCService()); map.put(4, new QueryTicketDService()); } public static void main(String[] args) { int type = 1; System.out.println(map.get(type).getTicketList()); } } 复制代码
运行结果:
[Ticket{desc='去程机票'}, Ticket{desc='返程机票'}] 复制代码
当初我也不知道什么设计模式,就是感觉这样写完,代码清爽多了,后来才知道这就是策略模式的雏形了。
GoF23种设计模式真正应用广泛的设计模式不多,但是策略模式绝对算其中之一了,你看,当初我都不懂这些,就写出了策略模式的雏形。
如果我们遇到类似于上面的需求,第一反应肯定是用if else语句或者switch语句,根据不同的情况执行不同的代码,这样做也没什么大问题,但是我们的项目会越来越复杂,这么做的缺陷就慢慢的显现了出来:如果现在线路新增了一个类型,需要中转两次,就又得加好几个判断的分支(去程中转一次,返程中转两次;去程中转两次,返程中转一次;去程直达,返程中转两次等等),想想就恐怖,这样分支会越来越多,代码会越来越长,越来越难以维护,所以策略模式出现了。
当一个逻辑中,有很多if else语句或者switch语句,而且它们需要解决的问题是一样的,就可以考虑策略模式。
最原始的策略模式有三个角色:
而我上面的代码,就有了策略模式的味道,有了Strategy,也有了ConcreteStrategy,缺少的就是Context,如果用最原始的设计模式的写法来实现,是酱紫的:
public class Context { static Map<Integer, QueryTicketStrategy> map = new HashMap<>(); static { map.put(1, new QueryTicketAConcreteStrategy()); map.put(2, new QueryTicketBConcreteStrategy()); map.put(3, new QueryTicketCConcreteStrategy()); map.put(4, new QueryTicketDConcreteStrategy()); } public void getTicketList(int type) { System.out.println(map.get(type).getTicketList()); } } 复制代码
public class Main { public static void main(String[] args) { Context context = new Context(); context.getTicketList(1); } } 复制代码
运行结果:
[Ticket{desc='去程机票'}, Ticket{desc='返程机票'}] 复制代码
在这里,我把类名重新定义了下,让人一眼就可以看出这里使用了策略模式,这也是阿里推荐的命名方法。
策略模式是不是很简单(我在学习设计模式的时候,甚至觉得它比单例、简单工厂还要简单),而且特别实用,下面我们来看看策略模式的UML图:
既然策略模式那么实用,那么在JDK中有策略模式的应用吗?当然有。JDK中定义的Comparator接口就是策略模式的一种实践了:
public class SortLengthComparator implements Comparator<String> { @Override public int compare(String o1, String o2) { return (o1.length() - o2.length() > 0) ? 1 : -1; } } 复制代码
public class Main { public static void main(String[] args) { List<String>list=new ArrayList<>(); list.add("hello"); list.add("world"); list.add("codebear"); list.add("balabala"); list.add("java"); list.sort(new SortLengthComparator()); System.out.println(list); } } 复制代码
我定义了一个比较器,实现了Comparator接口,重写了compare方法,实现了以比较字符串长度来比较字符串的功能。
运行结果:
[java, world, hello, balabala, codebear] 复制代码
Comparator接口就是Strategy,我定义的SortLengthComparator就是ConcreteStrategy。
定义一个比较器,虽然不难,但是总觉得不够简洁,不够方便,需要新建一个类,所以现在越来越多的人使用Lambda来进行排序,就像下面的酱紫:
List<String>list=new ArrayList<>(); list.add("hello"); list.add("world"); list.add("codebear"); list.add("balabala"); list.add("java"); List<String> newList = list.stream().sorted((a, b) -> (a.length() - b.length() > 0) ? 1 : -1).collect(Collectors.toList()); newList.forEach(System.out::println); 复制代码
虽然底层还是用的Comparator,但是这样的写法清爽多了,如果比较的策略比较复杂,或者有多个地方都需要用到这个比较策略,还是用最原始的写法更好一些。
现在我们已经知道了什么是策略模式,如何使用策略模式,但是还有一个天大的问题,要知道,现在每个项目都在用Spring,如果你还是这么写的话:
public class Context { static Map<Integer, QueryTicketStrategy> map = new HashMap<>(); static { map.put(1, new QueryTicketAConcreteStrategy()); map.put(2, new QueryTicketBConcreteStrategy()); map.put(3, new QueryTicketCConcreteStrategy()); map.put(4, new QueryTicketDConcreteStrategy()); } public void getTicketList(int type) { System.out.println(map.get(type).getTicketList()); } } 复制代码
就意味着实现类里面的依赖需要自己去维护,无法使用神奇的@Autowired注解,所以策略模式与Spring碰撞,策略模式必须发生一点改变,而这改变让策略模式变得更加简单,性能更好,也更加迷人。
@Service public class QueryTicketAConcreteStrategy implements QueryTicketStrategy { @Override public List<Ticket> getTicketList() { List<Ticket> list = new ArrayList<>(); list.add(new Ticket("去程机票")); list.add(new Ticket("返程机票")); return list; } } 复制代码
@Service public class QueryTicketDConcreteStrategy implements QueryTicketStrategy { @Override public List<Ticket> getTicketList() { List<Ticket> list = new ArrayList<>(); list.add(new Ticket("去程第一张机票")); list.add(new Ticket("去程第二张机票")); list.add(new Ticket("返程第一张机票")); list.add(new Ticket("返程第二张机票")); return list; } } 复制代码
@Service public class Context { @Autowired private QueryTicketStrategy queryTicketAConcreteStrategy; @Autowired private QueryTicketStrategy queryTicketDConcreteStrategy; private static Map<Integer, QueryTicketStrategy> map = new HashMap<>(); @PostConstruct public void init() { map.put(1, queryTicketAConcreteStrategy); map.put(4, queryTicketAConcreteStrategy); } public void getTicketList(int type) { System.out.println(map.get(type).getTicketList()); } } 复制代码
@SpringBootApplication public class Main { public static void main(String[] args) { ConfigurableApplicationContext run = SpringApplication.run(Main.class, args); run.getBean(Context.class).getTicketList(1); } } 复制代码
运行结果:
[Ticket{desc='去程机票'}, Ticket{desc='返程机票'}] 复制代码
原始的设计模式有一个缺点,不管是具体的策略实现类,还是上下文类,都不是单例模式,而我们的方法在大多数情况下是无状态的,所以改成单例模式是非常合适的,而结合了Spring,我们完全不需要手写单例模式,Spring就帮我们完成了。
不管是原始的策略模式,还是Spring与策略模式结合的第一种写法,都没有完全符合开闭原则,如果有新的策略引入,必须修改上下文类,往map里面添加一组新的映射关系,而第二种写法完美的解决了这个问题,而且让策略模式变得非常优雅,下面直接放出代码:
@Service("1") public class QueryTicketAConcreteStrategy implements QueryTicketStrategy { @Override public List<Ticket> getTicketList() { List<Ticket> list = new ArrayList<>(); list.add(new Ticket("去程机票")); list.add(new Ticket("返程机票")); return list; } } 复制代码
@Service("4") public class QueryTicketDConcreteStrategy implements QueryTicketStrategy { @Override public List<Ticket> getTicketList() { List<Ticket> list = new ArrayList<>(); list.add(new Ticket("去程第一张机票")); list.add(new Ticket("去程第二张机票")); list.add(new Ticket("返程第一张机票")); list.add(new Ticket("返程第二张机票")); return list; } } 复制代码
@Service public class Context { @Autowired private Map<String, QueryTicketStrategy> map = new HashMap<>(); public void getTicketList(int type) { String typeStr = String.valueOf(type); System.out.println(map.get(typeStr).getTicketList()); } } 复制代码
@SpringBootApplication public class Main { public static void main(String[] args) { ConfigurableApplicationContext run = SpringApplication.run(Main.class, args); run.getBean(Context.class).getTicketList(1); } } 复制代码
运行结果:
[Ticket{desc='去程机票'}, Ticket{desc='返程机票'}] 复制代码
这就是Spring和神奇、迷人之处了,竟然可以自动注入map,key就是beanName,value就是接口(具体的实现类)。
用这种写法不但完成了天然的单例模式,而且真正的符合了开闭原则,引入新的策略,完全不需要修改任何一行旧代码,自认为这种写法是最优雅、最迷人的。