从之前的一个总结性话题引出:
涉及的知识点总结如下:
StrategyPattern:策略模式也算比较简单的,同工厂模式一样都属于面向接口编程…… 策略模式是对象的行为模式之一,而工厂模式是对象的创建模式! 它对一系列的算法加以封装 ,为所有算法定义一个抽象的算法接口,并通过继承该抽象算法接口
对所有的算法加以封装和实现,具体的算法选择交由客户端决定(策略)。 策略模式使得算法可以在不影响到客户端的情况下发生变化。 Strategy模式主要用来 平滑地处理算法的切换 。 想到这里,联系之前的工厂模式(静态工厂,简单工厂,抽象工厂),不禁发问了:
抽象工厂模式就是策略模式吧?只不过这个策略是个简单工厂而已? 先看策略模式到底是怎么运行的
前面说了,策略模式封装算法,自然有一个抽象的算法接口IStrategy,扩展的不同的策略(算法封装)StrategyABuilder,StrategyBBuilder…… 再来一个策略的容器(其实就是一个工厂) ,下面总结下策略模式的角色:
比如有这样一个程序,给文件加密的程序,当前有三种加密方法可以选用,分别是MD5加密算法、RSA加密算法,和AES加密算法,下面运用策略模式,看代码实现;
1 public interface EncryptStrategy { 2 /** 3 * 加密算法的抽象接口 4 */ 5 void doEncrypt(); 6 } 7 8 public class AESEncryptBuilder implements EncryptStrategy { 9 @Override 10 public void doEncrypt() { 11 System.out.println("进行AES加密!"); 12 } 13 } 14 15 public class MD5EncryptBuilder implements EncryptStrategy { 16 @Override 17 public void doEncrypt() { 18 System.out.println("进行MD5加密!"); 19 } 20 } 21 22 public class RSAEncryptBuilder implements EncryptStrategy { 23 @Override 24 public void doEncrypt() { 25 System.out.println("进行RSA加密!"); 26 } 27 } 28 29 public class Factory { 30 /** 31 * 聚合的算法接口 32 */ 33 private EncryptStrategy encryptStrategy; 34 35 public Factory(EncryptStrategy encryptStrategy) { 36 this.encryptStrategy = encryptStrategy; 37 } 38 39 /** 40 * 执行加密操作 41 * 本身不实现加密算法,而是转而去调用聚合的算法接口持有的方法 42 */ 43 public void execute() { 44 this.encryptStrategy.doEncrypt(); 45 } 46 } 47 48 public class TestStrategy { 49 public static void main(String[] args) { 50 Factory factory = new Factory(new MD5EncryptBuilder()); 51 factory.execute(); 52 } 53 } View Code
是不是非常简单!而且和 工厂模型有有些类似, 两个模式比较重要的一个区别 在于:工厂是创建型的设计模式,偏重于创建不同的对象,而策略模式是行为型的设计模式,偏重于通过不同的方式实现同样的行为。 反过来,创建对象本用的就是一个方法,通过不同的工厂动态指定实际的创建方法,就是封装了这个方法!可以说工厂模式的目的最终是为了创建对象,而策略模式的目的并不仅限于创建对象,可以说 工厂应用了策略模式,更好地说法是,抽象工厂模式和策略模式都应用了面向接口编程的思想。
上面的示例可以看出,策略模式仅仅封装算法,提供新的算法插入到已有系统中,以及可以把不需要的算法从系统中除去,策略模式本身不决定在何时使用何种算法。 在什么情况下使用什么算法是由客户端决定的——策略模式的一个很重要的特点: 运行时策略的唯一性, 也就是策略模式运行期间,策略模式的客户端在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态地在不同的策略实现中切换,但是同时只能使用一个。
再看一个例子:购物结账问题,收银系统根据不同的折扣活动,得到商品的不同的价格。
1 public interface Strategy { 2 /** 3 * 花费的价钱计算 4 * 5 * @param num 6 * @return double 7 */ 8 double cost(double num); 9 } 10 11 public class StrategyA implements Strategy { 12 /** 13 * 打8折 14 * 15 * @param num 16 * @return double 17 */ 18 @Override 19 public double cost(double num) { 20 return num * 0.8; 21 } 22 } 23 24 public class StrategyB implements Strategy { 25 /** 26 * 9折 27 * 28 * @param num 29 * @return double 30 */ 31 @Override 32 public double cost(double num) { 33 return num * 0.9; 34 } 35 } 36 37 public class Context { 38 private Strategy strategy; 39 40 public Context(Strategy strategy) { 41 this.strategy = strategy; 42 } 43 44 public double costs(double num) { 45 return this.strategy.cost(num); 46 } 47 } 48 49 public class MainDemo { 50 public static void main(String[] args) { 51 double num = 200; 52 Context context = new Context(new StrategyB()); 53 double lastNum = context.costs(num); 54 55 System.out.println(lastNum);// 180.0 56 } 57 } View Code
这就要说到经常见到的一种场景:有时候所有的具体策略类都有一些公有的行为或者属性。这时候就应当把这些公有的行为或者属性统一提取,放到共同的抽象策略角色Strategy里面。当然这时候抽象策略角色必须要用抽象类;来实现,而不能使用接口。这其实也是典型的将代码向继承等级结构的上方集中的标准做法。
这里还有一个例子,如图,人需要出去旅游,出门赶路的方式有那么如下几种,骑自行车去,开汽车去,直接做火车,或者飞机去……我们应该能想到这是策略设计模式的一个应用场景,由此我想到有其他人问的一个问题:
两种实现代码:
Person person = new Person(new Bike()); person.travel(); person = new Person(new Car()); person.travel(); View Code
不用策略模式,不是也可以么!如下:
Bike bike = new Bike(); bike.travel(); Car car = new Car(); car.travel(); View Code
其实还是又回到了之前那篇文章 从接口、抽象类到工厂模式再到JVM来总结一些问题 里提到的接口的意义,论规模,这个场景其实更类似是多态,回答问题之前, 首先要明白一个道理——到底是 “人” 旅游,还是火车、汽车、自行车、飞机这些交通工具旅游?如果第二种实现方式,那旅游这件事就和人没有什么关系了,只和交通工具有关!!! 应该是人可以选择xxx交通工具去xxx旅游,而不是直接让交通工具去自己选自己……而人选交通工具,又有这么多工具可以选择,毫无疑问交通工具是易于变化的,所以把对应的交通工具嵌入到人这个类是合理的。
再来看,第二种做法的坏处,我们来把场景详细说下:现在有两个人(Person),分别是台湾的小明,和上海的老王,他们是老同学,相约去北京旅游,他们可以通过火车、飞机和汽车这三种不同的交通工具到达北京见面,到北京之后,两个人同时通过自行车到达天安门和另一个老同学小丽见面…… 现在把程序扩展:旅行结束后,每个人旅行的总用时都要被录下来 ,用程序实现之。
如果是第二种做法,仅仅是通过调用 car.travel(),plane.travel(),train.travel() 和 bike.Travel() 方法的话,如何记录 不同de人 旅行的用时?也许强行的通过不同的交通工具对象来记录,但是假如两人到达北京以后,小明通过自行车到天安门,而老王通过汽车car临时又去了故宫……此时,记录每个人旅行总用时更加麻烦了!因为总用时是跟每个人,而不是某种交通工具有关的,所以这就是为什么要使用 person.travel() 的原因,就是说我们要明白某种行为(操作)的主体是什么。
面向对象的思想实际上就是将把现实世界中的万事万物看成是对象,并进行抽象,然后以计算机世界的方式对现实世界进行模拟。 如果对现实世界的抽象错误,那么在之后就会碰到一系列很别扭,甚至无法解决的问题。 策略模式的优点就在于替换或是增加用于实现相同的功能的算法非常方便(基于多态这个特性)。为了达到这样的优点,“面向接口编程” 是必须的 。另外,策略模式一般强调行为,不关注不同的策略类本身的属性,也就是说对于图片中的例子,我们是不区分不同的 plane,bus 等交通工具对象的,我们需要的只是各个交通工具类的 travel() 方法!
充分的认识策略模式:策略模式的关注的不是如何实现算法,而是如何调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。 它的优点有:
javax.servlet.http.HttpServlet类:
HTTP请求实际上只是一个HTTP请求报文,Web容器会自动将这个HTTP请求报文包装成一个HttpServletRequest对象,并且自动调用HttpServlet的 service() 方法来解析这个HTTP请求,service()方法会解析HTTP请求行,而HTTP请求行由method,URI,HTTPVersion三个组成,method就是get或者post,service() 方法根据 method 来决定是执行 doGet 还是 doPost,这一切都是服务器(容器)自动完成的,HTTP的格式也自动被解析。 只要自定义的类继承了HttpServlet,并且在web.xml里面配置了相应的servlet和mapping,服务器就会自动执行以上过程。而我们自定义的 每一个Servlet都必须要实现Servlet接口,而图里的GenericServlet是个通用的、不特定于任何协议的Servlet,它实现了Servlet接口
而HttpServlet继承于GenericServlet,因此HttpServlet也实现了Servlet接口,所以我们定义的Servlet只需要继承HttpServlet父类即可。Servlet接口中定义了一个service方法:
public interface Servlet { void init(ServletConfig var1) throws ServletException; ServletConfig getServletConfig(); void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; String getServletInfo(); void destroy(); } View Code
HttpServlet对该方法进行了实现,实现方式就是将ServletRequest与ServletResponse转换为HttpServletRequest与HttpServletResponse。转换完毕后,会调用HttpServlet类中自己定义的service方法
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request; HttpServletResponse response; try { request = (HttpServletRequest)req; response = (HttpServletResponse)res; } catch (ClassCastException var6) { throw new ServletException("non-HTTP request or response"); } this.service(request, response); } View Code
发现下面这个 service 是 protected方法
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); long errMsg; if(method.equals("GET")) { errMsg = this.getLastModified(req); if(errMsg == -1L) { this.doGet(req, resp); } else { long ifModifiedSince; try { ifModifiedSince = req.getDateHeader("If-Modified-Since"); } catch (IllegalArgumentException var9) { ifModifiedSince = -1L; } if(ifModifiedSince < errMsg / 1000L * 1000L) { this.maybeSetLastModified(resp, errMsg); this.doGet(req, resp); } else { resp.setStatus(304); } } } else if(method.equals("HEAD")) { errMsg = this.getLastModified(req); this.maybeSetLastModified(resp, errMsg); this.doHead(req, resp); } else if(method.equals("POST")) { this.doPost(req, resp); } else if(method.equals("PUT")) { this.doPut(req, resp); } else if(method.equals("DELETE")) { this.doDelete(req, resp); } else if(method.equals("OPTIONS")) { this.doOptions(req, resp); } else if(method.equals("TRACE")) { this.doTrace(req, resp); } else { String errMsg1 = lStrings.getString("http.method_not_implemented"); Object[] errArgs = new Object[]{method}; errMsg1 = MessageFormat.format(errMsg1, errArgs); resp.sendError(501, errMsg1); } } View Code
在该转换之后的 service 方法中,首先获得到请求的方法名,然后根据方法名调用对应的doXXX方法,比如说请求方法为GET,那么就去调用doGet方法;请求方法为POST,那么就去调用doPost方法。比如:
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String protocol = req.getProtocol(); String msg = lStrings.getString("http.method_put_not_supported"); if(protocol.endsWith("1.1")) { resp.sendError(405, msg); } else { resp.sendError(400, msg); } } doPut方法
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String protocol = req.getProtocol(); String msg = lStrings.getString("http.method_get_not_supported"); if(protocol.endsWith("1.1")) { resp.sendError(405, msg); } else { resp.sendError(400, msg); } } doGet方法
在HttpServlet类中所提供的doGet、doPost……方法都是直接返回错误信息,所以我们需要在自己定义的Servlet类中重写这些方法,经过上面的歌过程,我们发现,HttpServlet 类就是一个策略抽象类,我们自己定义的servlet类去覆盖HttpServlet里的这些方法,自然不同的人,不同的项目里,需要重写的内容和方法都不尽相同,这就是一个策略模式的思想。
前面我们发现,要想使用策略模式,客户端(调用者)必须清楚所有的策略都是什么,才能决定使用哪一个策略,也就是说, 客户端必须理解所有算法的不同,才能适时的选择合适的算法,如果客户端不清楚以上,那么策略模式不适用。
现在开始说, 策略模式策略类膨胀的问题 ,这一度被认为是策略模式的阴暗面。因为不论是为了解决多重if-else语句,还是解决switch case分支过多,并且扩展性差的问题,一旦可替换的策略(算法)越来越多,虽然扩展性得到解决,但是策略(算法)类太多了,每一个分支对应一个……又非常繁琐!这就是它的一个缺点!换句话说,策略模式造成很多的策略类!
有时候可以通过把依赖于环境Context类的状态保存到客户端里面,而将策略类设计成可共享的,这样策略类实例可以被不同客户端使用。换言之,可以使用享元模式来减少对象的数量。下面就趁热打铁,说一说享元模式——