最近又看了一遍刘伟的Java设计模式,这次决定写一篇读后感,趁热将学到的东西和自己的理解总结下来,加深一下印象,也方便自己以后回顾。

刘伟Java设计模式地址:https://gof.quanke.name/, 个人认为是讲的比较详细的一本设计模式,值得学习。

设计模式的7大原则

  1. 单一职责

一个类只做一件事,不承担过多的职责,保持职责单一。如果一个类承担了太多的职责,就可能有更多引起它变化的原因,很不稳定

  1. 开闭原则

设计模式的终极目标,在不修改现有代码的前提下,进行功能的扩展。

  1. 里氏代换

使用基类的地方都可以替换为子类对象正常使用,在程序中应尽量使用基类来定义,在运行的时候再确定其子类类型,用子类对象来替换父类对象,其实也就是多态

是实现开闭原则的理论基础。

  1. 依赖倒置

针对抽象/接口编程,尽量依赖抽象,而不依赖具体的实现。

是实现开闭原则的手段。

  1. 接口隔离

拆分多个专门的接口,而不是使用单一的复杂接口。因为我们不需要依赖全部的接口方法。

  1. 合成复用

尽量使用对象的组合,而不是继承来达到复用的目的。

继承关系的耦合度更高,父类修改会影响到子类,而采用组合,关联的对象只暴露必要的实现,两者耦合度更低。

  1. 迪米特法则

一个对象应当尽量少地与其他对象发生关系。一个模块的修改势必会影响到与它有关系的其他对象,减少对象之间的耦合联系,就可以缩小修改影响的范围。

23种Java设计模式或多或少都满足了上面的一些设计原则,来保证可维护性和扩展性,其中开闭原则是一个理想设计模式的终极目标。

6个创建型模式

简单工厂模式

factory_simple

通过工厂来封装产品创建的细节,项目中使用频率较高,使用简单。缺点是:不满足开闭原则,增加新的产品类,需要改动工厂类。

工厂方法模式

工厂方法模式解决了简单工厂不满足开闭原则的问题,将工厂抽象出了一个Factory接口,每个实现的工厂只生产一个具体的产品。 当需要增加一种产品时,只需要增加一个产品类和一个工厂类,不需要改动现有代码,满足开闭原则。

缺点:当有大量的产品时,如果一个产品对应一个工厂,将会有大量的工厂类。

案例:Java中的一些集合类实现了Iterable工厂接口来生产Iterator产品。

抽象工厂模式

抽象工厂模式一定程度上弥补了工厂方法模式的不足(大量工厂类),一个工厂类可以生产一个产品族的所有产品。当增加一个产品族的时候,增加所有产品和工厂类,满足开闭原则;当增加某个产品时,需要修改工厂类,不满足开闭原则,称之为开闭原则的倾斜性。

单例模式

单例模式非常简单,实现方式有饿汉模式、懒汉模式(线程安全)、Holder

原型模式

原型模式就是克隆,当一个对象的创建非常复杂时,可以快速创建对象的副本。在Java中就是实现一个Cloneable接口,然后复写clone方法。需要注意浅拷贝和深拷贝的问题。

建造者模式

使用频率非常高的一种设计模式。如果要创建的对象有很多参数,并且其中某些参数还是可选的,用构造函数来创建的话就需要提供好多个重载来满足不同的配置,甚至还要记住这些参数的顺序。这种情况就可以使用建造者模式进行优化,通过配置参数的方式一步一步组装完成一个对象。

案例:OKHttp、Dialog等很多开源项目都采用了建造者方式来创建复杂对象。

7个结构型模式

适配器模式

当新旧接口不一致时,使用适配器模式适配旧接口为新的接口。

案例:Android中的Adapter,适配各种各样的数据,提供ViewHolder。

桥接模式

当一个业务有多个不同的维度变化时,使用继承会产生大量的类并且不满足开闭原则,使用组合对两个维度进行抽象层的桥接,可以方便以后的扩展,增加任何一个维度的实现都只需要增加一个类,不需要对现有逻辑做修改。

案例:

Android源码的ListView和GridView那块,AdapterView是一个维度,Adapter是另一个维度,通过AdapterView#setAdatper()进行桥接,最终结果是ListView、GridView可以和任意的Adapter进行两两组合。

组合模式

组合模式的目的就是:对于一个树型结构,能够采用一种一致的方式来处理叶子节点和容器节点。 如UML所示,叶子节点和容器节点都实现了Component接口,我们可以一致地调用operation接口进行处理,而不用care他是叶子还是容器。对于容器节点,多了一些管理叶子节点的方法,在构造树的时候使用。

案例:Android中的View树就是采用组合模式,绘制的时候只需要调用measure方法,不用管当前是View还是ViewGroup。

装饰者模式

装饰者模式的作用是通过装饰者为目标增强功能。装饰者和被装饰者实现同个接口。

案例:Java的InputStream、OutStream用到了装饰者。

外观模式

当业务中有多个子系统时,可以定义一个外观类提供更高层的接口,内部调用各个子系统的功能,实现客户端和各个子系统的解耦。典型的迪米特法则,一个实体尽可能与更少的实体发生关系。

享元模式

书中使用棋盘中的黑子和白子来举例享元模式,对应的UML图如上图所以,根据案例来理解享元模式比较简单。

享元模式是为了优化内存占用的问题,当一个系统中有大量重复对象的时候(黑子、白子),我们通过享元模式来共享内存最终达到减少内存占用的目的。

使用享元模式要区分对象的内部状态和外部状态。内部状态就是保持不变,可以共享的一部分,在案例中就是颜色这个属性,所有的白子都可以共享白色这个内部状态。外部状态就是会随着环境进行变化的部分,在案例中就是棋子坐标,所有的白子都有各自的坐标。使用享元模式时,对于内部状态我们可以共享内存,对于外部状态,我们需要提供方法来接受外部状态对象(Coordinate),实现共享对象的不同显示。

网上很多地方认为对象池(比如Message对象池)是享元模式的一种应用,我认为对象池和享元池完全是解决不同的问题,两者根本不能混为一谈。

对象池:

为了解决不断创建对象、销毁对象导致内存抖动问题,使用对象池来复用之前丢弃的对象。业务场景上并没有在同一时间共享同一个对象的内存,而是在时间上先后使用同一个对象。

享元池:

享元池是为了解决大量重复相似对象的使用导致的内存占用过大问题,享元池中的对象各不相同,业务场景上在同一时间某个对象被共享使用。

代理模式

代理模式从UML图上看跟装饰者模式很相似:都是实现同个接口,代理类代理目标类,装饰者装饰被装饰者,好像是同个东西换了个名字?其实不然,总结了下两者不同的地方:

1、应用场景上,代理模式是为了帮助目标类增强一些自己不关心的事,比如日志代理,在目标类前后加一些日志。而装饰模式则是用来增强自身的功能,比如Java的InputStream那些的子类装饰类,提供了一些更方便的接口给我们调用。

2、使用方式上,代理模式一般在代理类中确定了要被代理的目标对象,客户端根本不知道被代理类的存在。而装饰模式中被装饰者对象需要客户端创建提供,并且可以层层嵌套,层层装饰

11个行为性模式

职责链模式

将请求发送者和处理者解耦,发送者不需要关心到底谁处理了事件,只需要将请求发送到链上。

案例:OKHttp框架 https://monkeylmj.github.io/post/okhttp/

命令模式

通过命令对象将请求的发送者和处理者进行解耦,发送者只需要发出一个命令,不需要关心谁处理了这个请求(具体的Command知道谁处理了),类似于开关与灯泡的关系,一个开关并不关心自己控制哪个灯泡的亮灭,通过电线(命令)来决定。

解释器模式

解释器模式的应用场景是解释语言文法,将其抽象成一棵语法树然后解释执行。

从UML图来看跟组合模式基本是一个东西,两者都是操作树型结构。这里我也分不清两者的区别,可能只是应用场景上的不同,解释器用来解释语句??

迭代者模式

对集合对象的遍历逻辑如果放在集合对象中,会造成集合对象职责过重,并且更换遍历方式时必须修改现有代码,不符合开闭原则。所以可以将遍历部分的逻辑抽离到迭代器中单独变化。

UML图中是工厂方法模式和迭代器模式的结合使用。使用迭代器模式之后,可以在满足开闭原则的前提下为集合对象更换迭代方式。

中介者模式

对于多个对象之间的复杂交互,采用传统的方案有几个缺点:

1、系统结构复杂,耦合度高。每个组件都与其他组件相互关联和调用,系统呈网状结构。

2、组件的可重用性差。因为一个组件与其他组件有很强的关联,导致其没有其他组件的支持很难被重用。

3、可扩展性差。因为组件之间的复杂关系,导致增加一个组件,需要修改系统中大量的地方,不满足开闭原则

根据迪米特法则,一个类应该尽可能少地与其他类产生关系,通常的做法就是增加一层封装。在这里,增加的这一层就是中介者。中介者关联了所有的组件,任何一个组件都只需要与中介者发生关系,通过中介者与其他组件进行关联调用,整个系统结构呈星状。

备忘录模式

备忘录模式非常简单,当需要记录一个对象在某个时刻的状态,以后进行恢复的时候可以选择备忘录模式。

案例:Android的onSaveInstanceState和onRestoreInstanceState实现就是备忘录模式,在Activity异常关闭时记录下必要的状态,下次打开时进行恢复。

观察者模式

使用频率最高的一种设计模式,各种Callback。

状态者模式

状态模式适用于存在多个状态的系统。如果使用传统的方案(不分状态类),则需要在一个类中使用变量来控制各个状态,然后会有大量的if else判断和状态切换的代码,增加状态或者修改都十分麻烦,不满足开闭原则。

增加状态类之后,可以将某个状态的行为都封装在一起,满足了单一职责原则。环境类Context中关联当前的状态,只负责将请求发到当前状态上,而不需要关心当前状态是什么。如果增加一个状态,也只需要增加一个类,并且处理好状态之间的转换关系即可,满足开闭原则。

策略模式

比较简单,当需要切换不同策略时,可以使用策略模式,抽象一个接口出来。

模板方法模式

当一个操作是一个框架,有一个固定的执行流程,其中某些流程是共同的,某些是有差异的,就可以使用模板方法模式,将框架和共同的步骤封装在父类中,不同的步骤用抽象方法代替,由子类来实现具体的操作。

访问者模式

如果对于一个复杂对象有很多种访问处理方式,可以使用访问者模式将访问对象的行为封装到单独访问类中,满足单一职责。增加新的访问操作,只需要再添加一个具体访问类即可,满足开闭原则。增加元素类型,需要修改抽象访问类添加对象的访问方法,不满足开闭原则。

至此,23种设计模式已经过了一遍,个人认为他们之间有很多的共性,有些设计模式的UML图几乎是一模一样的,但是他们在应用的场景上有差异,只有在实践中多用才能慢慢掌握这些设计原则,做到"手中无模式,心中有模式"

23种设计模式的UML图: https://www.processon.com/view/link/5d1fff5de4b0d16653f67a39