我们知道,设计模式很有用,学好设计模式不但能让你写出更简洁,优雅的代码,还能使得代码的结构更清晰,也更有利于扩展
当然设计模式也不是万能的,一成不变的.设计模式只是前人总结出来的一种经验,一种特定问题的解决方法,不能看作是死的东西
不一定非要生搬硬套,非得按照设计模式书上来来,只要我们写的代码符合一定的一些原则,一样可以看作是自己的模式.但是前人
总结出来的东西必须非常值得我们学的.本系列23种设计模式会用最简单的例子,会用最让人明白的语言讲清楚里面的思想即可,
过多的细节不会涉及,本系列所有的代码有的是自己写的,有的例子是看书得来的或者看
其它博客学习来的,只供参考学习之用,所有的代码全部在github上,稍后会列出地址,供下载学习参考.
在讲设计模式之前,先讲一下代码编写的一些基本的原则
一 单一职责原则
优化代码的第一步:就是类或者方法的职责只做一件事,比如一个作家需要出一本书,首先需要写完一本书,然后再交给出版社出版.代码如下
1 //作家 2 public class Author { 3 4 //写一本书 5 public void writeBook(String bookName) { 6 System.out.println("在写一本" + bookName + "书"); 7 } 8 9 //出版一本书 10 public void publishBook(String bookName) { 11 System.out.println("出版了一本" + bookName + "书"); 12 } 13 14 }
上面的例子有点简单,但是可以说明问题就行了, 作家类 Author 的本职应该是只负责写书,至于出书交给出版社就行了,此例子中,作家Authon类不但负责写书,
还负责出书,这就造成了功能不单一的情况.改成下面这样,作家只负责写书,出书的工作就交给出版社.代码如下 :
1 //作家 2 public class Author { 3 private String bookName; 4 5 //写一本书 6 public String writeBook() { 7 System.out.println("在写一本" + bookName + "书"); 8 return bookName; 9 } 10 11 }
1 //出版社 2 public class Publisher { 3 4 public void publishBook(String bookName){ 5 System.out.println("出版了一本" + bookName + "的书"); 6 } 7 8 }
测试类:
1 public class TestAuthor { 2 public static void main(String[] args){ 3 //作家 4 Author author = new Author(); 5 6 //出版社 7 Publisher publisher = new Publisher(); 8 9 //写了一本书 10 String bookName = author.writeBook(); 11 12 //出版社出版这本书 13 publisher.publishBook(bookName); 14 } 15 }
通过以上功能拆解,将类的职责划分清楚,功能单一了.
二 开闭原则
让程序更稳定,更灵活:对扩展开放,对修改关闭,什么意思呢?
就是如果我加一个功能,可以不用修改原来的老代码,直接添加新的功能即可.不修改老的代码,就是对修改养老,直接添加新的代码,就是对扩展开放.
直接添加新的代码,不用修改老代码就可以完成软件功能的扩展,这样会减少出错的可能,提高系统的稳定性.
设计模式的工厂模式就是用了开闭原则:先看下普通工厂模式,以生产手机为例:
1 public class PhoneFactory { 2 3 public Phone produce(String type){ 4 Phone phone = null; 5 6 if("xiaomi".equals(type)){ 7 phone = new XiaoMiPhone(); 8 }else if("sanuag".equals(type)){ 9 phone = new SanuagPhone(); 10 }else if("nokia".equals(type)){ 11 phone = new NokiaPhone(); 12 } 13 14 return phone; 15 } 16 }
上面代码是生产手机的工厂,如果现在工厂升级,需要再生产华为手机,怎么办? 很显示我们可以在原来的代码上再加一个if条件判断:
1 public class PhoneFactory { 2 3 public Phone produce(String type){ 4 Phone phone = null; 5 6 if("xiaomi".equals(type)){ 7 phone = new XiaoMiPhone(); 8 }else if("sanuag".equals(type)){ 9 phone = new SanuagPhone(); 10 }else if("nokia".equals(type)){ 11 phone = new NokiaPhone(); 12 }else if("huawei".equals(type)){ 13 phone = new HuaweiPhone(); 14 } 15 16 return phone; 17 } 18 19 }
通过对原来代码的修改,我们就做到了再加一条生产线的需求。开闭原则,就是对修改关闭,对扩展开放。我们不提倡这种做法,对修改关闭,对于本例,就是不
要用上面这种在原来的代码中添加一条 if 判断的做法来添加功能,我们要做到对扩展开放:如下
我们把工厂独立出来
1 //工厂 2 public interface Factory { 3 Phone produce(); 4 }
我们在添加其它的专门生产某一种手机的工厂,如下
1 //生产Nokia手机的工厂 2 public class NokiaFactory implements Factory{ 3 @Override 4 public Phone produce() { 5 return new NokiaPhone(); 6 } 7 }
1 //生产三星手机的工厂 2 public class SanuagFactory implements Factory{ 3 @Override 4 public Phone produce() { 5 return new SanuagPhone(); 6 } 7 }
//生产小米手机的工厂 public class XiaomiFactory implements Factory{ @Override public Phone produce() { return new XiaoMiPhone(); } }
下面我们来看看怎么用,测试类如下
1 //测试工厂方法模式 2 private static void testFactoryMethod(){ 3 Factory factory = new NokiaFactory(); 4 Phone phone = factory.produce(); 5 phone.call(); 6 }
直接new一个工厂,生产对应的手机,这样我们如果再添加一条生产华为手机的时候,就可以添加一个华为手机的工厂,然后直接就可以生产手机了,这样的话,
我们就可以做到不修改原来的代码(对修改关闭),直接添加新的工厂(对扩展开放),就可以达到我们的要求了。
三 里氏替换原则
构建扩展性更好的系统:由于是一位姓里的女士提出来的所以就叫做里氏替换原则,说白了就是所有引用基类的地方也可以换成子类.
我们知道面向对象的三大特点是继承,封装,多态.里氏替换的原则就是基于继承和多态.
一个简单的示例,如下:
Window类,显示一个View的。show(View child)方法中 child可以替换成View的子类,就是里氏替换原则,里氏替换原则就是依赖面向对象的继承和多态
1 //窗口类 2 public class Window { 3 public void show(View child){ 4 child.draw(); 5 } 6 }
//各种View
1 //View 2 public abstract class View { 3 public abstract void draw(); 4 5 public void measure(int width,int height){ 6 //测量视图大小 7 } 8 } 9 10 class Button extends View{ 11 12 @Override 13 public void draw() { 14 //绘制按钮 15 } 16 } 17 18 class TextView extends View{ 19 20 @Override 21 public void draw() { 22 //绘制TextView 23 } 24 }
上面例子中,Window依赖于View,而View定义了一个视图对象,measure是各个子类共享的方法,子类通过重写View的draw()方法实现具体各自特色的功能,
在这里,这个功能就是绘制自身的内容,在任何继承自View类的子类都可以设置给show()方法,就是所说的里氏替换。
四 依赖倒置原则
让项目拥有变化的能力:有三个特点
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
- 抽象不应该依赖细节;
- 细节应该依赖抽象。
依赖倒置原则就是面向接口编程,下面的例子引用了 http://blog.csdn.net/zhengzhb/article/details/7289269
在这里我把这个例子拿过来,因为它能说明问题:
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在Java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
依赖倒置原则的核心思想是面向接口编程,我们依旧用一个例子来说明面向接口编程比相对于面向实现编程好在什么地方。场景是这样的,母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。代码如下:
1 class Book{ 2 public String getContent(){ 3 return "很久很久以前有一个阿拉伯的故事……"; 4 } 5 } 6 7 class Mother{ 8 public void narrate(Book book){ 9 System.out.println("妈妈开始讲故事"); 10 System.out.println(book.getContent()); 11 } 12 } 13 14 public class Client{ 15 public static void main(String[] args){ 16 Mother mother = new Mother(); 17 mother.narrate(new Book()); 18 } 19 }
运行结果:
妈妈开始讲故事
很久很久以前有一个阿拉伯的故事……
运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:
1 class Newspaper{ 2 public String getContent(){ 3 return "林书豪38+7领导尼克斯击败湖人……"; 4 } 5 }
这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。
我们引入一个抽象的接口IReader。读物,只要是带字的都属于读物:
interface IReader{ public String getContent(); }
Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改为:
1 class Newspaper implements IReader { 2 public String getContent(){ 3 return "林书豪17+9助尼克斯击败老鹰……"; 4 } 5 } 6 class Book implements IReader{ 7 public String getContent(){ 8 return "很久很久以前有一个阿拉伯的故事……"; 9 } 10 } 11 12 class Mother{ 13 public void narrate(IReader reader){ 14 System.out.println("妈妈开始讲故事"); 15 System.out.println(reader.getContent()); 16 } 17 } 18 19 public class Client{ 20 public static void main(String[] args){ 21 Mother mother = new Mother(); 22 mother.narrate(new Book()); 23 mother.narrate(new Newspaper()); 24 } 25 }
运行结果:
妈妈开始讲故事
很久很久以前有一个阿拉伯的故事……
妈妈开始讲故事
林书豪17+9助尼克斯击败老鹰……
这样修改后,无论以后怎样扩展Client类,都不需要再修改Mother类了。这只是一个简单的例子,实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。
采用依赖倒置原则给多人并行开发带来了极大的便利,比如上例中,原本Mother类与Book类直接耦合时,Mother类必须等Book类编码完成后才可以进行编码,因为Mother类依赖于Book类。修改后的程序则可以同时开工,互不影响,因为Mother与Book类一点关系也没有。参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大。现在很流行的TDD开发模式就是依赖倒置原则最成功的应用。
传递依赖关系有三种方式,以上的例子中使用的方法是接口传递,另外还有两种传递方式:构造方法传递和setter方法传递,相信用过Spring框架的,对依赖的传递方式一定不会陌生。
在实际编程中,我们一般需要做到如下3点:
- 低层模块尽量都要有抽象类或接口,或者两者都有。
- 变量的声明类型尽量是抽象类或接口。
- 使用继承时遵循里氏替换原则。
依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。
五 接口隔离原则
让系统有更高的灵活性:让客户端不应该依赖于它不需要的接口,就是让客户端依赖的接口尽可能的小.我们举一个例子
在此之前JDK6以及之前的版本,有一个非常讨厌的问题那就是在使用了OutputStream或者其它可关闭的对象之后,我们必须保证他们被关闭了。
比如下面的代码: 1 //将图片缓存到内存中
1 //将图片缓存到内存中 2 public void put(String url,Bitmap bmp){ 3 FileOutputStream fileOutputStream = null; 4 try{ 5 fileOutputStream = new FileOutputStream(fileName); 6 bmp.compress(CompressFormat.PNG,100,fileOutputStream); 7 }catch (FileNotFoundException e){ 8 e.printStackTrace(); 9 }finally { 10 if(fileOutputStream != null){ 11 try{ 12 fileOutputStream.close(); 13 }catch (IOException e){ 14 e.printStackTrace(); 15 } 16 } 17 } 18 }
可以看到这样的代码可读性非常的差,各种try..catch 里面都是一些简单的代码,但是会严重影响代码的可读性,并且多层级的大括号很容易将代码写到错误层级中去,大家应该对这类代码非常反感,反正我是非常反感。
我们知道Java中有一个Closeable接口,该接口标识了一个可关闭的对象,它只有一个close方法。我们要讲的FileOutputStream就实现了这个类。所以我们可以建一个可以关闭这个对象的类就可以了。工具类如下:
1 //关闭工具类 2 public class CloseUtils { 3 private CloseUtils(){} 4 5 //关闭Closeable对象 6 public static void closeQuitely(Closeable closeable){ 7 if(closeable != null){ 8 try { 9 closeable.close(); 10 }catch (IOException e){ 11 e.printStackTrace(); 12 } 13 } 14 } 15 }
我们再把这段代码运用到上面put代码中去效果如何:
1 //将图片缓存到内存中 2 public void put(String url,Bitmap bmp){ 3 FileOutputStream fileOutputStream = null; 4 try{ 5 fileOutputStream = new FileOutputStream(fileName); 6 bmp.compress(CompressFormat.PNG,100,fileOutputStream); 7 }catch (FileNotFoundException e){ 8 e.printStackTrace(); 9 }finally { 10 CloseUtils.closeQuitely(fileOutputStream); 11 } 12 }
代码简洁了很多,这个closeQuitely方法可以运用到各个类可关闭的对象中,保证了代码的可重用性。并且建立在最小化依赖原则的基础上,它只需要这个对象是可关闭的,其它的一概不关心,也就是这里的接口隔离原则。
六 迪米特原则
让系统有更好的扩展性:一个对象应该对其它对象有最少的了解.通俗的讲,一个类应该对自己需要耦合或者调用的类知道的最少
类的内部如何实现与调用者或者依赖者没有关系.
迪米特原则还有一个英文的解释叫 Only talk to your immedate friends ,翻译过来就是:只与直接的朋友通信.写个例子就知道了 北漂的朋友都有过租房子的经过.大多数租房子都是找中介,我们要求是,我只要求房间的面积和价格,其它的一概不管,中介将符合的房子提供给我就行了.看下这个示例
1 //房间 2 public class Room { 3 public float area; //面积 4 public float price; //价格 5 6 public Room(float area, float price) { 7 this.area = area; 8 this.price = price; 9 } 10 11 @Override 12 public String toString() { 13 return "Room area=" + area + ",price=" + price; 14 } 15 }
1 //中介 2 public class Mediator { 3 List<Room> roomList = new ArrayList<>(); 4 5 public Mediator(){ 6 for (int i = 0;i < 10 ;i++){ 7 roomList.add(new Room(12 + i, (12 + i) * 100 )); 8 } 9 } 10 11 public List<Room> getRoomList(){ 12 return roomList; 13 } 14 }
1 //客户 2 public class Customer { 3 public float roomArea; 4 public float roomPrice; 5 6 public void rentRoom(Mediator mediator){ 7 System.out.println(mediator.rentOut(roomArea,roomPrice)); 8 } 9 10 11 public void main(String[] args){ 12 //客户 13 Customer customer = new Customer(); 14 //中介 15 Mediator mediator = new Mediator(); 16 //租房子 17 customer.rentRoom(mediator); 18 } 19 }
从上面的代码中可以看出, Customer不仅依赖了Mediator,还需要和Room打交道,客户只要求找到一间合适的房子罢了,如果把这些条件都放到Customer中,那么
中介的功能就会被弱化,而且导致Customer与Room的耦合较高,因为Customer必须知道Room的许多细节,Room变化也得跟着变化.这个时候,我们就需要知道谁才是我们真正的朋友,在我们这个例子中,显示是中介,我们只需要和中介打交道就可以了.所以我们可以进行如下重构
1 //中介 2 public class Mediator { 3 List<Room> roomList = new ArrayList<>(); 4 5 public Mediator(){ 6 for (int i = 0;i < 10 ;i++){ 7 roomList.add(new Room(12 + i, (12 + i) * 100 )); 8 } 9 } 10 11 public Room rentOut(float area,float price){ 12 for(Room room : roomList){ 13 if(isSuitable(room,area,price)){ 14 return room; 15 } 16 } 17 18 return null; 19 } 20 21 private boolean isSuitable(Room room,float area,float price){ 22 return room.area == area && room.price == price; 23 } 24 }
1 //客户 2 public class Customer { 3 public float roomArea; 4 public float roomPrice; 5 6 public void rentRoom(Mediator mediator){ 7 System.out.println(mediator.rentOut(roomArea,roomPrice)); 8 } 9 }
通过上面的重构,Customer只与Mediator打交道,这本应该就是Mediator的职责,根据客户设定的条件选出合适的房子,并把结果交给客户就行了.
这样就能把耦合比较复杂的代码解开了.使得代码耦合更低,稳定性更好.