Java实现 一篇文章说尽设计模式之六大原则

九路 等级 556 0 0

我们知道,设计模式很有用,学好设计模式不但能让你写出更简洁,优雅的代码,还能使得代码的结构更清晰,也更有利于扩展

当然设计模式也不是万能的,一成不变的.设计模式只是前人总结出来的一种经验,一种特定问题的解决方法,不能看作是死的东西

不一定非要生搬硬套,非得按照设计模式书上来来,只要我们写的代码符合一定的一些原则,一样可以看作是自己的模式.但是前人

总结出来的东西必须非常值得我们学的.本系列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的职责,根据客户设定的条件选出合适的房子,并把结果交给客户就行了.

这样就能把耦合比较复杂的代码解开了.使得代码耦合更低,稳定性更好.

收藏
评论区

相关推荐

Retrofit 支持suspend函数源码分析
Retrofit 2.6.0 之后支持接口suspend函数配合协程使用,举个例子: ApiService java interface LoginApiService : BaseService { @GET("/wxarticle/chapters/json") suspend fun getChapters(): BaseResp
一篇文章彻底搞懂Java的大Class到底是什么
作者在之前工作中,面试过很多求职者,发现有很多面试者对Java的 Class 搞不明白,理解的不到位,一知半解,一到用的时候,就不太会用。 因为自己本身以前刚学安卓的时候,甚至做安卓2,3年后,也是对 java的 Class不是太清楚,所以想写一篇关于Java Class 的文章,没有那么多专业名词,希望用通俗的语言能把Java的 Class 这个概念讲明
.NET C#到Java没那么难,MVC篇
.NET C到Java没那么难,MVC篇 .NET C到Java没那么难,MVC篇 最典型的JAVA MVC就是JSP servlet javabean的模式。比较好的MVC,老牌的有Struts、
Groovy初探
开始之前 了解本教程的主要内容,以及如何从中获得最大收获。 关于本教程 如果现在有人要开始完全重写 Java,那么 Groovy 就像是 Java 2.0。Groovy 并没有取代 Java,而是作为 Java 的补充,它提供了更简单、更灵活的语法,可以在运行时动态地进行类型检查。您可以使用 Groovy 随意编写 Java 应用程序,连接 Java
《java 核心技术》卷1 学习 概述 第一章Java程序设计概述
从浅面了解Java 1.Java 在语言得地位 现在有所下降 但仍是老大哥 所以值得学习 2.Java特性 1.简单性:从一方面来说 Java可以支持在小型机器上运行 必定不是很复杂得,所以上手不难 2.面向对象:Java有相比于其他的语言 更简单得接口
java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一
java 泛型详解绝对是对泛型方法讲解最详细的,没有之一 对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下。 本文参考、、 1、概述泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。什么是泛型?
Java集合之综合论述
1.Java集合 1.1 集合应用场景1. 无法预测存储数据的数量的情况下,2. 同时存储一对一关系的数据3. 需要进行数据的增删4. 数据重复问题 1.2 集合框架的体系结构 集合框架分为两类,一是Collection,用于存储类的对象。二是Map,以键值对的形式存储信息。 Collection主要有三个子接口,List(序列),Queue(队列
Java里面的十万个为什么
Java里面的十万个为什么 1.不是说 JVM 是运行 Java 程序的虚拟机吗?那 JRE 和 JVM 的关系是怎么样的呢?简单地说,JRE 包含 JVM 。JVM 是运行 Java 程序的核心虚拟机,而运行 Java 程序不仅需要核心虚拟机,还需要其他的类加载器,字节码校验器以及大量的基础类库。JRE 除包含 JVM 之外,还包含运行 Java 程序的其
一篇文章弄懂 Java 反射的使用
说到Java反射,必须先把 Java 的字节码搞明白了,也就是 Class , 大 Class在之前的文章中,我们知道了Java的大Class就是类的字节码,就是一个普通的类,里面保存的是类的信息,还不太明白Java的大Class的,可以先看一下之前的文章 先想一个问题 1. 给我们一个类,我们如何使用?这还不简单,通过这个类,创建一个类的对象,再通过这个
2021年度最全面JVM虚拟机,类加载过程与类加载器
前言类装载器子系统是JVM中非常重要的部分,是学习JVM绕不开的一关。一般来说,Java 类的虚拟机使用 Java 方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表
2021年度最全面JVM虚拟机,类加载过程与类加载器
前言类装载器子系统是JVM中非常重要的部分,是学习JVM绕不开的一关。一般来说,Java 类的虚拟机使用 Java 方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表
Java并发之ReentrantLock源码解析
Java并发之ReentrantLock源码解析 Condition在上一章中,我们大概了解了Condition的使用,下面我们来看看Condition再juc的实现。juc下Condition本质上是一个接口,它只定义了这个接口的使用方式,具体的实现其实是交由子类完成。cpublic interface Condition void await()
Java并发之Semaphore源码解析
Semaphore 前情提要在学习本章前,需要先了解ReentrantLock源码解析,ReentrantLock源码解析里介绍的方法有很多是本章的铺垫。下面,我们进入本章正题Semaphore。从概念上来讲,信号量(Semaphore)会维护一组许可证用于限制线程对资源的访问,当我们有一资源允许线程并发访问,但我们希望能限制访问量,就可以用信号量对访问线程
五面阿里巴巴拿offer后定级P6:分享Java面经及答案总结
一面(电话)说说对JVM的理解treemap和hashmap有什么区别?Java多线程的的5大状态图流转mysql主键和唯一索引的区别说说最近的项目如何实现session共享,用redis如何实现缓存击穿的概念和解决方案说说微服务,微服务之间如何管理二面(现场)java nio常?用的三个类java里面的同步锁了解吗?Countdownlauch和Cylic
腾讯T3团队整理,持续更新中
Java基础1.JAVA 中的几种数据类型是什么,各自占用多少字节。2.String 类能被继承吗,为什么。3\. 两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?4\. String 属于基础的数据类型吗?5.Java 中操作字符串都有哪些类?它们之间有什么区别?6.Java 中 IO 流分为几种?7.BIO、NIO