五种Java设计模式透析

来源:互联网 发布:代尔塔劳保鞋淘宝 编辑:程序博客网 时间:2024/05/17 08:26

一、Java设计模式透析之 —— 模板方法(Template Method)

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/8744002

今天你还是像往常一样来上班,一如既往地开始了你的编程工作。

项目经理告诉你,今天想在服务器端增加一个新功能,希望写一个方法,能对Book对象进行处理,将Book对象的所有字段以XML格式进行包装,这样以后可以方便与客户端进行交互。并且在包装开始前和结束后要打印日志,这样方便调试和问题定位。

没问题!你觉得这个功能简直是小菜一碟,非常自信地开始写起代码。

Book对象代码如下:

[java] view plaincopy
  1. public class Book {  
  2.   
  3.     private String bookName;  
  4.   
  5.     private int pages;  
  6.   
  7.     private double price;  
  8.   
  9.     private String author;  
  10.   
  11.     private String isbn;  
  12.   
  13.     public String getBookName() {  
  14.         return bookName;  
  15.     }  
  16.   
  17.     public void setBookName(String bookName) {  
  18.         this.bookName = bookName;  
  19.     }  
  20.   
  21.     public int getPages() {  
  22.         return pages;  
  23.     }  
  24.   
  25.     public void setPages(int pages) {  
  26.         this.pages = pages;  
  27.     }  
  28.   
  29.     public double getPrice() {  
  30.         return price;  
  31.     }  
  32.   
  33.     public void setPrice(double price) {  
  34.         this.price = price;  
  35.     }  
  36.   
  37.     public String getAuthor() {  
  38.         return author;  
  39.     }  
  40.   
  41.     public void setAuthor(String author) {  
  42.         this.author = author;  
  43.     }  
  44.   
  45.     public String getIsbn() {  
  46.         return isbn;  
  47.     }  
  48.   
  49.     public void setIsbn(String isbn) {  
  50.         this.isbn = isbn;  
  51.     }  
  52.   
  53. }  

然后写一个类专门用于将Book对象包装成XML格式:
[java] view plaincopy
  1. public class Formatter {  
  2.   
  3.     public String formatBook(Book book) {  
  4.         System.out.println("format begins");  
  5.         String result = "";  
  6.         result += "<book_name>" + book.getBookName() + "</book_name>\n";  
  7.         result += "<pages>" + book.getPages() + "</pages>\n";  
  8.         result += "<price>" + book.getPrice() + "</price>\n";  
  9.         result += "<author>" + book.getAuthor() + "</author>\n";  
  10.         result += "<isbn>" + book.getIsbn() + "</isbn>\n";  
  11.         System.out.println("format finished");  
  12.         return result;  
  13.     }  
  14.   
  15. }  
调用代码如下:
[java] view plaincopy
  1. public class Test {  
  2.       
  3.     public static void main(String[] args) throws Exception {  
  4.         Book book = new Book();  
  5.         book.setBookName("Thinking in Java");  
  6.         book.setPages(880);  
  7.         book.setPrice(68);  
  8.         book.setAuthor("Bruce Eckel");  
  9.         book.setIsbn("9787111213826");  
  10.         Formatter formatter = new Formatter();  
  11.         String result = formatter.formatBook(book);  
  12.         System.out.println(result);  
  13.     }  
  14.   
  15. }  

你写好了之后,迫不及待地开始运行,运行结果也完全符合你的期望。


项目经理看完后,对你非常满意,小伙效率很高的嘛!你也非常的得意。

不过两天之后,项目经理又找到了你,他说之前没有考虑到需要交互的客户端还包括手机设备,而手机设备都比较吃流量,用XML格式来传输太耗流量了,想最好能改成使用JSON格式传输。但是之前的XML格式也要保留,最好可以由客户端指定使用哪种格式。

你有些不开心,心里低估着,为什么一开始不考虑周全呢,现在又要改遗留代码。但对方毕竟是领导,你还是要服从命令的,于是你开始修改Formatter类:

[java] view plaincopy
  1. public class Formatter {  
  2.   
  3.     public static final int XML = 0;  
  4.   
  5.     public static final int JSON = 1;  
  6.   
  7.     public String formatBook(Book book, int format) {  
  8.         System.out.println("format begins");  
  9.         String result = "";  
  10.         if (format == XML) {  
  11.             result += "<book_name>" + book.getBookName() + "</book_name>\n";  
  12.             result += "<pages>" + book.getPages() + "</pages>\n";  
  13.             result += "<price>" + book.getPrice() + "</price>\n";  
  14.             result += "<author>" + book.getAuthor() + "</author>\n";  
  15.             result += "<isbn>" + book.getIsbn() + "</isbn>\n";  
  16.         } else if (format == JSON) {  
  17.             result += "{\n";  
  18.             result += "\"book_name\" : \"" + book.getBookName() + "\",\n";  
  19.             result += "\"pages\" : \"" + book.getPages() + "\",\n";  
  20.             result += "\"price\" : \"" + book.getPrice() + "\",\n";  
  21.             result += "\"author\" : \"" + book.getAuthor() + "\",\n";  
  22.             result += "\"isbn\" : \"" + book.getIsbn() + "\",\n";  
  23.             result += "}";  
  24.         }  
  25.         System.out.println("format finished");  
  26.         return result;  
  27.     }  
  28.   
  29. }  

调用代码如下:
[java] view plaincopy
  1. public class Test {  
  2.   
  3.     public static void main(String[] args) throws Exception {  
  4.         Book book = new Book();  
  5.         book.setBookName("Thinking in Java");  
  6.         book.setPages(880);  
  7.         book.setPrice(68);  
  8.         book.setAuthor("Bruce Eckel");  
  9.         book.setIsbn("9787111213826");  
  10.         Formatter formatter = new Formatter();  
  11.         String result = formatter.formatBook(book, Formatter.XML);  
  12.         System.out.println(result);  
  13.         result = formatter.formatBook(book, Formatter.JSON);  
  14.         System.out.println(result);  
  15.     }  
  16.   
  17. }  
再次运行程序,得到了以下结果。


项目经理看到运行结果后开心地说:“太好了,这正是我想要的!” 

可是你这次却没有那么开心,你觉得代码已经有些混乱了,XML格式的逻辑和JSON格式的逻辑混淆在一起,非常不利于阅读,而且如果以后还需要扩展功能也会非常困难。好在传输格式一般也就XML和JSON了,应该不会再有什么扩展了,你这样安慰自己道。

但幻想总会被现实打破,“我最近听说有个YAML格式挺好玩的.......” 项目经理说道。这个时候你已经有想打人的冲动了!!!


很多时候就是这样,在公司里写的代码乱七八糟,质量极差,很大一部分原因就是因为需求变来变去。我们不断在原有代码基础上补充各种后续加入的情况,在一行行新增的if语句下面,我们的代码变得不堪入目。当然,我们作为程序员,对于需求这种东西没有太多的话语权,在这方面我们无能为力。但是我们可以尽量地把程序的架构设计好,让我们写出的代码更具有扩展性,这样就可以应对各种需求变更了。


下面你将要使用23种设计模式中的模板方法来改进以上程序。

首先将Formatter中的代码进行修改,如下所示:

[java] view plaincopy
  1. public abstract class Formatter {  
  2.   
  3.     public String formatBook(Book book, int format) {  
  4.         beforeFormat();  
  5.         String result = formating(book);  
  6.         afterFormat();  
  7.         return result;  
  8.     }  
  9.   
  10.     protected void beforeFormat() {  
  11.         System.out.println("format begins");  
  12.     }  
  13.   
  14.     protected abstract String formating(Book book);  
  15.   
  16.     protected void afterFormat() {  
  17.         System.out.println("format finished");  
  18.     }  
  19.   
  20. }  
你会发现format_book方法只有四步,第一步调用before_format,去打印格式转换前的日志。第二步调用formating,这个方法是个抽象方法,用于处理具体的转换逻辑,因此每一个继承自Formatter的子类都需要重写此方法,来实现各自的转换逻辑。第三步调用after_format,去打印格式转换后的日志。第四步返回result。由于类中存在了抽象方法,我们也就需要把Formatter声明成抽象类。

然后要定义专门的子类来处理每种传输格式的具体逻辑,这样不同传输格式的逻辑可以从一个方法里分离开,明显便于阅读和理解。

定义类XMLFormatter继承自Formatter,里面加入处理XML格式的具体逻辑:

[java] view plaincopy
  1. public class XMLFormatter extends Formatter {  
  2.   
  3.     @Override  
  4.     protected String formating(Book book) {  
  5.         String result = "";  
  6.         result += "<book_name>" + book.getBookName() + "</book_name>\n";  
  7.         result += "<pages>" + book.getPages() + "</pages>\n";  
  8.         result += "<price>" + book.getPrice() + "</price>\n";  
  9.         result += "<author>" + book.getAuthor() + "</author>\n";  
  10.         result += "<isbn>" + book.getIsbn() + "</isbn>\n";  
  11.         return result;  
  12.     }  
  13.   
  14. }  

定义类JSONFormatter继承自Formatter,里面加入处理JSON格式的具体逻辑:
[java] view plaincopy
  1. public class JSONFormatter extends Formatter {  
  2.   
  3.     @Override  
  4.     protected String formating(Book book) {  
  5.         String result = "";  
  6.         result += "{\n";  
  7.         result += "\"book_name\" : \"" + book.getBookName() + "\",\n";  
  8.         result += "\"pages\" : \"" + book.getPages() + "\",\n";  
  9.         result += "\"price\" : \"" + book.getPrice() + "\",\n";  
  10.         result += "\"author\" : \"" + book.getAuthor() + "\",\n";  
  11.         result += "\"isbn\" : \"" + book.getIsbn() + "\",\n";  
  12.         result += "}";  
  13.         return result;  
  14.     }  
  15.   
  16. }  

最后调用代码如下:

[java] view plaincopy
  1. public class Test {  
  2.   
  3.     public static void main(String[] args) throws Exception {  
  4.         Book book = new Book();  
  5.         book.setBookName("Thinking in Java");  
  6.         book.setPages(880);  
  7.         book.setPrice(68);  
  8.         book.setAuthor("Bruce Eckel");  
  9.         book.setIsbn("9787111213826");  
  10.         XMLFormatter xmlFormatter = new XMLFormatter();  
  11.         String result = xmlFormatter.formatBook(book);  
  12.         System.out.println(result);  
  13.         JSONFormatter jsonFormatter = new JSONFormatter();  
  14.         result = jsonFormatter.formatBook(book);  
  15.         System.out.println(result);  
  16.     }  
  17.   
  18. }  

运行之后,你会发现运行结果和修改前代码的运行结果完全相同。但是使用模板方法之后,代码的可读性有了很大的提高,因为处理格式转换的代码都放到了各自的类当中,而不是全部塞进一个方法中。并且在扩展性上也有了很大的提升,比如你开始感兴趣项目经理说的YAML格式了。

定义类YAMLFormatter继承自Formatter,里面加入处理YAML格式的具体逻辑:

[java] view plaincopy
  1. public class YAMLFormatter extends Formatter {  
  2.   
  3.     @Override  
  4.     protected String formating(Book book) {  
  5.         String result = "";  
  6.         result += "book_name: " + book.getBookName() + "\n";  
  7.         result += "pages: " + book.getPages() + "\n";  
  8.         result += "price: " + book.getPrice() + "\n";  
  9.         result += "author: " + book.getAuthor() + "\n";  
  10.         result += "isbn: " + book.getIsbn() + "\n";  
  11.         return result;  
  12.     }  
  13.   
  14. }  

调用代码只需要加入:
[java] view plaincopy
  1. YAMLFormatter yamlFormatter = new YAMLFormatter();  
  2. String result = yamlFormatter.formatBook(book);  
  3. System.out.println(result);  

好了,令人头疼的YAML格式就这样被支持了,只需要在调用的时候决定是实例化XMLFormatter,JSONFormatter还是YAMLFormatter,就可以按照相应的规格进行格式转换了。而且整体的代码很有条理,看起来也很舒心。这个时候,你会轻松地向项目经理调侃一句,还有需要支持的格式吗?


模板方法: 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。



二、Java设计模式透析之 —— 单例(Singleton)

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/8860649

写软件的时候经常需要用到打印日志功能,可以帮助你调试和定位问题,项目上线后还可以帮助你分析数据。但是Java原生带有的System.out.println()方法却很少在真正的项目开发中使用,甚至像findbugs等代码检查工具还会认为使用System.out.println()是一个bug。

为什么作为Java新手神器的System.out.println(),到了真正项目开发当中会被唾弃呢?其实只要细细分析,你就会发现它的很多弊端。比如不可控制,所有的日志都会在项目上线后照常打印,从而降低运行效率;又或者不能将日志记录到本地文件,一旦打印被清除,日志将再也找不回来;再或者打印的内容没有Tag区分,你将很难辨别这一行日志是在哪个类里打印的。

你的leader也不是傻瓜,用System.out.println()的各项弊端他也清清楚楚,因此他今天给你的任务就是制作一个日志工具类,来提供更好的日志功能。不过你的leader人还不错,并没让你一开始就实现一个具备各项功能的牛逼日志工具类,只需要一个能够控制打印级别的日志工具就好。

这个需求对你来说并不难,你立刻就开始动手编写了,并很快完成了第一个版本:

[java] view plaincopy
  1. public class LogUtil {  
  2.   
  3.     public final int DEGUB = 0;  
  4.   
  5.     public final int INFO = 1;  
  6.   
  7.     public final int ERROR = 2;  
  8.   
  9.     public final int NOTHING = 3;  
  10.   
  11.     public int level = DEGUB;  
  12.   
  13.     public void debug(String msg) {  
  14.         if (DEGUB >= level) {  
  15.             System.out.println(msg);  
  16.         }  
  17.     }  
  18.   
  19.     public void info(String msg) {  
  20.         if (INFO >= level) {  
  21.             System.out.println(msg);  
  22.         }  
  23.     }  
  24.   
  25.     public void error(String msg) {  
  26.         if (ERROR >= level) {  
  27.             System.out.println(msg);  
  28.         }  
  29.     }  
  30.   
  31. }  
通过这个类来打印日志,只需要控制level的级别,就可以自由地控制打印的内容。比如现在项目处于开发阶段,就将level设置为DEBUG,这样所有的日志信息都会被打印。而项目如果上线了,可以把level设置为INFO,这样就只能看到INFO及以上级别的日志打印。如果你只想看到错误日志,就可以把level设置为ERROR。而如果你开发的项目是客户端版本,不想让任何日志打印出来,可以将level设置为NOTHING。打印的时候只需要调用:
[java] view plaincopy
  1. new LogUtil().debug("Hello World");  

你迫不及待地将这个工具介绍给你的leader,你的leader听完你的介绍后说:“好样的,今后大伙都用你写的这个工具来打印日志了!”

可是没过多久,你的leader找到你来反馈问题了。他说虽然这个工具好用,可是打印这种事情是不区分对象的,这里每次需要打印日志的时候都需要new出一个新的LogUtil,太占用内存了,希望你可以将这个工具改成用单例模式实现。

你认为你的leader说的很有道理,而且你也正想趁这个机会练习使用一下设计模式,于是你写出了如下的代码:

[java] view plaincopy
  1. public class LogUtil {  
  2.   
  3.     private static LogUtil sLogUtil;  
  4.   
  5.     public final int DEGUB = 0;  
  6.   
  7.     public final int INFO = 1;  
  8.   
  9.     public final int ERROR = 2;  
  10.   
  11.     public final int NOTHING = 3;  
  12.   
  13.     public int level = DEGUB;  
  14.   
  15.     private LogUtil() {  
  16.     }  
  17.   
  18.     public static LogUtil getInstance() {  
  19.         if (sLogUtil == null) {  
  20.             sLogUtil = new LogUtil();  
  21.         }  
  22.         return sLogUtil;  
  23.     }  
  24.   
  25.     public void debug(String msg) {  
  26.         if (DEGUB >= level) {  
  27.             System.out.println(msg);  
  28.         }  
  29.     }  
  30.   
  31.     public void info(String msg) {  
  32.         if (INFO >= level) {  
  33.             System.out.println(msg);  
  34.         }  
  35.     }  
  36.   
  37.     public void error(String msg) {  
  38.         if (ERROR >= level) {  
  39.             System.out.println(msg);  
  40.         }  
  41.     }  
  42.   
  43. }  
首先将LogUtil的构造函数私有化,这样就无法使用new关键字来创建LogUtil的实例了。然后使用一个sLogUtil私有静态变量来保存实例,并提供一个公有的getInstance方法用于获取LogUtil的实例,在这个方法里面判断如果sLogUtil为空,就new出一个新的LogUtil实例,否则就直接返回sLogUtil。这样就可以保证内存当中只会存在一个LogUtil的实例了。单例模式完工!这时打印日志的代码需要改成如下方式:
[java] view plaincopy
  1. LogUtil.getInstance().debug("Hello World");  
你将这个版本展示给你的leader瞧,他看后笑了笑,说:“虽然这看似是实现了单例模式,可是还存在着bug的哦。

你满腹狐疑,单例模式不都是这样实现的吗?还会有什么bug呢? 

你的leader提示你,使用单例模式就是为了让这个类在内存中只能有一个实例的,可是你有考虑到在多线程中打印日志的情况吗?如下面代码所示:

[java] view plaincopy
  1. public static LogUtil getInstance() {  
  2.     if (sLogUtil == null) {  
  3.         sLogUtil = new LogUtil();  
  4.     }  
  5.     return sLogUtil;  
  6. }  
如果现在有两个线程同时在执行getInstance方法,第一个线程刚执行完第2行,还没执行第3行,这个时候第二个线程执行到了第2行,它会发现sLogUtil还是null,于是进入到了if判断里面。这样你的单例模式就失败了,因为创建了两个不同的实例。

你恍然大悟,不过你的思维非常快,立刻就想到了解决办法,只需要给方法加上同步锁就可以了,代码如下:

[java] view plaincopy
  1. public synchronized static LogUtil getInstance() {  
  2.     if (sLogUtil == null) {  
  3.         sLogUtil = new LogUtil();  
  4.     }  
  5.     return sLogUtil;  
  6. }  
这样,同一时刻只允许有一个线程在执行getInstance里面的代码,这样就有效地解决了上面会创建两个实例的情况。

你的leader看了你的新代码后说:“恩,不错。这确实解决了有可能创建两个实例的情况,但是这段代码还是有问题的。”

你紧张了起来,怎么还会有问题啊?

你的leader笑笑:“不用紧张,这次不是bug,只是性能上可以优化一些。你看一下,如果是在getInstance方法上加了一个synchronized,那么我每次去执行getInstace方法的时候都会受到同步锁的影响,这样运行的效率会降低,其实只需要在第一次创建LogUtil实例的时候加上同步锁就好了。我来教你一下怎么把它优化的更好。”

首先将synchronized关键字从方法声明中去除,把它加入到方法体当中:

[java] view plaincopy
  1. public static LogUtil getInstance() {  
  2.     synchronized (LogUtil.class) {  
  3.         if (sLogUtil == null) {  
  4.             sLogUtil = new LogUtil();  
  5.         }  
  6.         return sLogUtil;  
  7.     }  
  8. }  

这样效果是和直接在方法上加synchronized完全一致的。然后在synchronized的外面再加一层判断,如下所示:

[java] view plaincopy
  1. public static LogUtil getInstance() {  
  2.     if (sLogUtil == null) {  
  3.         synchronized (LogUtil.class) {  
  4.             if (sLogUtil == null) {  
  5.                 sLogUtil = new LogUtil();  
  6.             }  
  7.         }  
  8.     }  
  9.     return sLogUtil;  
  10. }  
代码改成这样之后,只有在sLogUtil还没被初始化的时候才会进入到第3行,然后加上同步锁。等sLogUtil一但初始化完成了,就再也走不到第3行了,这样执行getInstance方法也不会再受到同步锁的影响,效率上会有一定的提升。

你情不自禁赞叹到,这方法真巧妙啊,能想得出来实在是太聪明了。

你的leader马上谦虚起来:“这种方法叫做双重锁定(Double-Check Locking),可不是我想出来的,更多的资料你可以在网上查一查。”

单例:保证一个类仅有一个实例,并提供一个访问它的全局访问点。


三、Java设计模式透析之 —— 策略(Strategy)

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/8986285

今天你的leader兴致冲冲地找到你,希望你可以帮他一个小忙,他现在急着要去开会。要帮什么忙呢?你很好奇。

他对你说,当前你们项目的数据库中有一张用户信息表,里面存放了很用户的数据,现在需要完成一个选择性查询用户信息的功能。他说会传递给你一个包含许多用户名的数组,你需要根据这些用户名把他们相应的数据都给查出来。

这个功能很简单的嘛,你爽快地答应了。由于你们项目使用的是MySQL数据库,你很快地写出了如下代码:

[java] view plaincopy
  1. public class QueryUtil {  
  2.   
  3.     public void findUserInfo(String[] usernames) throws Exception {  
  4.         Class.forName("com.mysql.jdbc.Driver");  
  5.         Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test""root",  
  6.                 "123456");  
  7.         Statement stat = conn.createStatement();  
  8.         StringBuilder sql = new StringBuilder("select * from user_info where ");  
  9.         for (String user : usernames) {  
  10.             sql.append("username = '");  
  11.             sql.append(user);  
  12.             sql.append("' or ");  
  13.         }  
  14.         System.out.println(sql);  
  15.         ResultSet resultSet = stat.executeQuery(sql.toString());  
  16.         while (resultSet.next()) {  
  17.             // 处理从数据库读出来的数据  
  18.         }  
  19.         // 后面应将读到的数据组装成对象返回,这里略去。  
  20.     }  
  21. }  
这里根据传入的用户名数组拼装了SQL语句,然后去数据库中查找相应的行。为了方面调试,你还将拼装好的SQL语句打印了出来。

然后,你写了如下代码来测试这个方法:

[java] view plaincopy
  1. public class Test {  
  2.   
  3.     public static void main(String[] args) throws Exception {  
  4.         QueryUtil query = new QueryUtil();  
  5.         query.findUserInfo(new String[] { "Tom""Jim""Anna" });  
  6.     }  
  7.   
  8. }  
现在运行一下测试代码,你发现程序出错了。于是你立刻去检查了一下打印的SQL语句,果然发现了问题。
[sql] view plaincopy
  1. select * from user_info where username = 'Tom' or username = 'Jim' or username = 'Anna' or   
拼装出来的SQL语句在最后多加了一个 or 关键字!因为for循环执行到最后一条数据时不应该再加上or,可是代码很笨地给最后一条数据也加了or关键字,导致SQL语句语法出错了。

这可怎么办呢?

有了!你灵光一闪,想出了一个解决办法。等SQL语句拼装完成后,把最后一个or删除掉不就好了么。于是你将代码改成如下所示:

[java] view plaincopy
  1. public class QueryUtil {  
  2.   
  3.     public void findUserInfo(String[] usernames) throws Exception {  
  4.         Class.forName("com.mysql.jdbc.Driver");  
  5.         Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test""root",  
  6.                 "123456");  
  7.         Statement stat = conn.createStatement();  
  8.         StringBuilder sql = new StringBuilder("select * from user_info where ");  
  9.         for (String user : usernames) {  
  10.             sql.append("username = '");  
  11.             sql.append(user);  
  12.             sql.append("' or ");  
  13.         }  
  14.         sql.delete(sql.length() - " or ".length(), sql.length());  
  15.         System.out.println(sql);  
  16.         ResultSet resultSet = stat.executeQuery(sql.toString());  
  17.         while (resultSet.next()) {  
  18.             // 处理从数据库读出来的数据  
  19.         }  
  20.         // 后面应将读到的数据组装成对象返回,这里略去。  
  21.     }  
  22. }  

使用StringBuilder的delete方法,把最后多余的一个or删除掉了,这样再运行测试代码,一切就正常了,打印的SQL语句如下所示:

[sql] view plaincopy
  1. select * from user_info where username = 'Tom' or username = 'Jim' or username = 'Anna'  
好了,完工!你自信满满。

你的leader开完会后,过来看了下你的成果。总体来说,他还挺满意,但对于你使用的SQL语句拼装算法,他总是感觉有些不对劲,可是又说不上哪里不好。于是他告诉了你另一种拼装SQL语句的算法,让你加入到代码中,但是之前的那种算法也不要删除,先保留着再说,然后他又很忙似的跑开了。于是,你把他刚刚教你的算法加了进去,代码如下所示:

[java] view plaincopy
  1. public class QueryUtil {  
  2.   
  3.     public void findUserInfo(String[] usernames, int strategy) throws Exception {  
  4.         Class.forName("com.mysql.jdbc.Driver");  
  5.         Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test""root",  
  6.                 "123456");  
  7.         Statement stat = conn.createStatement();  
  8.         StringBuilder sql = new StringBuilder("select * from user_info where ");  
  9.         if (strategy == 1) {  
  10.             for (String user : usernames) {  
  11.                 sql.append("username = '");  
  12.                 sql.append(user);  
  13.                 sql.append("' or ");  
  14.             }  
  15.             sql.delete(sql.length() - " or ".length(), sql.length());  
  16.         } else if (strategy == 2) {  
  17.             boolean needOr = false;  
  18.             for (String user : usernames) {  
  19.                 if (needOr) {  
  20.                     sql.append(" or ");  
  21.                 }  
  22.                 sql.append("username = '");  
  23.                 sql.append(user);  
  24.                 sql.append("'");  
  25.                 needOr = true;  
  26.             }  
  27.         }  
  28.         System.out.println(sql);  
  29.         ResultSet resultSet = stat.executeQuery(sql.toString());  
  30.         while (resultSet.next()) {  
  31.             // 处理从数据库读出来的数据  
  32.         }  
  33.         // 后面应将读到的数据组装成对象返回,这里略去。  
  34.     }  
  35. }  
可以看到,你leader教你的拼装算法,使用了一个布尔变量来控制是否需要加个or这个关键字,第一次执行for循环的时候因为该布尔值为false,所以不会加上or,在循环的最后将布尔值赋值为true,这样以后循环每次都会在头部加上一个or关键字,由于使用了头部添加or的方法,所以不用再担心SQL语句的尾部会多出一个or来。然后你为了将两个算法都保留,在findUserInfo方法上加了一个参数,strategy值为1表示使用第一种算法,strategy值为2表示使用第二种算法。

这样测试代码也需要改成如下方式:

[java] view plaincopy
  1. public class Test {  
  2.   
  3.     public static void main(String[] args) throws Exception {  
  4.         QueryUtil query = new QueryUtil();  
  5.         query.findUserInfo(new String[] { "Tom""Jim""Anna" }, 2);  
  6.     }  
  7.   
  8. }  
这里你通过参数指明了使用第二种算法来拼装SQL语句,打印的结果和使用第一种算法是完全相同的。
你立刻把你的leader从百忙之中拖了过来,让他检验一下你当前的成果,可是他还是一如既往的挑剔。

“你这样写的话,findUserInfo这个方法的逻辑就太复杂了,非常不利于阅读,也不利于将来的扩展,如果我还有第三第四种算法想加进去,这个方法还能看吗?”  你的leader指点你,遇到这种情况,就要使用策略模式来解决,策略模式的核心思想就是把算法提取出来放到一个独立的对象中

为了指点你,他不顾自己的百忙,开始教你如何使用策略模式进行优化。

首先定义一个策略接口:

[java] view plaincopy
  1. public interface Strategy {  
  2.   
  3.     String getSQL(String[] usernames);  
  4.   
  5. }  
然后定义两个子类都实现了上述接口,并将两种拼装SQL语句的算法分别加入两个子类中:
[java] view plaincopy
  1. public class Strategy1 implements Strategy {  
  2.   
  3.     @Override  
  4.     public String getSQL(String[] usernames) {  
  5.         StringBuilder sql = new StringBuilder("select * from user_info where ");  
  6.         for (String user : usernames) {  
  7.             sql.append("username = '");  
  8.             sql.append(user);  
  9.             sql.append("' or ");  
  10.         }  
  11.         sql.delete(sql.length() - " or ".length(), sql.length());  
  12.         return sql.toString();  
  13.     }  
  14.   
  15. }  
[java] view plaincopy
  1. public class Strategy2 implements Strategy {  
  2.   
  3.     @Override  
  4.     public String getSQL(String[] usernames) {  
  5.         StringBuilder sql = new StringBuilder("select * from user_info where ");  
  6.         boolean needOr = false;  
  7.         for (String user : usernames) {  
  8.             if (needOr) {  
  9.                 sql.append(" or ");  
  10.             }  
  11.             sql.append("username = '");  
  12.             sql.append(user);  
  13.             sql.append("'");  
  14.             needOr = true;  
  15.         }  
  16.         return sql.toString();  
  17.     }  
  18.   
  19. }  
然后把QueryUtil中findUserInfo方法的第二个参数改成Strategy对象,这样只需要调用Strategy的getSQL方法就可以获得拼装好的SQL语句,代码如下所示:
[java] view plaincopy
  1. public class QueryUtil {  
  2.   
  3.     public void findUserInfo(String[] usernames, Strategy strategy) throws Exception {  
  4.         Class.forName("com.mysql.jdbc.Driver");  
  5.         Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test""root",  
  6.                 "123456");  
  7.         Statement stat = conn.createStatement();  
  8.         String sql = strategy.getSQL(usernames);  
  9.         System.out.println(sql);  
  10.         ResultSet resultSet = stat.executeQuery(sql);  
  11.         while (resultSet.next()) {  
  12.             // 处理从数据库读出来的数据  
  13.         }  
  14.         // 后面应将读到的数据组装成对象返回,这里略去。  
  15.     }  
  16. }  
最后,测试代码在调用findUserInfo方法时,只需要显示地指明需要使用哪一个策略对象就可以了:
[java] view plaincopy
  1. public class Test {  
  2.   
  3.     public static void main(String[] args) throws Exception {  
  4.         QueryUtil query = new QueryUtil();  
  5.         query.findUserInfo(new String[] { "Tom""Jim""Anna" }, new Strategy1());  
  6.         query.findUserInfo(new String[] { "Jac""Joe""Rose" }, new Strategy2());  
  7.     }  
  8.   
  9. }  
打印出的SQL语句丝毫不出预料,如下所示:
[sql] view plaincopy
  1. select * from user_info where username = 'Tom' or username = 'Jim' or username = 'Anna'  
  2. select * from user_info where username = 'Jac' or username = 'Joe' or username = 'Rose'  

使用策略模式修改之后,代码的可读性和扩展性都有了很大的提高,即使以后还需要添加新的算法,你也是手到擒来了!

策略:它定义了算法家庭,分别封装起来。让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。



四、Java设计模式透析之 —— 组合(Composite)

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9153753

听说你们公司最近新推出了一款电子书阅读应用,市场反应很不错,应用里还有图书商城,用户可以在其中随意选购自己喜欢的书籍。你们公司也是对此项目高度重视,加大了投入力度,决定给此应用再增加点功能。

好吧,你也知道你是逃不过此劫了,没过多久你的leader就找到了你。他告诉你目前的应用对每本书的浏览量和销售量做了统计,但现在想增加对每个书籍分类的浏览量和销售量以及所有书籍总的浏览量和销售量做统计的功能,希望你可以来完成这项功能。

领导安排的工作当然是推脱不掉的,你只能硬着头皮上了,不过好在这个功能看起来也不怎么复杂。

你比较喜欢看小说,那么就从小说类的统计功能开始做起吧。首先通过getAllNovels方法可以获取到所有的小说名,然后将小说名传入getBrowseCount方法可以得到该书的浏览量,将小说名传入getSaleCount方法可以得到该书的销售量。你目前只有这几个已知的API可以使用,那么开始动手吧!

[java] view plaincopy
  1. public int getNovelsBrowseCount() {  
  2.     int browseCount = 0;  
  3.     List<String> allNovels = getAllNovels();  
  4.     for (String novel : allNovels) {  
  5.         browseCount += getBrowseCount(novel);  
  6.     }  
  7.     return browseCount;  
  8. }  
  9.   
  10. public int getNovelsSaleCount() {  
  11.     int saleCount = 0;  
  12.     List<String> allNovels = getAllNovels();  
  13.     for (String novel : allNovels) {  
  14.         saleCount += getSaleCount(novel);  
  15.     }  
  16.     return saleCount;  
  17. }  
很快你就写下了以上两个方法,这两个方法都是通过获取到所有的小说名,然后一一计算每本小说的浏览量和销售量,最后将结果相加得到总量。

小说类的统计就完成了,然后你开始做计算机类书籍的统计功能,代码如下所示:

[java] view plaincopy
  1. public int getComputerBooksBrowseCount() {  
  2.     int browseCount = 0;  
  3.     List<String> allComputerBooks = getAllComputerBooks();  
  4.     for (String computerBook : allComputerBooks) {  
  5.         browseCount += getBrowseCount(computerBook);  
  6.     }  
  7.     return browseCount;  
  8. }  
  9.   
  10. public int getComputerBooksSaleCount() {  
  11.     int saleCount = 0;  
  12.     List<String> allComputerBooks = getAllComputerBooks();  
  13.     for (String computerBook : allComputerBooks) {  
  14.         saleCount += getSaleCount(computerBook);  
  15.     }  
  16.     return saleCount;  
  17. }  
除了使用了getAllComputerBooks方法获取到所有的计算机类书名,其它的代码基本和小说统计中的是一样的。

现在你才完成了两类书籍的统计功能,后面还有医学类、自然类、历史类、法律类、政治类、哲学类、旅游类、美食类等等等等书籍。你突然意识到了一些问题的严重性,工作量大倒还不算什么,但再这么写下去,你的方法就要爆炸了,这么多的方法让人看都看不过来,别提怎么使用了。

这个时候你只好向你的leader求助了,跟他说明了你的困惑。只见你的leader思考了片刻,然后自信地告诉你,使用组合模式不仅可以轻松消除你的困惑,还能出色地完成功能。

他立刻向你秀起了编码操作,首先定义一个Statistics接口,里面有两个待实现方法:

[java] view plaincopy
  1. public interface Statistics {  
  2.   
  3.     int getBrowseCount();  
  4.       
  5.     int getSalesCount();  
  6.   
  7. }  
然后定义一个用于统计小说类书籍的NovelStatistics类,实现接口中定义的两个方法:
[java] view plaincopy
  1. public class NovelStatistics implements Statistics {  
  2.   
  3.     @Override  
  4.     public int getBrowseCount() {  
  5.         int browseCount = 0;  
  6.         List<String> allNovels = getAllNovels();  
  7.         for (String novel : allNovels) {  
  8.             browseCount += getBrowseCount(novel);  
  9.         }  
  10.         return browseCount;  
  11.     }  
  12.   
  13.     @Override  
  14.     public int getSalesCount() {  
  15.         int saleCount = 0;  
  16.         List<String> allNovels = getAllNovels();  
  17.         for (String novel : allNovels) {  
  18.             saleCount += getSaleCount(novel);  
  19.         }  
  20.         return saleCount;  
  21.     }  
  22.   
  23. }  
在这两个方法中分别统计了小说类书籍的浏览量和销售量。那么同样的方法,你的leader又定义了一个ComputerBookStatistics类用于统计计算机类书籍的浏览量和销售量:
[java] view plaincopy
  1. public class ComputerBookStatistics implements Statistics {  
  2.   
  3.     @Override  
  4.     public int getBrowseCount() {  
  5.         int browseCount = 0;  
  6.         List<String> allComputerBooks = getAllComputerBooks();  
  7.         for (String computerBook : allComputerBooks) {  
  8.             browseCount += getBrowseCount(computerBook);  
  9.         }  
  10.         return browseCount;  
  11.     }  
  12.   
  13.     @Override  
  14.     public int getSalesCount() {  
  15.         int saleCount = 0;  
  16.         List<String> allComputerBooks = getAllComputerBooks();  
  17.         for (String computerBook : allComputerBooks) {  
  18.             saleCount += getSaleCount(computerBook);  
  19.         }  
  20.         return saleCount;  
  21.     }  
  22.   
  23. }  
这样将具体的统计实现分散在各个类中,就不会再出现你刚刚那种方法爆炸的情况了。不过这还没开始真正使用组合模式呢,好戏还在后头,你的leader吹嘘道。

再定义一个MedicalBookStatistics类实现了Statistics接口,用于统计医学类书籍的浏览量和销售量,代码如下如示:
[java] view plaincopy
  1. public class MedicalBookStatistics implements Statistics {  
  2.   
  3.     @Override  
  4.     public int getBrowseCount() {  
  5.         int browseCount = 0;  
  6.         List<String> allMedicalBooks = getAllMedicalBooks();  
  7.         for (String medicalBook : allMedicalBooks) {  
  8.             browseCount += getBrowseCount(medicalBook);  
  9.         }  
  10.         return browseCount;  
  11.     }  
  12.   
  13.     @Override  
  14.     public int getSalesCount() {  
  15.         int saleCount = 0;  
  16.         List<String> allMedicalBooks = getAllMedicalBooks();  
  17.         for (String medicalBook : allMedicalBooks) {  
  18.             saleCount += getSaleCount(medicalBook);  
  19.         }  
  20.         return saleCount;  
  21.     }  
  22.   
  23. }  
不知道你发现了没有,计算机类书籍和医学类书籍其实都算是科技类书籍,它们是可以组合在一起的。这个时候你的leader定义了一个TechnicalStatistics类用于对科技这一组合类书籍进行统计:
[java] view plaincopy
  1. public class TechnicalStatistics implements Statistics {  
  2.       
  3.     private List<Statistics> statistics = new ArrayList<Statistics>();  
  4.       
  5.     public TechnicalStatistics() {  
  6.         statistics.add(new ComputerBookStatistics());  
  7.         statistics.add(new MedicalBookStatistics());  
  8.     }  
  9.   
  10.     @Override  
  11.     public int getBrowseCount() {  
  12.         int browseCount = 0;  
  13.         for (Statistics s : statistics) {  
  14.             browseCount += s.getBrowseCount();  
  15.         }  
  16.         return browseCount;  
  17.     }  
  18.   
  19.     @Override  
  20.     public int getSalesCount() {  
  21.         int saleCount = 0;  
  22.         for (Statistics s : statistics) {  
  23.             saleCount += s.getBrowseCount();  
  24.         }  
  25.         return saleCount;  
  26.     }  
  27.   
  28. }  
可以看到,由于这个类是组合类,和前面几个类还是有不少区别的。首先TechnicalStatistics中有一个构造函数,在构造函数中将计算机类书籍和医学类书籍作为子分类添加到statistics列表当中,然后分别在getBrowseCount和getSalesCount方法中遍历所有的子分类,计算出它们各自的浏览量和销售量,然后相加得到总额返回。

组合模式的扩展性非常好,没有各种条条框框,想怎么组合就怎么组合,比如所有书籍就是由各个分类组合而来的,你的leader马上又向你炫耀了统计所有书籍的浏览量和销售量的办法。

定义一个AllStatistics类实现了Statistics接口,具体代码如下所示:

[java] view plaincopy
  1. public class AllStatistics implements Statistics {  
  2.   
  3.     private List<Statistics> statistics = new ArrayList<Statistics>();  
  4.   
  5.     public AllStatistics() {  
  6.         statistics.add(new NovelStatistics());  
  7.         statistics.add(new TechnicalStatistics());  
  8.     }  
  9.   
  10.     @Override  
  11.     public int getBrowseCount() {  
  12.         int browseCount = 0;  
  13.         for (Statistics s : statistics) {  
  14.             browseCount += s.getBrowseCount();  
  15.         }  
  16.         return browseCount;  
  17.     }  
  18.   
  19.     @Override  
  20.     public int getSalesCount() {  
  21.         int saleCount = 0;  
  22.         for (Statistics s : statistics) {  
  23.             saleCount += s.getBrowseCount();  
  24.         }  
  25.         return saleCount;  
  26.     }  
  27.   
  28. }  
在AllStatistics的构造函数中将小说类书籍和科技类书籍作为子分类添加到了statistics列表当中,目前你也就只写好了这几个分类。然后使用同样的方法在getBrowseCount和getSalesCount方法中统计出所有书籍的浏览量和销售量。

当前组合结构的示意图如下:

            

现在你就可以非常方便的得到任何分类书籍的浏览量和销售量了,比如说获取科技类书籍的浏览量,你只需要调用:

[java] view plaincopy
  1. new TechnicalStatistics().getBrowseCount();  
而获取所有书籍的总销量,你只需要调用:
[java] view plaincopy
  1. new AllStatistics().getSalesCount();  

当然你后面还可以对这个组合结构随意地改变,添加各种子分类书籍,而且子分类的层次结构可以任意深,正如前面所说,组合模式的扩展性非常好。

你的leader告诉你,目前他写的这份代码重复度比较高,其实还可以好好优化一下的,把冗余代码都去除掉。当然这个任务就交给你来做了,你的leader可是大忙人,早就一溜烟跑开了。

组合:将对象组合成树形结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9400141

今天一大早,你的leader就匆匆忙忙跑过来找到你:“快,快,紧急任务!最近ChinaJoy马上就要开始了,老板要求提供一种直观的方式,可以查看到我们新上线的游戏中每个服的在线人数。”

你看了看日期,不是吧!这哪里是马上要开始了,分明是已经开始了!这怎么可能来得及呢?

“没关系的。”你的leader安慰你道:“功能其实很简单的,接口都已经提供好了,你只需要调用一下就行了。”

好吧,你勉为其难地接受了,对于这种突如其来的新需求,你早已习惯。

你的leader向你具体描述了一下需求,你们的游戏目前有三个服,一服已经开放一段时间了,二服和三服都是新开的服。设计的接口非常轻便,你只需要调用Utility.getOnlinePlayerCount(int),传入每个服对应的数值就可以获取到相应服在线玩家的数量了,如一服传入1,二服传入2,三服则传入3。如果你传入了一个不存在的服,则会返回-1。然后你只要将得到的数据拼装成XML就好,具体的显示功能由你的leader来完成。

好吧,听起来功能并不是很复杂,如果现在就开始动工好像还来得及,于是你马上敲起了代码。

首先定义一个用于统计在线人数的接口PlayerCount,代码如下:

[java] view plaincopy
  1. public interface PlayerCount {  
  2.   
  3.     String getServerName();  
  4.   
  5.     int getPlayerCount();  
  6.   
  7. }  
接着定义三个统计类实现了PlayerCount接口,分别对应了三个不同的服,如下所示:
[java] view plaincopy
  1. public class ServerOne implements PlayerCount {  
  2.   
  3.     @Override  
  4.     public String getServerName() {  
  5.         return "一服";  
  6.     }  
  7.   
  8.     @Override  
  9.     public int getPlayerCount() {  
  10.         return Utility.getOnlinePlayerCount(1);  
  11.     }  
  12.   
  13. }  
[java] view plaincopy
  1. public class ServerTwo implements PlayerCount {  
  2.   
  3.     @Override  
  4.     public String getServerName() {  
  5.         return "二服";  
  6.     }  
  7.   
  8.     @Override  
  9.     public int getPlayerCount() {  
  10.         return Utility.getOnlinePlayerCount(2);  
  11.     }  
  12.   
  13. }  
[java] view plaincopy
  1. public class ServerThree implements PlayerCount {  
  2.   
  3.     @Override  
  4.     public String getServerName() {  
  5.         return "三服";  
  6.     }  
  7.   
  8.     @Override  
  9.     public int getPlayerCount() {  
  10.         return Utility.getOnlinePlayerCount(3);  
  11.     }  
  12.   
  13. }  
然后定义一个XMLBuilder类,用于将各服的数据封装成XML格式,代码如下:
[java] view plaincopy
  1. public class XMLBuilder {  
  2.   
  3.     public static String buildXML(PlayerCount player) {  
  4.         StringBuilder builder = new StringBuilder();  
  5.         builder.append("<root>");  
  6.         builder.append("<server>").append(player.getServerName()).append("</server>");  
  7.         builder.append("<player_count").append(player.getPlayerCount()).append("</player_count>");  
  8.         builder.append("</root>");  
  9.         return builder.toString();  
  10.     }  
  11.   
  12. }  
这样的话,所有代码就完工了,如果你想查看一服在线玩家数只需要调用:
[java] view plaincopy
  1. XMLBuilder.buildXML(new ServerOne());  
查看二服在线玩家数只需要调用:
[java] view plaincopy
  1. XMLBuilder.buildXML(new ServerTwo());  
查看三服在线玩家数只需要调用:
[java] view plaincopy
  1. XMLBuilder.buildXML(new ServerThree());  

咦?你发现查看一服在线玩家数的时候,返回值永远是-1,查看二服和三服都很正常。

你只好把你的leader叫了过来:“我感觉我写的代码没有问题,但是查询一服在线玩家数总是返回-1,为什么会这样呢?”

“哎呀!”你的leader猛然想起,“这是我的问题,前面没跟你解释清楚。由于我们的一服已经开放一段时间了,查询在线玩家数量的功能早就有了,使用的是ServerFirst这个类。当时写Utility.getOnlinePlayerCount()这个方法主要是为了针对新开的二服和三服,就没把一服的查询功能再重复做一遍。”

听到你的leader这么说,你顿时松了一口气:“那你修改一下Utility.getOnlinePlayerCount()就好了,应该没我什么事了吧?”

“晤。。。本来应该是这样的。。。可是,Utility和ServerFirst这两个类都已经被打到Jar包里了,没法修改啊。。。”你的leader有些为难。

“什么?这不是坑爹吗,难道要我把接口给改了?”你已经泪流满面了。

“这倒不用,这种情况下可以使用适配器模式,这个模式就是为了解决接口之间不兼容的问题而出现的。”

其实适配器模式的使用非常简单,核心思想就是只要能让两个互不兼容的接口能正常对接就行了。上面的代码中,XMLBuilder中使用PlayerCount这个接口来拼装XML,而ServerFirst并没有实现PlayerCount这个接口,这个时候就需要一个适配器类来为XMLBuilder和ServerFirst之间搭起一座桥梁,毫无疑问,ServerOne就将充当适配器类的角色。修改ServerOne的代码,如下所示:

[java] view plaincopy
  1. public class ServerOne implements PlayerCount {  
  2.       
  3.     private ServerFirst mServerFirst;  
  4.       
  5.     public ServerOne() {  
  6.         mServerFirst = new ServerFirst();  
  7.     }  
  8.   
  9.     @Override  
  10.     public String getServerName() {  
  11.         return "一服";  
  12.     }  
  13.   
  14.     @Override  
  15.     public int getPlayerCount() {  
  16.         return mServerFirst.getOnlinePlayerCount();  
  17.     }  
  18.   
  19. }  
这样通过ServerOne的适配,XMLBuilder和ServerFirst之间就成功完成对接了!使用的时候我们甚至无需知道有ServerFirst这个类,只需要正常创建ServerOne的实例就行了。

需要值得注意的一点是,适配器模式不并是那种会让架构变得更合理的模式,更多的时候它只是充当救火队员的角色,帮助解决由于前期架构设计不合理导致的接口不匹配的问题。更好的做法是在设计的时候就尽量把以后可能出现的情况多考虑一些,在这个问题上不要向你的leader学习。

适配器:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。


五、Java设计模式透析之 —— 适配器(Adapter)


0 0
原创粉丝点击