接口的常用用法都有什么?策略设计模式复习总结
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了接口的常用用法都有什么?策略设计模式复习总结相关的知识,希望对你有一定的参考价值。
从之前的一个总结性话题引出:
从接口、抽象类到工厂模式再到JVM来总结一些问题
涉及的知识点总结如下:
- 策略模式概念和例子
- 工厂模式和策略模式比较
- 策略模式优缺点
- 策略模式的意义
- 使用了策略模式的API——HttpServlet类分析
- 解决策略膨胀问题——享元模式
StrategyPattern:策略模式也算比较简单的,同工厂模式一样都属于面向接口编程……策略模式是对象的行为模式之一,而工厂模式是对象的创建模式!它对一系列的算法加以封装,为所有算法定义一个抽象的算法接口,并通过继承该抽象算法接口
对所有的算法加以封装和实现,具体的算法选择交由客户端决定(策略)。策略模式使得算法可以在不影响到客户端的情况下发生变化。Strategy模式主要用来平滑地处理算法的切换 。想到这里,联系之前的工厂模式(静态工厂,简单工厂,抽象工厂),不禁发问了:
抽象工厂模式就是策略模式吧?只不过这个策略是个简单工厂而已?先看策略模式到底是怎么运行的
前面说了,策略模式封装算法,自然有一个抽象的算法接口IStrategy,扩展的不同的策略(算法封装)StrategyABuilder,StrategyBBuilder……再来一个策略的容器(其实就是一个工厂),下面总结下策略模式的角色:
- Strategy接口 : 策略(算法)的抽象接口。
- ConcreteStrategy :各种策略(算法)的具体实现。
- Context:策略的外部封装类,或者说策略的容器类。根据不同策略执行不同的行为。策略由外部环境决定,自然这个工厂就需要聚合一个抽象策略接口的引用,并配有对应的执行策略的方法。
比如有这样一个程序,给文件加密的程序,当前有三种加密方法可以选用,分别是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 }
是不是非常简单!而且和工厂模型有有些类似,两个模式比较重要的一个区别在于:工厂是创建型的设计模式,偏重于创建不同的对象,而策略模式是行为型的设计模式,偏重于通过不同的方式实现同样的行为。反过来,创建对象本用的就是一个方法,通过不同的工厂动态指定实际的创建方法,就是封装了这个方法!可以说工厂模式的目的最终是为了创建对象,而策略模式的目的并不仅限于创建对象,可以说工厂应用了策略模式,更好地说法是,抽象工厂模式和策略模式都应用了面向接口编程的思想。
上面的示例可以看出,策略模式仅仅封装算法,提供新的算法插入到已有系统中,以及可以把不需要的算法从系统中除去,策略模式本身不决定在何时使用何种算法。在什么情况下使用什么算法是由客户端决定的——策略模式的一个很重要的特点:运行时策略的唯一性,也就是策略模式运行期间,策略模式的客户端在每一个时刻只能使用一个具体的策略实现对象,虽然可以动态地在不同的策略实现中切换,但是同时只能使用一个。
再看一个例子:购物结账问题,收银系统根据不同的折扣活动,得到商品的不同的价格。
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 }
等等,为什么有的人实现策略模式会用抽象类而不是接口呢?是无所谓还是另有原因?
这就要说到经常见到的一种场景:有时候所有的具体策略类都有一些公有的行为或者属性。这时候就应当把这些公有的行为或者属性统一提取,放到共同的抽象策略角色Strategy里面。当然这时候抽象策略角色必须要用抽象类;来实现,而不能使用接口。这其实也是典型的将代码向继承等级结构的上方集中的标准做法。
这里还有一个例子,如图,人需要出去旅游,出门赶路的方式有那么如下几种,骑自行车去,开汽车去,直接做火车,或者飞机去……我们应该能想到这是策略设计模式的一个应用场景,由此我想到有其他人问的一个问题:
为什么这个场景要使用策略模式?究竟使用策略模式有什么实际的好处or意义呢?
两种实现代码:
Person person = new Person(new Bike()); person.travel(); person = new Person(new Car()); person.travel();
不用策略模式,不是也可以么!如下:
Bike bike = new Bike(); bike.travel(); Car car = new Car(); car.travel();
其实还是又回到了之前那篇文章从接口、抽象类到工厂模式再到JVM来总结一些问题里提到的接口的意义,论规模,这个场景其实更类似是多态,回答问题之前,首先要明白一个道理——到底是 “人” 旅游,还是火车、汽车、自行车、飞机这些交通工具旅游?如果第二种实现方式,那旅游这件事就和人没有什么关系了,只和交通工具有关!!!应该是人可以选择xxx交通工具去xxx旅游,而不是直接让交通工具去自己选自己……而人选交通工具,又有这么多工具可以选择,毫无疑问交通工具是易于变化的,所以把对应的交通工具嵌入到人这个类是合理的。
再来看,第二种做法的坏处,我们来把场景详细说下:现在有两个人(Person),分别是台湾的小明,和上海的老王,他们是老同学,相约去北京旅游,他们可以通过火车、飞机和汽车这三种不同的交通工具到达北京见面,到北京之后,两个人同时通过自行车到达天安门和另一个老同学小丽见面……现在把程序扩展:旅行结束后,每个人旅行的总用时都要被录下来,用程序实现之。
如果是第二种做法,仅仅是通过调用 car.travel(),plane.travel(),train.travel() 和 bike.Travel() 方法的话,如何记录不同de人旅行的用时?也许强行的通过不同的交通工具对象来记录,但是假如两人到达北京以后,小明通过自行车到天安门,而老王通过汽车car临时又去了故宫……此时,记录每个人旅行总用时更加麻烦了!因为总用时是跟每个人,而不是某种交通工具有关的,所以这就是为什么要使用 person.travel() 的原因,就是说我们要明白某种行为(操作)的主体是什么。
面向对象的思想实际上就是将把现实世界中的万事万物看成是对象,并进行抽象,然后以计算机世界的方式对现实世界进行模拟。如果对现实世界的抽象错误,那么在之后就会碰到一系列很别扭,甚至无法解决的问题。策略模式的优点就在于替换或是增加用于实现相同的功能的算法非常方便(基于多态这个特性)。为了达到这样的优点,“面向接口编程” 是必须的。另外,策略模式一般强调行为,不关注不同的策略类本身的属性,也就是说对于图片中的例子,我们是不区分不同的 plane,bus 等交通工具对象的,我们需要的只是各个交通工具类的 travel() 方法!
充分的认识策略模式:策略模式的关注的不是如何实现算法,而是如何调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。它的优点有:
- 策略模式提供了管理相关的平等的算法族的办法。策略类的等级结构定义了一个算法或行为族。恰当使用继承可以把公共的代码移到父类里面,从而避免重复的代码。策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法,大家的地位是完全一样的,正因为这个平等性,才能实现算法之间可以相互替换。所有的策略算法在实现上也是相互独立的,所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现。
- 策略模式提供了可以替换继承关系的办法(聚合抽象的引用)。继承可以处理多种算法或行为。如果不用策略模式,那么使用算法或行为的环境类就可能会有一些子类,每一个子类提供一个不同的算法或行为。但是,这样一来算法或行为的使用者就和算法或行为本身混在一起。决定使用哪一种算法或采取哪一种行为的逻辑就和算法或行为的逻辑混合在一起,从而不可能再独立演化。继承使得动态改变算法或行为变得不可能。
- 使用策略模式可以避免使用多重条件转移语句。多重转移语句不易维护,它把采取哪一种算法或采取哪一种行为的逻辑与算法或行为的逻辑混合在一起,统统列在一个多重转移语句里面,比使用继承的办法还要原始和落后。
JDK里使用了策略模式的类都有哪些?
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(); }
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); }
发现下面这个 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); } }
在该转换之后的 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); } }
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); } }
在HttpServlet类中所提供的doGet、doPost……方法都是直接返回错误信息,所以我们需要在自己定义的Servlet类中重写这些方法,经过上面的歌过程,我们发现,HttpServlet 类就是一个策略抽象类,我们自己定义的servlet类去覆盖HttpServlet里的这些方法,自然不同的人,不同的项目里,需要重写的内容和方法都不尽相同,这就是一个策略模式的思想。
说了那么多,策略模式就没有缺点么?
前面我们发现,要想使用策略模式,客户端(调用者)必须清楚所有的策略都是什么,才能决定使用哪一个策略,也就是说,客户端必须理解所有算法的不同,才能适时的选择合适的算法,如果客户端不清楚以上,那么策略模式不适用。
现在开始说,策略模式策略类膨胀的问题,这一度被认为是策略模式的阴暗面。因为不论是为了解决多重if-else语句,还是解决switch case分支过多,并且扩展性差的问题,一旦可替换的策略(算法)越来越多,虽然扩展性得到解决,但是策略(算法)类太多了,每一个分支对应一个……又非常繁琐!这就是它的一个缺点!换句话说,策略模式造成很多的策略类!
怎么解决策略类膨胀的问题呢?
有时候可以通过把依赖于环境Context类的状态保存到客户端里面,而将策略类设计成可共享的,这样策略类实例可以被不同客户端使用。换言之,可以使用享元模式来减少对象的数量。下面就趁热打铁,说一说享元模式——
减小内存的占用问题——享元模式和单例模式的对比分析
以上是关于接口的常用用法都有什么?策略设计模式复习总结的主要内容,如果未能解决你的问题,请参考以下文章