策略模式:告别if else
阅读完本篇文章你将了解到什么是策略模式,策略模式的优缺点,以及策略模式在源码中的应用。
策略模式引入
在软件开发中,我们常常会遇到这样的情况,实现某一个功能有多条途径,每一条途径对应一种算法,此时我们可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径。
譬如商场购物场景中,有些商品按原价卖,商场可能为了促销而推出优惠活动,有些商品打九折,有些打八折,有些则是返现10元等。而优惠活动并不影响结算之外的其他过程,只是在结算的时候需要根据优惠方案结算。
再比如不同的人出去旅游出行的交通方式也不同,经济条件好的会选择高铁飞机,而普通人可能会选择绿皮火车。
富豪老王打算去西藏旅游,老王定了豪华酒店,并且定了机票当天直达。而普通人老张也要去西藏旅游,他打算选择乘坐高铁出行。而学生党的我小汪肯定会选择绿皮火车,主要是为了看路边的风景,而不是因为穷。
下面我们用代码来描述一下上诉场景:
public class Travel { private String vehicle;//出行方式 private String name; public String getName() { return name; } public Travel(String name) { this.name = name; } public void setName(String name) { this.name = name; } public void setVehicle(String vehicle) { this.vehicle = vehicle; } public String getVehicle() { return vehicle; } public void TravelTool(){ if(name.equals("小汪")){ setVehicle("绿皮火车"); }else if(name.equals("老张")){ setVehicle("高铁"); }else if(name.equals("老王")){ setVehicle("飞机"); } System.out.println(name+"选择坐"+getVehicle()+"去西藏旅游"); } } public class Test { public static void main(String[] args) { Travel travel1 = new Travel("小汪"); Travel travel2 = new Travel("老王"); Travel travel3 = new Travel("老张"); travel1.TravelTool(); travel2.TravelTool(); travel3.TravelTool(); } } 小汪选择坐绿皮火车去西藏旅游 老王选择坐飞机去西藏旅游 老张选择坐高铁去西藏旅游
以上代码虽然完成了我们的需求,但是存在以下问题:
Travel类的TravelTool方法非常庞大,它包含各种人的旅行实现代码,在代码中出现了较长的 if…else… 语句,假如日后小汪发达了也想体验一下做飞机去西藏旅游,那就要去修改TravelTool方法。违反了 “开闭原则”,系统的灵活性和可扩展性较差。
算法的复用性差,如果在另一个系统中需要重用某些算法,只能通过对源代码进行复制粘贴来重用,无法单独重用其中的某个或某些算法。
策略模式
策略模式的介绍
策略模式(Strategy Pattern)中,定义算法族,分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户这算法体现了几个设计原则,
第一、把变化的代码从不变的代码中分离出来;
第二、针对接口编程而不是具体类(定义了策略接口);
第三、多用组合/聚合,少用继承(客户通过组合方式使用策略)。
策略模式的原理类图
角色分析
Context
(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。
Strategy
(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。
ConcreteStrategy
(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。
我们下面用策略模式来改进一下上面旅行的代码例子。
抽象策略类
Discount
public abstract class AbstractTravle { private String vehicle; private String name; public AbstractTravle(String vehicle, String name) { this.vehicle = vehicle; this.name = name; } public String getVehicle() { return vehicle; } public String getName() { return name; } public abstract void TravelTool(); }
ConcreteStrategy
(具体策略类)
public class XiaoWang extends AbstractTravle{ public XiaoWang(String vehicle, String name) { super(vehicle, name); } @Override public void TravelTool() { System.out.println(getName()+"选择坐"+getVehicle()+"去西藏旅游"); } } public class LaoWang extends AbstractTravle{ public LaoWang(String vehicle, String name) { super(vehicle, name); } @Override public void TravelTool() { System.out.println(getName()+"选择坐"+getVehicle()+"去西藏旅游"); } } public class LaoZhang extends AbstractTravle{ public LaoZhang(String vehicle, String name) { super(vehicle, name); } @Override public void TravelTool() { System.out.println(getName()+"选择坐"+getVehicle()+"去西藏旅游"); } }
环境类
public class Context { private AbstractTravle abstractTravle; public Context(AbstractTravle abstractTravle) { this.abstractTravle = abstractTravle; } public void TravelTool() { System.out.println(abstractTravle.getName()+"选择坐"+abstractTravle.getVehicle()+"去西藏旅游"); } }
策略模式总结
public class Test { public static void main(String[] args) { Context context1 = new Context(new LaoWang("飞机", "老王")); context1.TravelTool(); Context context2 = new Context(new LaoZang("高铁", "老张")); context2.TravelTool(); Context context3 = new Context(new XiaoWang("绿皮火车", "小汪")); context3.TravelTool(); } } 老王选择坐飞机去西藏旅游 老张选择坐高铁去西藏旅游 小汪选择坐绿皮火车去西藏旅游
策略模式的主要优点如下:
1.模式提供了对 “开闭原则” 的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
2.模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码。
3.模式提供了一种可以替换继承关系的办法。如果不使用策略模式而是通过继承,这样算法的使用就和算法本身混在一起,不符合 “单一职责原则”,而且使用继承无法实现算法或行为在程序运行时的动态切换。
4.模式可以避免多重条件选择语句。多重条件选择语句是硬编码,不易维护。
5.模式提供了一种算法的复用机制,由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类。
策略模式的主要缺点如下:
1.端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
2.将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类。
3.同时在客户端使用多个策略类,也就是说,在使用策略模式时,客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩余功能的情况。
适用场景
1.系统需要动态地在几种算法中选择一种,那么可以将这些算法封装到一个个的具体算法类中,而这些具体算法类都是一个抽象算法类的子类。换言之,这些具体算法类均有统一的接口,根据 “里氏代换原则” 和面向对象的多态性,客户端可以选择使用任何一个具体算法类,并只需要维持一个数据类型是抽象算法类的对象。
2.对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重条件选择语句来实现。此时,使用策略模式,把这些行为转移到相应的具体策略类里面,就可以避免使用难以维护的多重条件选择语句。
3.望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性与安全性。
源码分析策略模式的典型应用
Java Comparator 中的策略模式
java.util.Comparator 接口是比较器接口,可以通过 Collections.sort(List,Comparator) 和 Arrays.sort(Object[],Comparator) 对集合和数据进行排序,下面为示例程序
@Data @AllArgsConstructor public class Student { private Integer id; private String name; @Override public String toString() { return "{id=" + id + ", name='" + name + "'}"; } }
// 降序 public class DescSortor implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o2.getId() - o1.getId(); } } // 升序 public class AscSortor implements Comparator<Student> { @Override public int compare(Student o1, Student o2) { return o1.getId() - o2.getId(); } }
通过 Arrays.sort() 对数组进行排序
public class Test1 { public static void main(String[] args) { Student[] students = { new Student(3, "张三"), new Student(1, "李四"), new Student(4, "王五"), new Student(2, "赵六") }; toString(students, "排序前"); Arrays.sort(students, new AscSortor()); toString(students, "升序后"); Arrays.sort(students, new DescSortor()); toString(students, "降序后"); } public static void toString(Student[] students, String desc){ for (int i = 0; i < students.length; i++) { System.out.print(desc + ": " +students[i].toString() + ", "); } System.out.println(); } } 排序前: {id=3, name='张三'}, 排序前: {id=1, name='李四'}, 排序前: {id=4, name='王五'}, 排序前: {id=2, name='赵六'}, 升序后: {id=1, name='李四'}, 升序后: {id=2, name='赵六'}, 升序后: {id=3, name='张三'}, 升序后: {id=4, name='王五'}, 降序后: {id=4, name='王五'}, 降序后: {id=3, name='张三'}, 降序后: {id=2, name='赵六'}, 降序后: {id=1, name='李四'},
通过 Collections.sort() 对集合List进行排序
public class Test2 { public static void main(String[] args) { List<Student> students = Arrays.asList( new Student(3, "张三"), new Student(1, "李四"), new Student(4, "王五"), new Student(2, "赵六") ); toString(students, "排序前"); Collections.sort(students, new AscSortor()); toString(students, "升序后"); Collections.sort(students, new DescSortor()); toString(students, "降序后"); } public static void toString(List<Student> students, String desc) { for (Student student : students) { System.out.print(desc + ": " + student.toString() + ", "); } System.out.println(); } } 排序前: {id=3, name='张三'}, 排序前: {id=1, name='李四'}, 排序前: {id=4, name='王五'}, 排序前: {id=2, name='赵六'}, 升序后: {id=1, name='李四'}, 升序后: {id=2, name='赵六'}, 升序后: {id=3, name='张三'}, 升序后: {id=4, name='王五'}, 降序后: {id=4, name='王五'}, 降序后: {id=3, name='张三'}, 降序后: {id=2, name='赵六'}, 降序后: {id=1, name='李四'},
我们向 Collections.sort() 和 Arrays.sort() 分别传入不同的比较器即可实现不同的排序效果(升序或降序)
这里 Comparator 接口充当了抽象策略角色,两个比较器 DescSortor 和 AscSortor 则充当了具体策略角色,Collections 和 Arrays 则是环境角色
Spring Resource 中的策略模式
Spring 把所有能记录信息的载体,如各种类型的文件、二进制流等都称为资源,譬如最常用的Spring配置文件。
在 Sun 所提供的标准 API 里,资源访问通常由 java.NET.URL 和文件 IO 来完成,尤其是当我们需要访问来自网络的资源时,通常会选择 URL 类。
URL 类可以处理一些常规的资源访问问题,但依然不能很好地满足所有底层资源访问的需要,比如,暂时还无法从类加载路径、或相对于 ServletContext 的路径来访问资源,虽然 Java 允许使用特定的 URL 前缀注册新的处理类(例如已有的 http: 前缀的处理类),但是这样做通常比较复杂,而且 URL 接口还缺少一些有用的功能,比如检查所指向的资源是否存在等。
Spring 改进了 Java 资源访问的策略,Spring 为资源访问提供了一个 Resource 接口,该接口提供了更强的资源访问能力,Spring 框架本身大量使用了 Resource 接口来访问底层资源。
public interface Resource extends InputStreamSource { boolean exists(); // 返回 Resource 所指向的资源是否存在 boolean isReadable(); // 资源内容是否可读 boolean isOpen(); // 返回资源文件是否打开 URL getURL() throws IOException; URI getURI() throws IOException; File getFile() throws IOException; // 返回资源对应的 File 对象 long contentLength() throws IOException; long lastModified() throws IOException; Resource createRelative(String var1) throws IOException; String getFilename(); String getDescription(); // 返回资源的描述信息 }
Resource 接口是 Spring 资源访问策略的抽象,它本身并不提供任何资源访问实现,具体的资源访问由该接口的实现类完成——每个实现类代表一种资源访问策略。
Spring 为 Resource
接口提供的部分实现类如下:
1.lResource
:访问网络资源的实现类。
2.assPathResource
:访问类加载路径里资源的实现类。
3.leSystemResource
:访问文件系统里资源的实现类。
4.rvletContextResource
:访问相对于ServletContext 路径里的资源的实现类:
5.putStreamResource
:访问输入流资源的实现类。
6.teArrayResource
:访问字节数组资源的实现类。
7.itableResource
:写资源文件
类图如下:
AbstractResource 资源抽象类实现了 Resource 接口,为子类通用的操作提供了具体实现,非通用的操作留给子类实现,所以这里也应用了模板方法模式。(只不过缺少了模板方法)
Resource 不仅可在 Spring 的项目中使用,也可直接作为资源访问的工具类使用。意思是说:即使不使用 Spring 框架,也可以使用 Resource 作为工具类,用来代替 URL。
譬如我们可以使用 UrlResource 访问网络资源。
也可以通过其它协议访问资源,file: 用于访问文件系统;http: 用于通过 HTTP 协议访问资源;ftp: 用于通过 FTP 协议访问资源等
public class Test { public static void main(String[] args) throws IOException { UrlResource ur = new UrlResource("http://image.laijianfeng.org/hello.txt"); System.out.println("文件名:" + ur.getFilename()); System.out.println("网络文件URL:" + ur.getURL()); System.out.println("是否存在:" + ur.exists()); System.out.println("是否可读:" + ur.isReadable()); System.out.println("文件长度:" + ur.contentLength()); System.out.println("\n--------文件内容----------\n"); byte[] bytes = new byte[47]; ur.getInputStream().read(bytes); System.out.println(new String(bytes)); } } 文件名:hello.txt 网络文件URL:http://image.laijianfeng.org/hello.txt 是否存在:true 是否可读:true 文件长度:47 --------文件内容---------- hello world! welcome to http://laijianfeng.org
Spring Bean 实例化中的策略模式
Spring实例化Bean有三种方式:构造器实例化、静态工厂实例化、实例工厂实例化
具体实例化Bean的过程中,Spring中角色分工很明确,创建对象的时候先通
<?xml
version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="person" class="com.demo.Person"></bean>
<bean id="personWithParam" class="com.demo.Person">
<constructor-arg name="name" value="小旋锋"/>
</bean>
<bean id="personWirhParams" class="com.demo.Person">
<constructor-arg name="name" value="小旋锋"/>
<constructor-arg name="age" value="22"/>
</bean>
</beans>
过 ConstructorResolver 找到对应的实例化方法和参数,再通过实例化策略 InstantiationStrategy 进行实例化,根据创建对象的三个分支( 工厂方法、有参构造方法、无参构造方法 ), InstantiationStrategy 提供了三个接口方法:
public interface InstantiationStrategy { // 默认构造方法 Object instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner) throws BeansException; // 指定构造方法 Object instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner, Constructor<?> ctor, Object[] args) throws BeansException; // 指定工厂方法 Object instantiate(RootBeanDefinition beanDefinition, String beanName, BeanFactory owner, Object factoryBean, Method factoryMethod, Object[] args) throws BeansException; }
InstantiationStrategy 为实例化策略接口,扮演抽象策略角色,有两种具体策略类,分别为
SimpleInstantiationStrategy
和 CglibSubclassingInstantiationStrategy
leInstantiationStrategy 中对这三个方法做了简单实现,如果工厂方法实例化直接用反射创建对象,如果是构造方法实例化的则判断是否有 MethodOverrides,如果有无 MethodOverrides 也是直接用反射,如果有 MethodOverrides 就需要用 cglib 实例化对象,SimpleInstantiationStrategy 把通过 cglib 实例化的任务交给了它的子类ibSubclassingInstantiationStrategy。
总结
1.略类之间可以自由切换,由于策略类实现自同一个抽象,所以他们之间可以自由切换。
2.于扩展,增加一个新的策略对策略模式来说非常容易,基本上可以在不改变原有代码的基础上进行扩展。
3.使用多重条件,如果不使用策略模式,对于所有的算法,必须使用条件语句进行连接,通过条件判断来决定使用哪一种算法,在上一篇文章中我们已经提到,使用多重条件判断是非常不容易维护的。
以上就是java策略模式的详细内内容。也请关注脚本之家其它相关文章!
最新评论