《Head First设计模式》中文版读书笔记
我只是怕某天死了,我的生命却一无所有。—-《奇幻之旅》
写在前面:
嗯,参加软考,考设计模式,之前都是学一半就丢掉了,《图解设计模式》看了$\frac{1}{4}$后没在看,所以现在刷一下。嘻嘻,如获至宝,感觉不错,没有早一点读这本书,记得以前是打开来着,看着花花绿绿的就没细看…
嗯,本书的电子档资源我放到评论里了,博客主要是以书里内容为主,关于博客里的UML图,小伙伴有需要的可以联系我,生活加油 :(
笔记内容
:这本书的设计模式
不全,所以结合菜鸟教程
上的笔记,还有那本著名的《设计模式_可复用面向对象软件的基础》的部分,《设计模式_可复用面向对象软件的基础》
这本以后有时间刷,感觉非常不错,就是有些重,有些还是读不懂,水平有限,不适合快速学习,如果时间多,强烈建议小伙伴真的要刷一下,超级棒,需要资源可以联系我 :)
我只是怕某天死了,我的生命却一无所有。—-《奇幻之旅》
一、软件设计七大原则
嗯,看书学习之前,先了解下面向对象设计原则吧
名称 | 描述 |
---|---|
单一职责原则(SRP:Single responsibility principle) | 单一职责原则:一个类应该只有一个发生变化的原因,应该只有一个职责 |
开闭原则(Open Close Principle) | 开闭原则的意思是:对扩展开放,对修改关闭。 |
里氏代换原则(Liskov Substitution Principle) | 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。 |
依赖倒转原则(Dependence Inversion Principle) | 这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。 |
接口隔离原则(Interface Segregation Principle) | 这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。 |
迪米特法则,又称最少知道原则(Demeter Principle) | 最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。 |
合成复用原则(Composite Reuse Principle) | 合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。 |
里氏代换原则
是对开闭原则
的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
嗯,学设计模式,多态的概念是必须要掌握的,我们在看下多态吧:
多态分为两种通用的多态
和特定的多态
。
特定的多态
分为过载多态(overloading)
和强制多态(coercion)
.
多态类型 | 描述 | 实例 |
---|---|---|
强制多态 |
编译程序通过语义操作,把操作对象的类型强行加以变换,以符合函数或操作符的要求。程序设计语言中基本类型的大多数操作符,在发生不同类型的数据进行混合运算时,编译程序一般都会进行强制多态。程序员也可以显示地进行强制多态的操作(Casting) 。 | 三元运算符返回类型总是最大的那个,强制类型转化符 |
过载(overloading)多态 |
同一个名(操作符、函数名)在不同的上下文中有不同的类型`。程序设计语言中基本类型的大多数操作符都是过载多态的。 | 通过不同的方法签名实现方法重载 |
通用的多态
又分为参数多态(parametric)
和包含多态(inclusion)
;
多态类型 | 描述 | 实例 |
---|---|---|
参数多态 |
采用参数化模板,通过给出不同的类型参数,使得一个结构有多种类型 | 对应中java中的泛型的概念public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable |
包含多态 |
同样的操作可用于一个类型及其子类型。(注意是子类型,不是子类。)包含多态一般需要进行运行时的类型检查。 | 定义接口引用,运行时动态绑定实现类实例 |
两者的区别是: |
- 前者对工作的类型不加限制,允许对不同类型的值执行相同的代码;
- 后者只对有限数量的类型有效,而且对不同类型的值可能要执行不同的代码。
二,设计模式概述
嗯,学设计模式之前,先大概整体了解下吧,如果想在项目中使用,这些应该都要熟悉。
创建型
设计模式名称 | 简要说明 | 速记关键字 |
---|---|---|
Abstract Factory 抽象工厂模式 | 提供一个接口,可以创建一系列相关或相互依赖的对象而无需指定它们具体的类 | 生产成系列对象 |
Builder 构建器模式 | 将一个复杂类的表示与其构造相分离,使得相同的构建过程能够得出不同的表示 | 复杂对象构造 |
Factory Method 工厂方法模式 | 定义一个创建对象的接口,但由子类决定需要实例化哪一个类。工厂方法使得子类实例化的过程推迟 | 动态生产对象 |
Prototype 原型模式 | 用原型实例指定创建对象的类型,并且通过拷贝这个原型来创建新的对象 | 克隆对象 |
singleton 单例模式 | 保证一个类只有一个实例,并提供一个访问它的全局访问点 | 单实例 |
结构型
设计模式名称 | 简要说明 | 速记关键字 |
---|---|---|
Adapter 适配器模式 | 将一个类的接口转换成用户希望得到的另一种接口。它使原本不相容的接口得以协同工作 | 转换接口 |
Bridge 桥接模式 | 将类的抽象部分和它的实现部分分离开来,使它们可以独立地变化 | 继承树拆分 |
Composite组合模式 | 将对象组合成树型结构以表示“整体-部分”的层次结构,使得用户对单个对象和组合对象的使用具有一致性 | 树形目录结构 |
Decorator装饰模式 | 动态地给一个对象添加一些额外的职责。它提供了用子类扩展功能的一个灵活的替代,比派生一个子类更加灵活 | 附加职责 |
Facade外观模式 | 定义一个高层接口,为子系统中的一组接口提供一个一致的外观,从而简化了该子系统的使用 | 对外统一接口 |
Flyweight 享元模式 | 提供支持大量细粒度对象共享的有效方法 | 文章共享文字对象 |
行为型
设计模式名称 | 简要说明 | 速记关键字 |
---|---|---|
Chain of Responsibility 职责链模式 | 通过给多个对象处理请求的机会,减少请求的发送者与接收者之间的耦合。将接收对象链接起来,在链中传递请求,直到有一个对象处理这个请求 | 传递职责 |
Command 命令模式 | 将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化,将请求排队或记录请求日志,支持可撒销的操作 | 日志记录,可撒销 |
Interpreter 解释器模式 | 给定一种语言,定义它的文法表示,并定义一个解释器,该解释器用来根据文法表示来解释语言中的句子 | 虚拟机的机制?? |
Iterator 迭代器模式 | 提供一种方法来顺序访问一个聚合对象中的各个元素,而不需要暴露该对象的内部表示 | 数据库数据集 |
Mediator 中介者模式 | 用一个中介对象来封装一系列的对象交互。它使各对象不需要显式地相互调用,从而达到低耦合,还可以独立地改变对象间的交互 | 不直接引用 |
Memento 备忘录模式 | 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,从而可以在以后将该对象恢复到原先保存的状态 | 可恢复 |
Observer 观察者模式 | 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新 | 联动 |
State 状态模式 | 允许一个对象在其内部状态改变时改变它的行为 | 状态变成类 |
Strategy 策略模式 | 定义一系列算法,把它们一个个封装起来,并且使它们之间可互相替换,从而让算法可以独立于使用它的用户而变化 | 多方案切换 |
TemplateMethod 模板方法模式 | 定义一个操作中的算法骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重新定义算法的某些特定步骤 | 文档模板填空 |
Visitor 访问者模式 | 表示一个作用于某对象结构中的各元素的操作,使得在不改变各元素的类的前提下定义作用于这些元素的新操作 | 数据与操作分离 |
三,设计模式笔记
下面我们就开动啦….嘻嘻,好激动,嗯,笔记不是按照书里的记得,后面根据模式类型调整啦,第一书里原本是策略模式啦…嗯
原型(Prototype)模式-对象创建型
原型模式(Prototype Pattern)
是用于创建重复的对象
,同时又能保证性能。用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
,它提供了一种创建对象的最佳方式。
这种模式是实现了一个原型接口
,该接口用于创建当前对象的克隆
。当直接创建对象的代价比较大时,则采用这种模式。例如,一个对象需要在一个高代价的数据库操作之后被创建。我们可以缓存该对象,在下一个请求时返回它的克隆,在需要的时候更新数据库,以此来减少数据库调用。
意图
:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。主要解决
:在运行期建立和删除原型。何时使用
: 1、当一个系统应该独立于它的产品创建,构成和表示时。 2、当要实例化的类是在运行时刻指定时,例如,通过动态装载。 3、为了避免创建一个与产品类层次平行的工厂类层次时。 4、当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。如何解决
:利用已有的一个原型对象,快速地生成和原型对象一样的实例。关键代码
: 1、实现克隆操作,在 JAVA 继承 Cloneable,重写 clone(),在 .NET 中可以使用 Object 类的 MemberwiseClone() 方法来实现对象的浅拷贝或通过序列化的方式来实现深拷贝。 2、原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些”易变类”拥有稳定的接口。
应用实例
: 1、细胞分裂。 2、JAVA 中的 Object clone() 方法。优点
: 1、性能提高。 2、逃避构造函数的约束。缺点
: 1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象
,或者引用含有循环结构的时候。 2、必须实现Cloneable
接口。
使用场景
: 1、资源优化场景。 2、类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。 3、性能和安全要求的场景。 4、通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。 5、一个对象多个修改者的场景。 6、一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。 7、在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。
注意事项
:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。
创建一个实现了 Cloneable 接口的抽象类。Shape.java
1 | public abstract class Shape implements Cloneable { |
创建扩展了上面抽象类的实体类。Rectangle.java
1 | public class Rectangle extends Shape { |
Square.java
1 | public class Square extends Shape { |
Circle.java
1 | public class Circle extends Shape { |
创建一个类,从数据库获取实体类,并把它们存储在一个 Hashtable 中。ShapeCache.java
1 | import java.util.Hashtable; |
PrototypePatternDemo 使用 ShapeCache 类来获取存储在 Hashtable 中的形状的克隆。PrototypePatternDemo.java
1 | public class PrototypePatternDemo { |
执行程序,输出结果:
1 | Shape : Circle |
GOF描述
Prototype
有许多和Abstract Factory
和Builder
一样的效果:它对客户隐藏
了具体的产品类
,因此减少了客户知道的名字的数目
。此外,这些模式使客户无需改变即可使用与特定应用相关的类。下面列出Prototype模式的另外一些优点。
1)
运行时刻增加和删除产品
Prototype
允许只通过客户注册原型实例就可以将一个新的具体产品类并入系统。它比其他创建型模式更为灵活,因为客户可以在运行时刻建立和删除原型
2)改变值以指定新对象
高度动态的系统允许你通过对象复合定义新的行为-例如,通过为一个对象变量指定值-并且不定义新的类。你通过实例化已有类并且将这些实例注册为客户对象的原型,就可以有效定义新类别的对象。客户可以将职责代理给原型,从而表现出新的行为。这种设计使得用户无需编程即可定义新“类”。实际上,克隆一个原型类似于实例化一个类。Prototype模式可以极大的减少系统所需要的类的数目
。
3)改变结构以指定新对象
许多应用由部件和子部件来创建对象。例如电路设计编辑器就是由子电路来构造电路的。。为方便起见,这样的应用通常允许你实例化复杂的、用户定义的结构,比方说,一次又一次的重复使用一个特定的子电路。Prototype模式也支持这一点。我们仅需将这个子电路作为一个原型增加到可用的电路元素选择板中。只要复合电路对象将Clone实现为一个深拷贝(deep copy )
,具有不同结构的电路就可以是原型了。
4)减少子类的构造
Factory Method ( 3.3)经常产生一个与产品类层次平行的Creator类层次。Prototype模式使得你克隆一个原型而不是请求一个工厂方法去产生一个新的对象。因此你根本不需要Creator类层次。
5)用类动态配置应用
一些运行时刻环境允许你动态将类装载到应用中。Prototype的主要缺陷
是每一个Prototype的子类都必须实现Clone操作,这可能很困难。例如,当所考虑的类已经存在时就难以新增Clone操作。当内部包括一些不支持拷贝或有循环引用的对象时,实现克隆可能也会很困难的。
单例(Singleton)模式-对象行为型:
即在整个生命周期中,对于该对象的生产始终都是一个,不曾变化。保证了一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决
:一个全局使用的类频繁地创建与销毁。何时使用
:当您想控制实例数目,节省系统资源的时候。如何解决
:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。关键代码:
公有的方法获取实例, 私有的构造方法,私有的成员变量。应用实例:
1 、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
2 、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
3 、要求生产唯一序列号。
4 、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
5 、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
优点:在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
实现
一,饿汉式
饿汉式单例关键在于singleton作为类变量并且直接得到了初始化
,即类中所有的变量都会被初始化
singleton作为类变量在初始化的过程中会被收集进<clinit>()
方法中,该方法能够百分之百的保证同步
,但是因为不是懒加载
,singleton被加载后可能很长一段时间不被使用,即实例所开辟的空间会存在很长时间.
1 | package com.liruilong.singleton; |
二,懒汉式
懒汉式单例模式,可以保证懒加载,但是线程不安全, 当有两个线程访问时,不能保证单例的唯一性
1 | package com.liruilong.singleton; |
三,懒汉式加同步方法
懒汉式+同步方法单例模式,即能保证懒加载,又可以保证singleton实例的唯一性
,但是synchronizeed关键字的排他性导致getInstance0()方法只能在同一时间被一个线程访问。性能低下
。
1 | package com.liruilong.singleton; |
四,双重效验锁单例
双重校验锁单例(Double-Check)+Volatile
对懒汉-同步方法的改进,当有两个线程发现singleton为null时,只有一个线程可以进入到同步代码块里。
即满足了懒加载,又保证了线程的唯一性,不加volition的缺点,有时候可能会报NPE,(JVM运行指令重排序),有可能实例对象的变量未完成实例化其他线程去获取到singleton变量。未完成初始化的实例调用其方法会抛出空指针异常。
1 | package com.liruilong.singleton; |
五,静态内部类单例
静态内部类的单例模式
在Singleton类初始化并不会创建Singleton实例,在静态内部类中定义了singleton实例。 当给静态内部类被主动创建时则会创建Singleton静态变量,是最好的单例模式之一,;类似于静态工厂,将类的创建延迟到静态内部类,外部类的初始化不会实例化静态内部类的静态变量。
1 | package com.liruilong.singleton; |
六,枚举类单例
基于枚举类线程安全
枚举类型不允许被继承,同样线程安全的,且只能被实例化一次。
1 | package com.liruilong.singleton; |
GOF描述
Singleton模式有许多优点:
1)
对唯一实例的受控访问
因为Singleton类封装它的唯一实例,所以它可以严格的控制
2)缩小名空间
Singleton模式是对全局变量的一种改进。它避免了那些存储唯一实例的全局变量污染名空间。
3)允许对操作和表示的精化
Singleton类可以有子类,而且用这个扩展类的实例来配置一个应用是很容易的。你可以用你所需要的类的实例在运行时刻配置应用。
4)允许可变数目的实例
这个模式使得你易于改变你的想法,并允许Singleton类的多个,实例。此外,你可以用相同的方法来控制应用所使用的实例的数目。只有允许访问Singleton实例的操作需要改变。
5)比类操作更灵活
另一种封装单件功能的方式是使用类操作(即C++中的静态成员函数或者是Smallak中的类方法),但这两种语言技术都难以改变设计以允许一个类有多个实例。此外, C++中的静态成员函数不是虚函数,因此子类不能多态的重定义它们。
建造者/生成器(Builder)模式-对象创建型
建造者模式(Builder Pattern)
使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。一个 Builder 类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。
意图
:**将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。**主要解决
:主要解决在软件系统中,有时候面临着”一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。何时使用
:一些基本部件不会变,而其组合经常变化的时候。如何解决
:将变与不变分离开。应用实例:
- 、去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的”套餐”。
- 、JAVA 中的 StringBuilder。
- 、构建一个Excel表格,不通数据可能有不同的构建逻辑。形成不同的表格
优点
: 1、建造者独立,易扩展。 2、便于控制细节风险。缺点
: 1、产品必须有共同点,范围有限制。 2、如内部变化复杂,会有很多的建造类。使用场景
: 1、需要生成的对象具有复杂的内部结构。 2、需要生成的对象内部属性本身相互依赖。注意事项
:与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。
实现
我们假设一个快餐店的商业案例,其中,一个典型的套餐可以是一个汉堡(Burger)和一杯冷饮(Cold drink)。汉堡(Burger)可以是素食汉堡(Veg Burger)或鸡肉汉堡(Chicken Burger),它们是包在纸盒中。冷饮(Cold drink)可以是可口可乐(coke)或百事可乐(pepsi),它们是装在瓶子中。
我们将创建一个表示食物条目(比如汉堡和冷饮)的 Item 接口和实现 Item 接口的实体类,以及一个表示食物包装的 Packing 接口和实现 Packing 接口的实体类,汉堡是包在纸盒中,冷饮是装在瓶子中。
然后我们创建一个 Meal 类,带有 Item 的 ArrayList 和一个通过结合 Item 来创建不同类型的 Meal 对象的 MealBuilder。BuilderPatternDemo 类使用 MealBuilder 来创建一个 Meal。
创建一个表示食物条目和食物包装的接口。
1 | //食物条目的接口 |
创建实现 Packing 食物包装接口的实体类。
1 | //纸盒包装 |
创建实现 食物条目Item 接口的抽象类,该类提供了默认的功能。
1 | //汉堡 |
创建扩展了 Burger 和 ColdDrink 的实体类。
1 | //汉堡扩展-素食汉堡 |
创建一个 Meal 套餐类,带有上面定义的 Item 食物条目对象。
1 | import java.util.ArrayList; |
·创建一个 MealBuilder 套餐构建类,实际的 builder 类负责创建 Meal 对象。
1 | public class MealBuilder { |
BuiderPatternDemo 使用 MealBuilder 来演示建造者模式(Builder Pattern)。
1 | public class BuilderPatternDemo { |
GOF描述
- 1)
它使你可以改变一个产品的内部表示
Builder对象提供给导向器一个构造产品的抽象接口。该接口使得生成器可以隐藏这个产品的表示和内部结构。它同时也隐藏了该产品是如何装配的。因为产品是通过抽象接口构造的,你在改变该产品的内部表示时所要做的只是定义一个新的生成器。- 2)
它将构造代码和表示代码分开
, Builder模式通过封装一个复杂对象的创建和表示方式提高了对象的模块性。客户不需要知道定义产品内部结构的类的所有信息;这些类是不出现在Builder接口中的。每个ConcreteBuilder包含了创建和装配一个特定产品的所有代码。这些代码只需要写一次;然后不同的Director可以复用它以在相同部件集合的基础上构作不同的Product,在前面的RTF例子中,我们可以为RTF格式以外的格式定义一个阅读器,比如一个SGMLReader,并使用相同的TextConverter生成SGMLX档的ASCIIText, TexText和TextWidget译本。- 3)
它使你可对构造过程进行更精细的控制
Builder模式与一下子就生成产品的创建型模式不同,它是在导向者的控制下一步一步构造产品的。仅当该产品完成时导向者才从生成器中取回它。因此Builder接口相比其他创建型模式能更好的反映产品的构造过程。这使你可以更精细的控制构建过程,从而能更精细的控制所得产品的内部结构。
工厂方法(Factory methon)模式-对象创建型
工厂方法模式
定义了一个创建对象的接口
,但由子类决定要实例化的类
是哪一个。工厂方法让类把实例化推迟到子类
。
情境:如何将实例化具体类的代码从应用中抽离,或者封装起来,使它们不会干扰应用的其他部分?
在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
分为:简单工厂模式,工厂方法模式
区别:
优点
:
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
- 屏蔽产品的具体实现,调用者只关心产品的接口。
- . 对于静态工厂来讲,不必在每次
调用它们的时候都创建一个新对象
。 它从来不创建对象。 这种方法类似于享元 (Flyweight)模式 。 如果程序经常请求创建相同的对象,并且创建对象的代价很高,则这项技术可以极大地提升性能。它们可以返回原返回类型的任何子类 型的对象。 所返回的对象的类可以随着每次调用而发生变化,这取 决于静态工厂方法的参数值。 性能上优于构造方法。
缺点
:
- 每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加.
- 在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
使用场景
:
- 日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
- 数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。
- 设计一个连接服务器的框架,需要三个协议,”POP3”、”IMAP”、”HTTP”,可以把这三个作为产品类,共同实现一个接口。
注意事项
:作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
简单工厂模式(jdk源码Boolean对象获取):
静态工厂方法
实现jdk源码Boolean对象通过静态工厂生成
1 | /** |
工厂方法模式 :
实例构建不同类型的小汽车,对于简单工厂模式,有一个问题就是,
类的创建依赖工厂类
,也就是说,如果想要拓展程序
,必须对工厂类进行修改
。假如增加其他,工厂类需要修改,不满足开闭原则
如何解决?就用到工厂方法模式
,创建一个工厂接口
和创建多个工厂实现类
,这样一旦需要增加新的功能
,直接增加新的工厂类
就可以了,不需要修改之前的代码
。
比如:比亚迪的产品线原来生成汽车的,后来疫情来了,要生产口罩,使用工厂方法模式后,核心的产品模式没变,只需要增加新的生产口罩的工厂类就可以了,不需要改变之前生产汽车的工厂。
1 | /** |
回头看一下
工厂方法模式
定义了一个创建对象的接口
,但由子类决定要实例化的类
是哪一个。工厂方法让类把实例化推迟到子类
。
设计模式_可复用面向对象软件的基础中的类图
适配器模式(Adapter)-类对象结构型
适配器模式将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。在实际情况和需求之间,填补两者之间的差异的设计模式,即将一个类的接口转换为客户期望的另一个接口,
适配器有两种
:类适配器模式(使用继承的适配器)和对象适配器模式(使用委托的适配器).
- 类适配器模式:使用继承,即适配器继承于被适配者,实现了需求对象接口,可以理解为需求对象接口将需求告诉适配器(实现了接口方法),适配器对被适配者进行加工,然后满足客户需求.
- 对象适配器模式:使用委托,适配器继承了需求抽象类,与被适配者为聚合关系.可以理解需求对象抽象类将需求告诉适配器(实现了抽象类方法),适配器委托被适配者进行加工.然后满足客户需求.
情境:对于策略模式中使用的鸭子,当我们需要一只鸭子的时候,使用鸭子的接口实现一只火鸡的,这样火鸡看上来就是一只鸭子,
1 | package headfirst.designpatterns.adapter.ducks; |
1 | package headfirst.designpatterns.adapter.ducks; |
1 | package headfirst.designpatterns.adapter.ducks; |
同理,将交流的220V的电由ac适配器转换为直流电100v,ac适配器就充当适配角色.
- 客户通过目标接口调用适配器的方法对适配器发出请求。
- 适配器使用
被适配者接口
把请求转换成被适配者
的一个或多个调用接口
。- 客户接收到调用的结果,但并未察觉这一切是适配器在起转换作用。
1 | `问`:一个适配器需要做多少“适配”的工作?如果我需要实现一个很大的目标接口,似乎有“很多”工作要做。 |
真实世界的适配器
如果你已经使用过Java,可能记得早期的集合(collection)类型(例如: Vector.Stack, Hashtable)都实现了一个名为.· elements()
的方法。该方法会返回一个.Enumeration
。这个Enumeration接口可以逐一走过此集合内的每个元素,而无需知道它们在集合内是如何被管理的。
当Sun推出更新后的集合类时,开始使用了Iterator
(迭代器)接口,这个接口和枚举接·口很像,都可以让你遍历此集合类型内的每个元素,但不同的是,迭代器还提供了删除元素的能力。
将枚举适配到迭代器
我们需要一个适配器,实现了目标接口,而此目标接口是由被适配接口组合的,hasNext()和next()方法很容易实现,直接把它们从目标对应到适配者就可以了,但是对于remove)方法,我们又该怎么办?
1 | package headfirst.designpatterns.adapter.iterenum; |
GOF描述
类适配器和对象适配器有不同的权衡。
类适配器
:用一个具体的Adapter类对Adaptee和Target进行匹配。结果是当我们想要匹配一个类以及所有它的子类时,类Adapter将不能胜任工作。使得Adapter可以重定义Adaptee的部分行为,因为Adapter是Adaptee的一个子类。仅仅引入了一个对象,并不需要额外的指针以间接得到adaptee。对象适配器则
:允许一个Adapter与多个Adaptee-即Adaptee本身以及它的所有子类(如果有子类的话)一同时工作。Adapter也可以一次给所有的Adaptee添加功能。使得重定义Adaptee的行为比较困难。这就需要生成Adaptee的子类并且使得Adapter引用这个子类而不是引用Adaptee本身。
使用Adapter模式时需要考虑的其他一些因素有:
- 1)
Adapter的匹配程度
对Adaptee的接口与Target的接口进行匹配的工作量各个Adapter可能不一样。工作范围可能是,从简单的接口转换(例如改变操作名)到支持完全不同的操作集合。Adapter的工作量取决于Target接口与Adaptee接口的相似程度。
- 2)
可插入的Adapter
当其他的类使用一个类时,如果所需的假定条件越少,这个类就更具可复用性。如果将接口匹配构建为一个类,就不需要假定对其他的类可见的是一个相同的接口。也就是说,接口匹配使得我们可以将自己的类加入到一些现有的系统中去,而这些系统对这个类的接口可能会有所不同。Object-Work/Smalltalk(Par90]使用pluggable adapter-词描述那些具有内部接口适配的类。 - 3)
使用双向适配器提供透明操作
使用适配器的一个潜在问题是,它们不对所有的客户都透明。被适配的对象不再兼容Adaptee的接口,因此并不是所有Adaptee对象可以被使用的
外观模式(Facade)-对象结构型
外观模式提供了一个统一的接口
,用来访问子系统中的一群接口
。外观定义了一个高层接口,让子系统更容易使用。
外观模式隐藏
系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口
。这种类型的设计模式属于结构型模式
,它向现有的系统添加一个接口
,来隐藏系统的复杂性。这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。
意图
:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。主要解决
:降低访问复杂系统的内部子系统时的复杂度,简化客户端与之的接口。何时使用
: 1、客户端不需要知道系统内部的复杂联系,整个系统只需提供一个”接待员”即可。 2、定义系统的入口。如何解决
:客户端不与系统耦合,外观类与系统耦合。关键代码
:在客户端和复杂系统之间再加一层,这一层将调用顺序、依赖关系等处理好。应用实例
: 1、去医院看病,可能要去挂号、门诊、划价、取药,让患者或患者家属觉得很复杂,如果有提供接待人员,只让接待人员来处理,就很方便。 2、winOS 的图形化界面和linuxOS的命令行方式,使用linuxOS分盘需要分区,格式化,挂载,而使用winOS快捷键只需要一步就可以。优点
: 1、减少系统相互依赖。 2、提高灵活性。 3、提高了安全性。缺点
:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。使用场景
: 1、为复杂的模块或子系统提供外界访问的模块。 2、子系统相对独立。 3、预防低水平人员带来的风险。注意事项
:在层次化结构中,可以使用外观模式定义系统中每一层的入口。情境
:家庭影院在播放电影的时候需要操作很多的设备,繁琐的很,这时候想通过遥控器来调用这些所有的设备开始工作,遥控器就可以看做是其他设备的一个外观样子,这种行为称为外观模式。
源代码设备接口很多,这里仅摘部分。
HomeTheaterFacade.java
外观类
1 | package headfirst.designpatterns.facade.hometheater; |
Amplifier.java
子系统类
1 | package headfirst.designpatterns.facade.hometheater; |
外观不只是简化了接口,也将客户从组件的子系统中解耦。外观
和适配器
可以包装许多类但是外观的意图是简化接口
,而适配器的意图是将接口转换成不同接口
。
最少知识(Least Knowledge)原则告诉我们要减少对象之间的交互,只留下几个“密友” 。这个原则通常是这么说的:设计原则,最少知识原则:只和你的密友谈话。
如何不要赢得太多的朋友和影响太多的对象
:
究竟要怎样才能避免这样呢?这个原则提供了一些方针:就·任何对象而言·,在该对象的方法内,我们只应该调用属干以下范围的方法;
- 该对象本身
- 被当做方法的参数而传递进来的对象
- 此方法所创建或实例化的任何对象
- 对象的任何组件
这听起来有点严厉,不是吗?如果调用从另一个调用中返回的对象的方法,会有什么害处呢?
如果我们这样做,相当于向另一个对象的子部分发请求(而增加我们直接认识·的对象数目)
。在这种情况下,原则要我们改为要求该对象为我们做出请求
,这么一来,我们就不需要认识该对象的组件了(让我们的朋友圈子维持在最小的状态)。比方说:
GOF描述
Facade模式有下面一些优点:
- 1)它对
客户屏蔽子系统组件
,因而减少了客户处理的对象的数目
并使得子系统使用起来更加方便。- 2)
它实现了子系统与客户之间的松耦合关系
,而子系统内部
的功能组件往往是紧耦合的
。松耦合关系使得子系统的组件变化不会影响到它的客户。Facade模式有助于建立层次结构系统
,也有助于对对象之间的依赖关系分层
。Facade模式
可以消除复杂的循环依赖关系
。这一点在客户程序与子系统是分别实现的时候尤为重要。
在大型软件系统中降低编译依赖性
至关重要。在子系统类改变时,希望尽量减少重编译工作以节省时间。用Facade可以降低编译依赖性,限制重要系统中较小的变化所需的重编译工作。Facade模式同样也有利于简化系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。???不理解這個- 3)如果应用需要,它并不限制它们使用子系统类。因此你可以在系统易用性和通用性之间加以选择。
模板(TemplateMethod)方法-类行为型
模板方法定义一个操作中的算法的骨架,而将一些步骤延迟到子类中
。TemplateMethod使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。定义了一个算法的步骤,并允许子类为一个或多个步骤提供实现。
模板方法可以使得子类在不改变算法算法结构的情况下,重新定义算法中的某些步骤.Thread类中,start方法就是一种模板方法模式的应用,对于不同的实现,有不同的run()方法.
我们看一个简单一点的:
1 | /** |
具体的实现
1 |
|
应用
1 | package com.liruilong.design_pattern.TemplaterMethod; |
嘻嘻,然后我们看看书里怎么讲的?
这个模式是用来创建一个算法的模板。什么是模板?如你所见的,模板就是一个方法。更具体地说,这个方法将算法定义成一组步骤,其中的任何步骤都可以是抽象的,由子类负责实现。这可以确保算法的结构保持不变,同时由子类是供部分实现。
模板的灵活性,使用钩子
钩子
是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在,可以让子类有能力对算法的不同点进行挂·钩。要不要挂钩,由子类自行决定。钩子有好几种用途,让我们先看其中一个,稍后再看其他几个:
好莱坞原则
别调用(打电话给)我们,我们会调用(打电话给)你。
好菜坞原则可以给我们一种防止“依赖腐败
”的方法。当高层组件依赖低层组件,而低层组件又依赖高层组件,而高层组件又依赖边侧组件,而边侧组件又依赖低层组件时,依赖腐败就发生了。在这种情况下,没有人可以轻易地搞懂系统是如何设计的。在好莱坞原则之下,我们允许低层组件将自己挂钩到系统上,但是高层组件会决定什么时候和怎样使用这些低层组件。换句话说,高层组件对待低层组件的方式是“别调用我们,我们会调用你
,你只要告诉我需要调用的信号,具体怎么调用不用你来操作。
问: 好莱坞原则
和依赖倒置原则
之间的关系如何?
答:依赖倒置原则教我们尽量避免使用具体类,而多使用抽象
。而好莱坞原则是用在创建框架或组件上的一种技巧
,好让低层组件能够被挂钩进计算
中,而且又不会让高层组件依赖低层组件
。两者的目标都是在于解耦
,但是依赖倒置原则更加注重如何在设计中避免依赖
。好莱坞原则教我们一个技巧
,创建一个有弹性的设计
,允许低层结构能够互相操作,而又防止其他类太过依赖它们。
问:低层组件不可以调用层组件中的方法吗?
答:并不尽想。事实上虽层组件在结束时,常常会调用从超类中继承来的方法。我们所要做的是,避免让高层和低层组件之间有明显的环状依赖。
JDK源码的应用,使用模板方法排序:Arrays的sort方法
嘻嘻,好激动,我们又要看源码啦!!额..java的水太深啦,啥都看不懂…..
1 | /* |
字符串类比较的一个例子:
1 |
|
GOF描述
模板方法是一种代码复用的基本技术。它们在类库中尤为重要,它们提取了类库中的公共行为。
模板方法
导致一种反向的控制结构,这种结构有时被称为“好莱坞法则”
,即“别找我们,我们找你”
。这指的是一个父类调用一个子类的操作,而不是相反。模板方法调用下列类型的操作:,
- 具体的操作(ConcreteClass或对客户类的操作)。
- 具体的AbstractClass的操作(即,通常对子类有用的操作)。
- 原语操作(即,抽象操作)。
- Factory Method (见Factory Method (3.5)。
- 钩子操作( hook operations ),它提供了缺省的行为,子类可以在必要时进行扩展。一个钩子操作在缺省操作通常是一个空操作。
很重要的一点是模板方法应该指明哪些操作是钩子操作(可以被重定义)
以及哪些是抽象操作(必须被重定义)
,要有效地重用一个抽象类,子类编写者必须明确了解哪些操作是设计为有待重定义的。子类可以通过
重定义父类
的操作来扩展该操作的行为,其间可显式地调用父类操作
。不幸的是,人们很容易忘记去调用被继承的行为。我们可以将这样一个操作转换为一个模板方法,以使得父类可以对子类的扩展方式进行控制
。也就是,在父类的模板方法中调用钩子操作。子类可以重定义这个钩子操作:**
桥接(Bridge)模式-对象结构型
桥接(Bridge)
是用于把抽象化与实现化解耦
,使得二者可以独立变化。这种类型的设计模式属于结构型模式
,它通过提供抽象化和实现化之间
的桥接结构
,来实现二者的解耦。与策略模式类似,所以本书里没有这个模式
这种模式涉及到一个作为桥接的接口
,使得实体类的功能独立于接口实现类
。这两种类型的类可被结构化改变而互不影响。
情景: 1、如果一个系统需要在
构件的抽象化角色和具体化角色之间
增加更多的灵活性
,避免在两个层次之间建立静态的继承联系
,通过桥接模式
可以使它们在抽象层建立一个关联关系
。 2、对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。 3、一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。意图
:将抽象部分与实现部分分离,使它们都可以独立的变化。主要解决
:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。何时使用
:实现系统可能有多个角度分类,每一种角度都可能变化。如何解决
:把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。关键代码
:抽象类依赖实现类
。
应用实例: 1、猪八戒从天蓬元帅转世投胎到猪,转世投胎的机制将尘世划分为两个等级,即:灵魂和肉体,前者相当于抽象化,后者相当于实现化。生灵通过功能的委派,调用肉体对象的功能,使得生灵可以动态地选择。 2、墙上的开关,可以看到的开关是抽象的,不用管里面具体怎么实现的。
优点
:1、抽象和实现的分离。 2、优秀的扩展能力。 3、实现细节对客户透明。缺点
:桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
我们通过下面的实例来演示桥接模式(Bridge Pattern)的用法。其中,可以使用相同的抽象类方法但是不同的桥接实现类,来画出不同颜色的圆。
注意事项:对于两个独立变化的维度,使用桥接模式再适合不过了。
创建桥接实现接口DrawAPI.java
1 | public interface DrawAPI { |
创建实现了 DrawAPI 接口的实体桥接实现类。RedCircle.java,GreenCircle.java
1 | public class RedCircle implements DrawAPI { |
使用 DrawAPI 接口创建抽象类 Shape。Shape.java
1 | public abstract class Shape { |
创建实现了 Shape 抽象类的实体类。Circle.java
1 | public class Circle extends Shape { |
使用 Shape 和 DrawAPI 类画出不同颜色的圆。BridgePatternDemo.java
1 | public class BridgePatternDemo { |
GOF描述
1)
分离接口及其实现部分
: 一个实现未必不变地绑定在一个接口上。抽象类的实现可以在运行时刻进行配置,一个对象甚至可以在运行时刻改变它的实现。将Abstraction与Implementor分离有助于降低对实现部分编译时刻的依赖性
,当改变一个实现类时,并不需要重新编译Abstraction类和它的客户程序。为了保证一个类库的不同版本之间的二进制兼容性,一定要有这个性质。另外,接口与实现分离有助于分层
,从而产生更好的结构化系统
,系统的高层部分仅需知道Abstraction和Implementor即可。
2)提高可扩充性你可以独立地对Abstraction和Implementor层次结构进行扩充。
3)实现细节对客户透明
你可以对客户隐藏实现细节,例如共享Implementor对象以及相应的引用计数机制(如果有的话)。
装饰者(Decorator)模式-对象结构型
装饰者模式
动态地将责任附加到对象上。若要
扩展功能`,装饰者提供了比继承更有弹性的替代方案。
情景:咖啡不同的配料有不同的价格,由于配料繁多,组合不同,价格不同,通过继承的方式会照成
类爆炸
,而且后期价格调整,需要修改继承类,维护特别麻烦,同过属性的方式处理,当价格变动,或者对配量有要求时,我们还是需要调整类的结构。特别麻烦,不符合开闭原则。所以在咖啡问题上我们遇到的问题:类数量爆炸
、设计死板
,以及基类加入的新功能并不适用于所有的子类。
,
利用组合(composition)和委托(delegation)可以在运行时動態具有继承行为的效果,利用继承设计子类的行为,是在编译时静态决定的,而且所有的子类都会继承到相同的行为。然而,如果能够利用组合的做法扩展对象的行为,就可以在运行时动态地进行扩展。
设计原则:类应该对扩展开放,对修改关闭(开放 -关闭原则)
上面問題换一种思路,我们把变化的部分提出来,我们要以饮料为主体,然后在运行时以调料来“装饰”(decorate)饮料。比方说,如果顾客想要摩卡和奶泡深焙咖啡,那么,要做的是:
1 拿一个咖啡(DarkRoast)对象
2 以摩卡(Mocha)对象装饰它
3 以奶泡(Whip)对象装饰它
4 调用cost()方法,并依赖委托(delegate)将调料的价钱加上去。
装饰者和被装饰对象有相同的超类型。
你可以用一个或多个装饰者包装一个对象。
既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合,
可以用装饰过的对象代替它。
装饰者可以在所委托被装饰者的行为之前与/或之后,加上自己的行为,以达到特定的目的。
对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用你喜欢的装饰者来装饰对象那个咖啡的例子
Beverage.java
1 | package com.liruilong.design_pattern.Decorator; |
DarkRoast.java
1 | package com.liruilong.design_pattern.Decorator; |
Decaf.java
1 | package com.liruilong.design_pattern.Decorator; |
Espresso.java
1 | package com.liruilong.design_pattern.Decorator; |
HouseBlend.java
1 | package com.liruilong.design_pattern.Decorator; |
CondimentDecorator.java
1 | package com.liruilong.design_pattern.Decorator; |
Mocha.java
1 | package com.liruilong.design_pattern.Decorator; |
Soy.java
1 | package com.liruilong.design_pattern.Decorator; |
Whip.java
1 | package com.liruilong.design_pattern.Decorator; |
StarbuzzCoffee.java
1 | package com.liruilong.design_pattern.Decorator; |
1 | D:\Java\jdk1.8.0_251\bin\java.exe "-javaagent:E:\学习软件\IntelliJ IDEA 2018.3.1\lib\idea_rt.jar=4118:E:\学习软件\IntelliJ IDEA 2018.3.1\bin" -Dfile.encoding=UTF-8 -classpath D:\Java\jdk1.8.0_251\jre\lib\charsets.jar;D:\Java\jdk1.8.0_251\jre\lib\deploy.jar;D:\Java\jdk1.8.0_251\jre\lib\ext\access-bridge-64.jar;D:\Java\jdk1.8.0_251\jre\lib\ext\cldrdata.jar;D:\Java\jdk1.8.0_251\jre\lib\ext\dnsns.jar;D:\Java\jdk1.8.0_251\jre\lib\ext\jaccess.jar;D:\Java\jdk1.8.0_251\jre\lib\ext\jfxrt.jar;D:\Java\jdk1.8.0_251\jre\lib\ext\localedata.jar;D:\Java\jdk1.8.0_251\jre\lib\ext\nashorn.jar;D:\Java\jdk1.8.0_251\jre\lib\ext\sunec.jar;D:\Java\jdk1.8.0_251\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk1.8.0_251\jre\lib\ext\sunmscapi.jar;D:\Java\jdk1.8.0_251\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk1.8.0_251\jre\lib\ext\zipfs.jar;D:\Java\jdk1.8.0_251\jre\lib\javaws.jar;D:\Java\jdk1.8.0_251\jre\lib\jce.jar;D:\Java\jdk1.8.0_251\jre\lib\jfr.jar;D:\Java\jdk1.8.0_251\jre\lib\jfxswt.jar;D:\Java\jdk1.8.0_251\jre\lib\jsse.jar;D:\Java\jdk1.8.0_251\jre\lib\management-agent.jar;D:\Java\jdk1.8.0_251\jre\lib\plugin.jar;D:\Java\jdk1.8.0_251\jre\lib\resources.jar;D:\Java\jdk1.8.0_251\jre\lib\rt.jar;D:\code\workspack\out\production\workspack;D:\code\workspack\lib\cglib-3.2.12.jar;D:\code\workspack\lib\mysql-connector-java-5.1.18-bin.jar com.liruilong.design_pattern.Decorator.StarbuzzCoffee |
JDK源码的应用,真实世界的装饰者:Java I/O
下面是一个典型的对象集合,用装饰者来将功能结合起来,以读取文件数据,
BufferedInputStream
及LineNumberInputStream
都扩展自FilterInputStream
,而FilterInputStream
是一个抽象的装饰类。和咖啡类对一下,发现基本一样。
FilterInputStream是一个抽象装饰者。
1 | * |
编写自己的java I/O装饰者
编写一个装饰者,把输入流内的所有大写字符转成小写
1 | package com.liruilong.design_pattern.Decorator; |
1 | package com.liruilong.design_pattern.Decorator; |
GOF描述
Decorator模式至少有两个主要优点和两个缺点:
- 1)
比静态继承更灵活
与对象的静态继承(多重继承)相比, Decorator模式提供了更加灵活的向对象添加职责的方式
。可以用添加和分离的方法,用装饰在运行时刻增加和删除职责。相比之下,继承机制要求为每个添加的职责创建一个新的子类(例如, BorderscrollableTextView, BorderedTextView ),这会产生许多新的类,并且会增加系统的复杂度。此外,为一个特定的Component类提供多个不同的Decorator类,这就使得你可以对一些职责进行混合和匹配
。使用Decorator模式可以很容易地重复添加一个特性,例如在TextView上添加双边框时,仅需将添加两个BorderDecorator即可。而两次继承Border类则极容易出错的- 2)
避免在层次结构高层的类有太多的特征
Decorator模式提供了一种“即用即付”的方法来添加职责。它并不试图在一个复杂的可定制的类中支持所有可预见的特征,相反,你可以定义一个简单的类,并且用Decorator类给它逐渐地添加功能。可以从简单的部件组合出复杂的功能
。这样,应用程序不必为不需要的特征付出代价。同时也更易于不依赖于Decorator昕扩展(甚至是不可预知的扩展)的类而独立地定义新类型的Decorator。扩展一个复杂类的时候,很可能会暴露与添加的职责无关的细节。- 3)
Decorator与它的Component不一样
Decorator是一个透明的包装
。如果我们从对象标识的观点出发,一个被装饰了的组件与这个组件是有差别的,因此,使用装饰时不应该依赖对象标识
。- 4)
有许多小对象
采用Decorator模式进行系统设计往往会产生许多看上去类似的小对象这些对象仅仅在他们相互连接的方式上有所不同,而不是它们的类或是它们的属性值有所不同。尽管对于那些了解这些系统的人来说,很容易对它们进行定制,但是很难学习这些系统,排错也很困难。
迭代器(Iterator)模式-对象行为型
提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。
动机
:一个聚合对象
,如列表(ist),应该提供一种方法来让别人可以访问它的元素,而又不需暴露它的内部结构,此外,针对不同的需要,可能要以不同的方式遍历这个列表。但是即使可以预见到所需的那些遍历操作,你可能也不希望列表的接口中充斥着各种不同遍历的操作。有,时还可能需要在同一个表列上同时进行多个遍历。迭代器模式都可帮你解决所有这些问题。
关键思想是将对列表的访问和遍历从列表对象中分离出来并放入一个迭代器(iterator)对象中
。迭代器类定义了一个访问该列表元素的接口。迭代器对象负责跟踪当前的元素;即,它知道哪些元素已经遍历过了。
在实例化列表迭代器之前,必须提供待遍历的列表。一旦有了该列表迭代器的实例,就可以顺序地访问该列表的各个元素。 Currentitem
操作返回表列中的当前元素, First
操作初始化迭代器,使当前元素指向列表的第一个元素, Next
操作将当前元素指针向前推进一步,指向下一个元素,而IsDone
检查是否已越过最后一个元素,也就是完成了这次遍历。
将遍历机制与列表对象分离
使我们可以定义不同的迭代器来实现不同的遍历策略,而无需在列表接口中列举它们。例如,过滤表列迭代器(FilteringListiterator)可能只访问那些满足特定过滤约束条件的元素。
注意迭代器和列表是耦合在一起的,而且客户对象必须知道遍历的是一个列表而不是其他聚合结构。最好能有一种办法使得不需改变客户代码即可改变该聚合类。可以通过将迭代器的概念推广到
多态迭代(polymorphic iteration)
来达到这个目标。
例如,假定我们还有一个列表的特殊实现,比如说SkipList, SkipList是一种具有类似于平衡树性质的随机数据结构。我们希望我们的代码对List和Skiplist对象都适用
。首先,定义一个抽象列表类AbstractList
,它提供操作
列表的公共接口
。类似地,我们也需要一个抽象的迭代器类Iterator
,它定义公共的迭代接口
。然后我们可以为每个不同的列表实现定义具体的Iterator子类
。这样迭代机制就与具体的聚合类无关了。
1 | package com.liruilong.design_pattern.Iterator; |
1 | package com.liruilong.design_pattern.Iterator; |
1 | package com.liruilong.design_pattern.Iterator; |
1 | package com.liruilong.design_pattern.Iterator; |
1 | package com.liruilong.design_pattern.Iterator; |
设计原则
:一个类应该只有一个引起变化的原因
类的每个责任都有改变的潜在区域。超过一个责任,意味着超过一个改变的区域。这个原则告诉我们,尽量让每个类保持单一责任
。内聚(cohesion)
这个术语你应该听过,它用来度量一个类或模块紧密地达到单一目的或责任。当一个模块
或一个类
被设计成只支持一组相关的功能时,我们说它具有高内聚
;反之,当被设计成支持一组不相关的功能时,我们说它具有低内聚。内聚
是一个比单一责任原则更普遍的概念,但两者其实关系是很密切的。遵守这个原则的类容易具有很高的凝聚力,而且比背负许多责任的低内聚类更容易维护。
迭代器模式,太不陌生啦。我们看看JDK源码吧,在熟悉熟悉
嘻嘻,是不是发现了简单工厂的影子
1 | public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> { |
源码的设计上迭代器是基于ListIterator是一个功能更加强大的, 它继承于Iterator接口,只能用于各种List类型的访问。
- (1)ListIterator有add()方法,可以向List中添加对象,而Iterator不能
- (2)ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
- (3)ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
- (4)都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public abstract class AbstractCollection<E> implements Collection<E> {
/**
* Sole constructor. (For invocation by subclass constructors, typically
* implicit.)
*/
protected AbstractCollection() {
}
// Query Operations
/**
* Returns an iterator over the elements contained in this collection.
*
* @return an iterator over the elements contained in this collection
*/
public abstract Iterator<E> iterator();
1 | public interface Collection<E> extends Iterable<E> { |
1 | public interface Iterable<T> { |
1 | public interface Iterator<E> { |
JDK1.5的迭代器*增强for循环
java5 包含一种新形式的for语句,称为for/in,这可以让你在一个集合或者一个数组中遍历,而且不需要显长的创建迭代器。
GOF描述
迭代器模式有三个重要的作用:
- 1)
它支持以不同的方式遍历一个聚合
复杂的聚合可用多种方式进行遍历。例如,代码生成和语义检查要遍历语法分析树。代码生成可以按中序或者按前序来遍历语法分析树。迭代器模式使得改变遍历算法变得很容易:仅需用一个不同的迭代器的实例代替原先的实例即可。你也可以自己定义迭代器的子类以支持新的遍历。- 2)
迭代器简化了聚合的接口
有了迭代器的遍历接口,聚合本身就不再需要类似的遍历接口了。这样就简化了聚合的接口。- 3)
在同一个聚合上可以有多个遍历
每个迭代器保持它自己的遍历状态。因此你可以同时进行多个遍历。
享元(Flyweight)模式-对象结构型
享元(Flyweight)模式:享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象
,主要用于减少创建对象的数量
,以减少内存占用和提高性能
。
这种类型的设计模式属于结构型模式
,它提供了减少对象数量从而改善应用所需的对象结构的方式。或者可以理解为细粒度的缓存
意图
:运用共享技术有效地支持大量细粒度的对象。
主要解决`:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
何时使用
: 1、系统中有大量对象。 2、这些对象消耗大量内存。 3、这些对象的状态大部分可以外部化。 4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。 5、系统不依赖于这些对象身份,这些对象是不可分辨的。
如何解决
:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象关键代码
:用 HashMap 存储这些对象。应用实例
: 1、JAVA 中的 String,Intege有对应的常量池,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。 2、数据库的数据池。优点
:大大减少对象的创建,降低系统的内存,使效率提高。缺点
:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。使用场景
: 1、系统有大量相似对象。 2、需要缓冲池的场景。注意事项
: 1、注意划分外部状态和内部状态,否则可能会引起线程安全问题。 2、这些类必须有一个工厂对象加以控制。
实现
:我们将通过创建 5 个对象来画出 20 个分布于不同位置的圆来演示这种模式。由于只有 5 种可用的颜色,所以 color 属性被用来检查现有的 Circle 对象。
创建一个Shape 接口
和实现了 Shape 接口的实体类 Circle。下一步是定义工厂类ShapeFactory
。
ShapeFactory 有一个Circle 的 HashMap
,其中键名为Circle 对象的颜色
。无论何时接收到请求,都会创建一个特定颜色的圆
。ShapeFactory 检查它的 HashMap 中的circle 对象
,如果找到 Circle 对象,则返回该对象,否则将创建一个存储在 hashmap 中以备后续使用的新对象,并把该对象返回到客户端。FlyWeightPatternDemo
类使用ShapeFactory
来获取Shape
对象。它将向ShapeFactory
传递信息(red / green / blue/ black / white),以便获取它所需对象的颜色。
Shape.java
1 | public interface Shape { |
创建实现接口的实体类。Circle.java
1 | public class Circle implements Shape { |
创建一个工厂,生成基于给定信息的实体类的对象。ShapeFactory.java
1 | import java.util.HashMap; |
使用该工厂,通过传递颜色信息来获取实体类的对象。FlyweightPatternDemo.java
1 | public class FlyweightPatternDemo { |
GOF描述
Flyweigh有下列优点和缺点(liabilities):
使用Flyweigh模式时,传输、查找和/或计算外部状态都会产生运行时的开销,尤其当fyweight原先被存储为内部状态时。然而,空间上的节省抵消了这些开销。共享的nyweight越多,空间节省也就越大
1 | 存储节约由以下几个因素决定: |
共享的Flyweight越多,存储节约也就越多
。节约量随着共享状态的增多而增大。当对象使用大量的内部及外部状态,并且外部状态是计算出来的而非存储的时候,节约量将达到最大。所以,可以用两种方法来节约存储:用共享减少内部状态的消耗,用计算时间换取对外部状态的存储。
Flyweight模式经常和Composite 模式结合
起来表示一个层次式结构,这一层次式结构是一个共享叶节点的图
。共享的结果是, Flyweight的叶节点不能存储指向父节点的指针。而父节点的指针将传给Flyweight作为它的外部状态的一部分。这对于该层次结构中对象之间相互通讯的方式将产生很大的影响。
Flyweight模式经常和Composite 模式结合這快不太懂以後在看????
职责链(Chain of Responsibility)模式-对象行为型
责任链模式(Chain of Responsibility Pattern)
为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式
。
在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者
,依此类推。MVC 的异常处理,异常链
意图
:避免请求发送者
与接收者
耦合在一起,让多个对象都有可能接收请求
,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
主要解决
:职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
何时使用
:在处理消息的时候以过滤很多道
,常用于异常处理,构成异常链。如何解决
:拦截的类都实现统一接口。关键代码
:Handler 里面聚合它自己,在 HandlerRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。应用实例
: 1、红楼梦中的”击鼓传花”。 2、JS 中的事件冒泡。 3、JAVA WEB 中 Apache Tomcat 对 Encoding 的处理,Struts2 的拦截器,jsp servlet 的 Filter。使用场景
: 1、有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。 2、在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。 3、可动态指定一组对象处理请求。
注意事项:在 JAVA WEB 中遇到很多应用。
优点
: 1、降低耦合度。它将请求的发送者和接收者解耦
。 2、简化了对象。使得对象不需要知道链的结构。 3、增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。 4、增加新的请求处理类很方便。缺点
: 1、不能保证请求一定被接收。 2、系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。 3、可能不容易观察运行时的特征,有碍于除错。
AbstractLogger.java
1 | public abstract class AbstractLogger { |
创建扩展了该记录器类的实体类。ConsoleLogger.java
1 | public class ConsoleLogger extends AbstractLogger { |
ErrorLogger.java
1 | public class ErrorLogger extends AbstractLogger { |
FileLogger.java
1 | public class FileLogger extends AbstractLogger { |
创建不同类型的记录器。赋予它们不同的错误级别,并在每个记录器中设置下一个记录器。每个记录器中的下一个记录器代表的是链的一部分。ChainPatternDemo.java
1 | public class ChainPatternDemo { |
1 | Standard Console::Logger: This is an information. |
GOF描述
Responsibility链有下列优点和缺点(liabilities):
- 1)
降低耦合度
该模式使得一个对象无需知道是其他哪一个对象处理其请求。对象仅需知道该请求会被“正确”地处理。接收者和发送者都没有对方的明确的信息,且链中的对象不需知道链的结构。结果是,职责链可简化对象的相互连
接。它们仅需保持一个指向其后继者的引用,而不需保持它所有的候选接受者的引用
。- 2)
增强了给对象指派职责(Responsibility)的灵活性
当在对象中分派职责时,职责链给你更多的灵活性。你可以通过在运行时刻对该链进行动态的增加或修改来增加或改变处理一个请求的那些职责。你可以将这种机制与静态的特例化处理对象的继承机制结合起来使用。- 3)
不保证被接受
既然一个请求没有明确的接收者,那么就不能保证它一定会被处理.该请求可能一直到链的末端都得不到处理。一个请求也可能因该链没有被正确配置而得不到处理。
解释器(Interpreter)模式-类行为型
解释器模式(Interpreter Pattern)
提供了评估语言的语法或表达式的方式,它属于行为型模式
。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。
意图
:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。主要解决
:对于一些固定文法构建一个解释句子的解释器。何时使用
:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。如何解决
:构建语法树,定义终结符与非终结符。关键代码
:构建环境类,包含解释器之外的一些全局信息,一般是 HashMap。应用实例
:编译器、运算表达式计算。优点
: 1、可扩展性比较好,灵活。 2、增加了新的解释表达式的方式。 3、易于实现简单文法。缺点
: 1、可利用场景比较少。 2、对于复杂的文法比较难维护。 3、解释器模式会引起类膨胀
。 4、解释器模式采用递归调用方法。使用场景
: 1、可以将一个需要解释执行的语言中的句子表示为一个抽象语法树
。 2、一些重复出现的问题可以用一种简单的语言来进行表达
。 3、一个简单语法需要解释的场景。
注意事项:可利用场景比较少,JAVA 中如果碰到可以用 expression4J 代替。
实现
我们将创建一个接口
Expression
和实现了 Expression 接口的实体类
。定义作为上下文中主要解释器的TerminalExpression
类。其他的类OrExpression
、AndExpression
用于创建组合式表达式。InterpreterPatternDemo
,我们的演示类使用 Expression 类创建规则和演示表达式的解析。
创建一个表达式接口。Expression.java
1 | public interface Expression { |
创建实现了上述接口的实体类。TerminalExpression.java
1 | public class TerminalExpression implements Expression { |
OrExpression.java
1 | public class OrExpression implements Expression { |
AndExpression.java
1 | public class AndExpression implements Expression { |
InterpreterPatternDemo 使用 Expression 类来创建规则,并解析它们。InterpreterPatternDemo.java
1 | /** |
执行程序,输出结果:
1 | 包含字符123 || abc ? true |
GOF描述
解释器模式有下列的优点和不足:
1)
易于改变和扩展文法
因为该模式使用类来表示文法规则,你可使用继承来改变或扩展该文法。已有的表达式可被增量式地改变,而新的表达式可定义为旧表达式的变体。
2)也易于实现文法
定义抽象语法树
中各个节点的类的实现大体类似。这些类易于直接编写,通常它们也可用一个编译器或语法分析程序生成器自动生成。
3)复杂的文法难以维护
解释器模式为文法中的每一条规则至少定义了一个类(使用BNF定义的文法规则需要更多的类)。因此包含许多规则的文法可能难以管理和维护。可应用其他的设计模式来缓解这一问题。但当文法非常复杂时,其他的技术如语法分析程序或编译器生成器更为合适。
4)增加了新的解释表达式的方式
解释器模式使得实现新表达式“计算”变得容易。例如,·你可以在表达式类上定义一个新的操作以支持优美打印或表达式的类型检查。如果你经常创建新的解释表达式的方式,那么可以考虑使用Visitor(5.11)模式以避免修改这些代表文法的类。
中介者(Mediator)模式-对象行为型
中介者模式
(Mediator)是用来降低多个对象
和类之间
的通信复杂性
。用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
意图
:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。主要解决
:对象与对象之间存在大量的关联关系
,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。何时使用
:多个类相互耦合,形成了网状结构。如何解决
:将上述网状结构分离为星型结构。关键代码
:对象 Colleague 之间的通信封装到一个类中单独处理。应用实例
: 1、中国加入 WTO 之前是各个国家相互贸易,结构复杂,现在是各个国家通过 WTO 来互相贸易。 2、机场调度系统。 3、MVC 框架,其中C(控制器)就是 M(模型)和 V(视图)的中介者
。优点
: 1、降低了类的复杂度,将一对多转化成了一对一。 2、各个类之间的解耦。 3、符合迪米特原则
。缺点
:中介者会庞大,变得复杂难以维护。使用场景
: 1、系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。 2、想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。注意事项
:不应当在职责混乱的时候使用。
实现
我们通过聊天室实例来演示中介者模式。实例中,多个用户可以向聊天室发送消息,聊天室向所有的用户显示消息。我们将创建两个类 ChatRoom 和 User。User 对象使用 ChatRoom 方法来分享他们的消息。MediatorPatternDemo,我们的演示类使用 User 对象来显示他们之间的通信。
创建中介类。ChatRoom.java
1 | import java.util.Date; |
创建 user 类。User.java
1 | public class User { |
使用 User 对象来显示他们之间的通信。MediatorPatternDemo.java
1 | public class MediatorPatternDemo { |
执行程序,输出结果:
1 | Thu Jan 31 16:05:46 IST 2013 [Robert] : Hi! John! |
GOF描述
中介者模式有以下优点和缺点:
1)减少了子类生成
Mediator将原本分布于多个对象间的行为集中在一起。改变这些行为只需生成Meditator的子类即可。这样各个Colleague类可被重用。.
2)它将各Colleague解耦
Mediator有利于各Colleague间的松耦合,你可以独立的改变和复用各Colleague类和Mediator类。
3)它简化了对象协议
用Mediator和各Colleague间的一对多的交互来代替多对多的交互。一对多的关系更易于理解、维护和扩展。4)它对对象如何协作进行了抽象
将中介作为一个独立的概念并将其封装在一个对象中,使你将注意力从对象各自本身的行为转移到它们之间的交互上来。这有助于弄清楚一个系统中的对象是如何交互的。
5)它使控制集中化
中介者模式将交互的复杂性变为中介者的复杂性。因为中介者封装了协议,它可能变得比任一个Colleague都复杂。这可能使得中介者自身成为一个难于维护的庞然大物。
备忘录(Memento)模式-对象行为型
备忘录模式(Memento)
保存一个对象的某个状态
,以便在适当的时候恢复对象。备忘录模式属于行为型模式。
意图
:在不破坏封装性
的前提下,捕获一个对象的内部状态
,并在该对象之外保存这个状态。主要解决
:所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。何时使用
:很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有”后悔药”可吃。如何解决
:通过一个备忘录类专门存储对象状态。关键代码
:客户不与备忘录类耦合,与备忘录管理类耦合。应用实例
: 1、后悔药。 2、打游戏时的存档。 3、Windows 里的 ctri + z。 4、IE 中的后退。 4、数据库的事务管理。优点
: 1、给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。 2、实现了信息的封装,使得用户不需要关心状态的保存细节。缺点
:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。使用场景
: 1、需要保存/恢复数据的相关状态场景。 2、提供一个可回滚的操作。注意事项
: 1、为了符合迪米特原则,还要增加一个管理备忘录的类。 2、为了节约内存,可使用原型模式+备忘录模式。
实现
备忘录模式使用三个类
Memento
、Originator
和CareTaker
。Memento
包含了要被恢复的对象的状态。Originator
创建并在 Memento 对象中存储状态。Caretaker
对象负责从Memento
中恢复对象的状态。
MementoPatternDemo,我们的演示类使用 CareTaker 和 Originator 对象来显示对象的状态恢复。
创建 Memento 类。Memento.java
1 | public class Memento { |
创建 Originator 类。Originator.java
1 | public class Originator { |
创建 CareTaker 类。CareTaker.java
1 | import java.util.ArrayList; |
使用 CareTaker 和 Originator 对象。MementoPatternDemo.java
1 | public class MementoPatternDemo { |
验证输出
。
1 | Current State: State #4 |
GOF描述
1)·
保持封装边界
使用备忘录可以避免暴露一些只应由原发器管理却又必须存储在原发器之外的信息。该模式把可能很复杂的Originator内部信息对其他对象屏蔽起来,从而保持了封装边界。.
2)它简化了原发器
在其他的保持封装性的设计中, Originator负责保持客户请求过的内部状态版本。这就把所有存储管理的重任交给了Originator。让客户管理它们请求的状态将会简化Originator,并且使得客户工作结束时无需通知原发器。
3)使用备忘录可能代价很高
如果原发器在生成备忘录时必须拷贝并存储大量的信息,或者客户非常频繁地创建备忘录和恢复原发器状态,可能会导致非常大的开销。除非封装和恢复Originator状态的开销不大,否则该模式可能并不合适。参见实现一节中关于增量式改变的
4)定义窄接口和宽接口
在一些语言中可能难以保证只有原发器可访问备忘录的状态。
5)维护备忘录的潜在代价
管理器负责删除它所维护的备忘录。然而,管理器不知道备忘录中有多少个状态。因此当存储备忘录时,一个本来很小的管理器,可能会产生大量的存储开销。
策略(Strategy)模式-对象行为型
定义了算法族,分别封装起来,让它们之间,可以互相替换,此模式让算法的变化独立于使用算法的客户。
情境:构建一个鸭子超类,当用于扩展不同鸭子子类型,但是有些鸭子不是活的,不能实现超类的行为,比如叫,游泳等,这时候通过覆盖行为或将行为抽象成接口实现他们也是很繁琐的,因为行为有许多。这样行为来自Duck超类的具体实现,或是继承某个接口并由子类自行实现而来。这两种做法都是依赖于“
实现
”,我们被实现绑得死死的,没办法更改行为
(除非写更多代码)。所以当继承解决不了问题的时候…
设计原则:找出应用中可能需要变化之处,把它们独立出
来,不要和那些不需要变化的代码
混在一起。把会变化的部分取出并“封装”起来,以便以后可以轻易地改动或扩充此部分,好让其他部分不会受到影响。结果如何?代码变化引起的不经意后果变少,系统变得更有弹性。
分开变化和不会变化的部分
设计鸭子的行为
鸭子的子类将使用接口(FlyBehavior与QuackBehavior)所表示的行为
设计原则:针对接口编程,而不是针对实现编程
。针对接口编程”真正的意思是“针对超类型(supertype)编程,“针对接口编程”,关键就在多态
。利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被绑死在超类型的行为上
。
我不懂你为什么非要把FlyBehavior设计成接口。为何不使用抽象超类,这样不就可以使用多态了吗?
“针对超类型编程”这句话,可以更明确地说成“变量的声明类型应该是超类型,通常是一个抽象类或者是一个接口,如此,只要是具体实现此超类型的类所产生的对象,都可以指定给这个变量。这也意味着,声明类时不用理会以后执行时的真正对象类型!”简单的多态例子
:假设有一个抽象类Animal,有两个具体的实现(Dog与Cat)继承Animal。
·
实现鸭子的行为
有两个接口,FlyBehavior和QuackBehavior,还有它们对应的类,负责实现具体的行为
- 这样的设计,可以让飞行和呱呱叫的动作被其他的对象复用,因为这些行为已经与鸭子类无关了。
- 而我们可以新增一些行为,不会影响到既有的行为类,也不会影响“使用”到飞行行为的鸭子类。
1 | `问`:用一个类代表一个行为,感觉似乎有点奇怪。类不是应该代表某种“东西”吗?类不是应该同时具备状态“与”行为吗? |
·**
整合鸭子的行为
**
·具体的鸭子(绿头鸭子)通过
子类构造器的方式
录入数据:
结合上面的UML看,(通过实例化类似Quack或FlyWithWings的行为类,并把它指定到行为引用变量中
动态设定行为,通过Setting方法。将构造器里的通过Setting方法获取
回过头来看一下
策略模式:定义了算法族
,分别封装
起来,让它们之间,可以互相替换
,此模式让算法的变化独立于使用算法的客户。
设计原则:多用组合,少用继承
。使用组合建立系统具有很大的弹性,不仅可将算法族封装成类
,更可以“在运行时动态地改变行为"
,只要组合的行为对象符合正确的接口标准即可
。组合用在“许多”设计模式中.
JDK源码策略模式的应用:
Thread类也是一种策略模式的实现,Thread
是客户,Runnable
是算法簇(行为),不同的线程构成算法簇
1 | ............. |
Runnable接口,算法簇接口
1 | * Arthur van Hoff |
看一个具体实现:
1 | + oo基础:抽象封装多态继承 |
其他实例
:java8 的写法
申明一个算法簇接口
1 |
|
实现不同的算法
1 | // 具体的实现 |
使用算法的客户
1 | class Validator { |
GOF描述
Strategy模式有下面的一些优点和缺点:
- 1)相关算法系列 Strategy类层次为Context定义了一系列的
可供重用的算法或行为
。继承有助于·析取出这些算法中的公共功能
- 2)
一个替代继承的方法
继承提供了另一种支持多种算法或行为的方法。你可以直接生成一个Context类的子类,从而给它以不同的行为
。但这会将行为硬行编制到Context中
,而将算法的实现与Context的实现混合起来
,从而使Context难以理解、难以维护和难以扩展,而且还不能动态地改变算法。最后你得到一堆相关的类,它们之间的唯一差别是它们所使用的算法或行为。将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。- 3)
消除了一些条件语句
Strategy模式提供了用条件语句选择所需的行为以外的另一种选择。当不同的行为堆砌在一个类中时,很难避免使用条件语句来选择合适的行为。将行为封装在一个个独立的Strategy类中消除了这些条件语句- 4)实现的选择Strategy模式可以提供相同行为的不同实现。客户可以根据不同时间/空间权衡取舍要求从不同策略中进行选择。
- 5)
客户必须了解不同的Strategy
本模式有一个潜在的缺点,就是一个客户要选择一个合适的Strategy就必须知道这些Strategy到底有何不同。此时可能不得不向客户暴露具体的实现问题。因此仅当这些不同行为变体与客户相关的行为时,才需要使用Strategy模式。- 6)
Strategy和Context之间的通信开销
,无论各个ConcreteStrategy实现的算法是简单还是复杂,它们都共享Strategy定义的接口。因此很可能某些ConcreteStrategy不会都用到所有通过这个接口传递给它们的信息;简单的ConcreteStrategy可能不使用其中的任何信息!这就意味着有时Context会创建和初始化一些永远不会用到的参数。如果存在这样问题;那么将需要在Strategy和Context之间更进行紧密的耦合。7)增加了对象的数目 Strategy增加了一个应用中的对象的数目。有时你可以将Strategy实现为可供各Context共享的无状态的对象来减少这一开销。任何其余的状态都由Context维护。
观察者(Observer)模式-对象行为型
观察者模式
观察者模式定义了对象之间
的一对多依赖
,这样一来,当一个对象改变状态
时,它的所有依赖者都会收到通知并自动更新
。
是JDK中使用最多的模式之一,·
出版者(Subject)+订阅者(Observer)=观察者模式
。
观察者模式的通知机制
与其他设计模式委托机制
是不同的,前者是“一对多”的对象之间的通信
,后者是“一对一”的对象之间的通信
。
情境:一个气象观测站,需要实时更新观测的信息,分别发布到三个公告栏上,一旦气象测量更新,调用更新的方法,不同的公告栏调用相同的更新方法,类似于
报纸和杂志的订阅
.这种发布-订阅
的模式为观察者模式,对于观察者模式来讲,在需要观察的时候要注册
,不需要时取消订阅
,数据更新时,通知
现有观察者.
观察者模式定义了一系列对象之间的一对多关系。当一个对象改变状态,其他依赖者都会收到通知。
·
定义观察者模式
松耦合的威力:观察者模式提供了一种对象设计,让主题和观察者
之间松耦合。
设计原则:为了交互对象之间的松耦合设计而努力。松耦合的设计之所以能让我们建立有弹性的OO系统,能够应对变化,是因为对象之间的互相依赖降到了最低。
回到气象观测站
WeatherData类具有getter方法,可以取得三个测量值:温度、湿度与气压
当新的测量数据备妥时,measurementsChanged()方法就会被调用
实现三个使用天气数据的布告板:“目前状况”布告、“气象统计”布告、“天气预报”布告
1 | `问 `:为什么要保存对Subject的引用呢?构造完后似乎用不着了呀? |
代码实现
Subject.java
1 | package com.liruilong.design_pattern.Observer; |
Observer.java
1 | package com.liruilong.design_pattern.Observer; |
DisplayElement.java
1 | package com.liruilong.design_pattern.Observer; |
WeatherData.java
1 | package com.liruilong.design_pattern.Observer; |
CurrentConditionsDisplay.java
1 | package com.liruilong.design_pattern.Observer; |
WeatherStation.java
测试类,通过setMeasurements改变值触发订阅推送。
1 | package com.liruilong.design_pattern.Observer; |
使用JDK内置的观察者模式
Java API
有内置的观察者模式
。java.util包(package)
内包含最基本的Observer接口与Observable类
,这和我们的Subject接口与Observer接口很相似。Observer接口与Observable类使用上更方便,因为许多功能都已经事先准备好了。你甚至可以使用推(push)
或拉(pull)
的方式传送数据
Java内置的观察者模式如何运作,嘻嘻,源码很容易懂,直接上源码,多了一个更新状态标识
Observer .java
1 | package java.util; |
Observable.java
1 | package java.util; |
利用内置的支持重做气象站
CurrentConditionsDisplay.java
1 | package com.liruilong.design_pattern.Observerjdk.Observer; |
WeatherData.java
1 | package com.liruilong.design_pattern.Observerjdk.Observer; |
1 | oo原则: 为交互对象之间的松耦合设计而努力 |
用java8写一个观察者
申明一个观察者接口,用来泛化观察行为
1 | /** |
申明几个观察者
1 | // 申明不同的观察者,定义不同的行为 |
申明主题
1 | interface Subject{ |
发布-订阅
1 | /** |
回过头来看一下
观察者模式:在对象之间定义一对多
的依赖,这样一来,当一个对象改变状态
,依赖它的对象
都会收到通知,并自动更新。观察者模式的代表人物——·MVC··
GOF描述
Observer模式允许你
独立的改变目标和观察者
。你可以单独复用目标对象而无需同时复用其观察者
,反之亦然。它也使你可以在不改动目标和其他的观察者的前提下增加观察者
。下面是观察者模式其它一些优缺点:
- 1)
目标和观察者间的抽象松耦合
一个目标所知道的仅仅是它有一系列观察者,每个都符合抽象的Observer类的简单接口。目标不知道任何一个观察者属于哪一个具体的类。这样目标和观察者之间的耦合是抽象的和最小的。因为目标和观察者不是紧密耦合的,它们可以属于一个系统中的不同抽象层次。一个处于较低层次的目标对象可与一个处于较高层次的观察者通信并通知它,这样就保持了系统层次的完整。如果目标和观察者混在一块,那么得到的对象要么横贯两个层次(违反了层次性),要么必须放在这两层的某一层中(这可能会损害层次抽象)。- 2)
支持广播通信
不像通常的请求,目标发送的通知不需指定它的接收者。通知被自动广播给所有已向该目标对象登记的有关对象。目标对象并不关心到底有多少对象对自己感兴趣;它唯一的责任就是通知它的各观察者。这给了你在任何时刻增加和删除观察者的自由。处理还是忽略一个通知取决于观察者。- 3)
意外的更新
因为一个观察者并不知道其它观察者的存在,它可能对改变目标的最终代价一无所知
。在目标上一个看似无害的的操作可能会引起一系列对观察者以及依赖于这些观察者的那些对象的更新。此外,如果依赖准则的定义或维护不当,常常会引起错误的更新,这种错误通常很难捕捉。简单的更新协议不提供具体细节说明目标中什么被改变了,这就使得上述问题更加严重。如果没有其他协议帮助观察者发现什么发生了改变,它们可能会被迫尽力减少改变。*
命令(command)模式-对象行为型:
命令模式将请求
封装成对象
,以便使用不同的请求
、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。
封装调用:我们将把封装带到一个全新的境界:把方法调用(method invocation)封装起来
。意图
:将一个请求
封装成一个对
象,从而使您可以用不同的请求
对客户进行行为参数化。函数式编程,行为参数化,lambda表达式主要解决
:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。何时使用
:在某些场合,比如要对行为进行”记录、撤销/重做、事务”等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将”行为请求者”与”行为实现者”解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。如何解决
:通过调用者调用接受者执行命令,顺序:调用者→命令→接受者。关键代码
:定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口应用实例
:struts 1 中的 action 核心控制器 ActionServlet 只有一个,相当于 Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的 Command。优点
: 1、降低了系统耦合度。 2、新的命令可以很容易添加到系统中去。缺点
:使用命令模式可能会导致某些系统有过多的具体命令类。使用场景
:认为是命令的地方都可以使用命令模式,比如: 1、GUI 中每一个按钮都是一条命令。 2、模拟 CMD。注意事项
:系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式,见命令模式的扩展。
情景:做一个家电自动化遥控器的API,不同按钮控制不同的家电行为。有一个整体的撤销功能,家电类特别多。正常的写法,我们的API里会嵌套很多的条件语句,使用命令模式可以解决我们的问题。
命令模式
可将动作的请求者
从动作的执行者
对象中`解耦。利用命令对象,把请求(例如开电灯)封装成一个特定对象(例如客厅电灯对象) 。所以,如果对每个按钮都在储一个命令对象,那么当按钮被按下的时候,就可以请命令对象做相关的工作.遥控器并不需要知道工作内容是什么,只要有个命令对象能和正确的对象沟通,把事情做好就可以了。所以,遥控器和电灯对象解耦了。
基本命令模式
命令对象
通过在特定接收者
上绑定一组动作
来封装一个请求。要达到这一点,·命令对象
将动作
和接收者
包进对象中。这个对象只暴露出一个execute()
方法,当此方法被调用的时候,接收者就会进行这些动作。
从外面来看,其他对象不知道究竟哪个接收者进行了哪些动作,只知道如果调用execute()方法,请求的目的就能达到。我们也看到了利用命令来参数化对象的一些例子
。在简单遥控器中,我们先用一个“打开电灯”命令加载按钮插槽,稍后又将命令替换成为另一个“打开车库门”命令。遥控器插槽根本不在乎所拥有的是什么命令对象
,只要该命令对象实现了Command
接口就可以了。我们还未说到使用命令模式来实现“队列、日志和支持撤销操作”。别担心,这是基本命令模式相当直接的扩展,很快我们就会看到这些内容。一旦有了足够的基础,也可以轻易地持所谓的Meta Command Pattern, Meta Command Pattern可以创建命令的宏,以便一次执行多个命令。
命令抽象及其实现类
1 | /** |
接受者类
1 | public class Light { |
遥控器类
1 | /** |
测试类
1 | /** |
java8的写法
命令通过行为参数化的方式,使用方法引用,或者lambda表达式的方式,只要符合Command 的函数签名即可 ()-void
1 |
|
JDK 中的对于基本命令模式的应用
JDK 中的对于基本命令模式在Thread中有类似的使用,但是还是有些不同,个人理解,感觉不合理小伙伴们可以留言 ,使用Runnable接口构造的线程,会通过Runnable接口的run方法编写线程行为,通过Thread的start方法调用run方法(start方法调用start0(),然后start0()方法调用用run方法)。Runnable 接口可以看做是抽象命令类Command,run方法即为execcute()方法,Thread 类即为Invoker,使用start()方法调用run方法,具体的使用Runnable接口构造的线程体类即实现了动作(逻辑)和接受者(业务对象)的绑定,我们来看一个实例
1 | package com.liruilong.design_pattern.command; |
1 | package com.liruilong.design_pattern.command; |
1 | package com.liruilong.design_pattern.command; |
撤销功能的处理
当命令支持撤销时,该命令就必须提供和
execute()
方法相反的undo()
方法。不管execute()
刚才做什么,undo()
都会倒转过来。这么一来,在各个命令中加入undo()
之前,我们必须先在Command接口中加入undo()方法:
抽象命令接口
1 | public interface Command { |
遥控器类
1 | package headfirst.designpatterns.command.undo; |
使用状态实现撤销
通常,想要实现撤销·的功能,需要记录一些状态。让我们试一个更有趣的例子,比方说厂商类中的天花板上的吊扇。吊扇允许有多种转动速度,当然也允许被关闭。
1 | package headfirst.designpatterns.command.undo; |
加入撤销到吊扇的命令类
1 | package headfirst.designpatterns.command.undo; |
使用宏命令,批量操作
1 | package headfirst.designpatterns.command.party; |
1 | `问`:接收者一定有必要存吗?为何命令对象不直接实现execute()方法的细节? |
命令模式的更多用途:
队列请求
命令可以将运算块打包(一个接收者和一组动作)然后将它传来传去,就像是一般的对象一样。现在,即使在命令对象
被创建许久之后,运算依然可以被调用
。事实上,它甚至可以在不同的线程中被调用
。我们可以利用这样的特性衍生一些应用,例如:日程安排(Scheduler)
、线程池
、工作队列
等。想象有一个工作队列
:你在某一端添加命令,然后另端则是线程。线程进行下面的动作:从队列中取出一个命令,调用它的execute)方法,等待这个调用完成,然后将此命令对象丢弃,再取出下一个命令…**
日志请求
某些应用需要我们将所有的动作都记录在日志中,并能在系统死机之后,重新调用些动作恢复到之前的状态。通过新增两个方法(store(), load()) ,命令模式
能够支持这一点。在Java中,我们可以利用对象的序列化(Serialization)实现方法,但是一般认为序列化最好还是只用在对象的持久化上(persistence) 。
怎么做呢?当我们执行命令的时候,将历史记录储存在磁盘中。一旦系统死机,可以将命令对象重新加载
,并成批地依次调用这些对象的execute0方法。
日志的方式
对于遥控器来说没有意义,然而,有许多调用大型数据结构的动作的应用无法在每次改变发生时被快速地存储。通过使用记录日志
,我们可以上次检查点(checkpoint)之后
的所有操作记录下来,如果系统出状况,从检查点开始应用这些操作。比方说,对于电子表格应用,我们可能想要实现的错恢复方式是将电子表格的操作记录在日志中,而不是每次电子表格一有变化就记录整个电子表格。对更高级的应用而言,这些技巧可以被扩展应用到事务tansaction)理中
,也就是说,一整群操作必须全部进行完成,或者没有进行任何的操作。
命令模式
将发出请求的对象和执行请求的对象解耦,在被解耦的两者直接是通过命令对象进行沟通的,命令对象封装了接受者和一个或者一组动作,调用者通过调用命令对象的execute()发出请求,这会使得接收者的动作被调用,调用者可以接受命令当做参数,甚至在运行时动态的进行,命令可以撤销
,实现的是一个叫undo()方法来回到execute()被执行前的状态,宏命令是命令的
回顾一下定义:
命令模式将请求
封装成对象
,以便使用不同的请求
、队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。,这可以让你使用不同的请求、队列,或者日志请求来と参数化其他对象。命令模式也可以支持微销操作。
GOF描述
Command模式有以下效果:
- Command模式将
调用操作的对象
与知道如何实现该操作的对象
解耦。Command是头等的对象
。它们可像其他的对象一样被操纵和扩展。你可将多个命令装配成一个复合命
令。例如是前面描述的MacroCommand类。一般说来,复合命令是Composite模式的一个实例。增加新的Command很容易
,因为这无需改变已有的类。
访问者(Visitor)模式-对象行为型
访问者模式
(Visitor Pattern)中,使用一个访问者类
,改变元素类
的执行算法
。通过这种方式,元素的执行算法可以随着访问者
改变而改变。这种类型的设计模式属于行为型模式
。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。
意图
:主要将 数据结构与 数据操作分离
。主要解决
:稳定的数据结构
和易变的操作
耦合问题。何时使用
:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作”污染”这些对象的类,使用访问者模式将这些封装到类中。如何解决
:在被访问的类里面加一个对外提供接待访问者的接口。关键代码
:在数据基础类里面有一个方法接受访问者,将·自身引用传入访问者
。
应用实例
:您在朋友家做客,您是访问者,朋友接受您的访问,您通过朋友的描述,然后对朋友的描述做出一个判断,这就是访问者模式。优点
: 1、符合单一职责原则。 2、优秀的扩展性。 3、灵活性。缺点
: 1、具体元素对访问者公布细节,违反了迪米特原则。 2、具体元素变更比较困难。 3、违反了依赖倒置原则,依赖了具体类,没有依赖抽象。使用场景
: 1、对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。 2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作”污染”这些对象的类,也不希望在增加新操作时修改这些类。注意事项
:访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。
实现
我们将创建一个定义接受操作的ComputerPart
接口。Keyboard
、Mouse
、Monitor
和Computer
是实现了ComputerPart
接口的实体类。我们将定义另一个接口ComputerPartVisitor
,它定义了访问者类的操作。Computer 使用实体访问者来执行相应的动作。
VisitorPatternDemo,我们的演示类使用 Computer、ComputerPartVisitor 类来演示访问者模式的用法。
定义一个表示元素的接口。ComputerPart.java
1 | public interface ComputerPart { |
创建扩展了上述类的实体类。Keyboard.java
1 | public class Keyboard implements ComputerPart { |
Monitor.java
1 | public class Monitor implements ComputerPart { |
Mouse.java
1 | public class Mouse implements ComputerPart { |
Computer.java
1 | public class Computer implements ComputerPart { |
定义一个表示访问者的接口。ComputerPartVisitor.java
1 | public interface ComputerPartVisitor { |
创建实现了上述类的实体访问者。ComputerPartDisplayVisitor.java
1 | public class ComputerPartDisplayVisitor implements ComputerPartVisitor { |
使用 ComputerPartDisplayVisitor 来显示 Computer 的组成部分。VisitorPatternDemo.java
1 | public class VisitorPatternDemo { |
执行程序,输出结果:
1 | Displaying Mouse. |
GOF描述
1)
访问者模式使得易于增加新的操作
访问者使得增加依赖于复杂对象结构的构件的操作变得容易了。仅需增加一个新的访问者即可在一个对象结构上定义一个新的操作
。相反,如果每个功能都分散在多个类之上的话,定义新的操作时必须修改每一类
。
2)访问者集中相关的操作而分离无关的操作
相关的行为不是分布在定义该对象结构的各个类上,而是集中在一个访问者中。无关行为却被分别放在它们各自的访问者子类中。这就既简化了这些元素的类,也简化了在这些访问者中定义的算法。所有与它的算法相关的数据结构都可以被隐藏在访问者中。
3)增加新的ConcreteElement类很困难
Visitor模式使得难以增加新的Element的子类。每添加一个新的ConcreteElement都要在Vistor中添加一个新的抽象操作,并在每一个ConcretVisitor类中实现相应的操作。有时可以在Visitor中提供一个缺省的实现,这一实现可以被大多数的ConcreteVisitor继承,但这与其说是一个规律还不如说是一种例外。所以在应用访问者模式时考虑关键的问题是系统的哪个部分会经常变化
,是作用于对象结构上的算法
呢还是构成该结构的各个对象的类
。如果老是有新的ConcretElement类
加入进来的话, Vistor类层次将变得难以维护。在这种情况下,直接在构成该结构的类中定义这些操作可能更容易一
些。如果Element类层次是稳定的,而你不断地增加操作获修改算法,访问者模式可以帮助你管理这些改动。
4)通过类层次进行访问一个迭代器
(参见Iterator)可以通过调用节点对象的特定操作来遍历整个对象结构,同时访问这些对象。但是迭代器不能对具有不同元素类型的对象结构进行操作。例如,定义在第5章的Iterator接口只能访问类型为Item的对象:
模式之间的区别
桥接模式与策略模式的区别
设计模式的划分更多的是面对问题域而不是面对实现方式,实际上所有模式可以只分为类模式
和对象模式
两种,类模式是用继承
而对象模式是用委托
Bridge模式和Strategy模式相似就是因为他们都将任务委托给了另外一个接口的具体实现
。
他们之间的区别在于Bridge的目的
是让底层实现和上层接口可以分别演化
,从而提高移植性而Strategy的目的
是将复杂的算法封装起来,从而便于替换不同的算法
。因此可以想象一般情况下Bridge的实现几乎不会在运行时更改而Strategy的算法则很有可能需要在运行时更换,这就导致在细节方面需要考虑的因素可能会很不相同。
strategy模式是为了扩展和修改,并提供动态配置
。它往往可以在同一环境当中使用不同的策略
,就是调用不同的派生类。其内部实现是自由的,不受已有的类接口的限制(很多时候根本就不调用现成的接口)。
bridge模式是往往是为了利用已有的方法或类。它将原来不统一,不兼容的接口封装起来,变成统一的接口
。它的应用往往是不同的环境或平台下只能选择一 种,比如说在windows平台下只能用WinClass,而在unix平台下只能用UnixClass.它的主要作用不是配置而是定义通用接口。
据个例子来说:我要画园,要实心园,我可以用SolidPen来配置,画虚线园可以用dashedPen来配置。这是strategy模式。而同样是画园,我是在windows下来画实心园,就用windowPen+solidPen来配置,在unix下画实心园就用 unixPen+solidPen来配置。如果要再windows下画虚线园,就用windowsPen+dashedPen来配置,要在unix下画虚 线园,就用unixPen+dashedPen来配置。
我这里仅仅是就一种情况来说strategy和bridge的组合应用,其他的组合可能性随环境变化而多种多样。从中可以看出,bridge和strategy是可能组合使用,侧重不同方面的。模式某种角度上来讲就是对象组合。不要看他们都是对象组合就好像是一样的。模式的动机,意图,使用场合,组合方式,这些都是模式的一部分。其中细微的不同足以区分不同的模式。
在桥接模式中,Abstraction通过聚合的方式引用Implementor。
在策略模式中,Context也使用聚合的方式引用Startegy抽象接口。
从他们的结构图可知,在这两种模式中,**都存在一个对象使用聚合的方式引用另一个对象的抽象接口的情况,而且该抽象接口的实现可以有多种并且可以替换。可以说两者在表象上都是调用者与被调用者之间的解耦,以及抽象接口与实现的分离。**
那么两者的区别体现在什么地方呢?
首先,在形式上,两者还是有一定区别的,对比两幅结构图,我们可以发现,在桥接模式中不仅Implementor具有变化 (ConcreateImplementior),而且Abstraction也可以发生变化(RefinedAbstraction),而且两者的变化 是完全独立的,RefinedAbstraction与ConcreateImplementior之间松散耦合,它们仅仅通过Abstraction与 Implementor之间的关系联系起来。而在策略模式中,并不考虑Context的变化,只有算法的可替代性。
其次在语意上,桥接模式强调Implementor接口仅提供基本操作,而Abstraction则基于这些基本操作定义更高层次的操作。而策略模式强调 Strategy抽象接口的提供的是一种算法,一般是无状态、无数据的,而Context则简单调用这些算法完成其操作。
桥接模式中不仅定义Implementor的接口而且定义Abstraction的接口,Abstraction的接口不仅仅是为了与 Implementor通信而存在的,这也反映了结构型模式的特点:通过继承、聚合的方式组合类和对象以形成更大的结构。在策略模式中,Startegy 和Context的接口都是两者之间的协作接口,并不涉及到其它的功能接口,所以它是行为模式的一种。行为模式的主要特点就是处理的是对象之间的通信方 式,往往是通过引入中介者对象将通信双方解耦,在这里实际上就是将Context与实际的算法提供者解耦。
所以相对策略模式,桥接模式要表达的内容要更多,结构也更加复杂。桥接模式表达的主要意义其实是接口隔离的原则,即把本质上并不内聚的两种体系区别
开来,使得它们可以松散的组合
,而策略在解耦上还仅仅是某一个算法的层次,没有到体系这一层次。从结构图中可以看到,策略的结构是包容在桥接结构中的,桥 接中必然存在着策略模式,Abstraction与Implementor之间就可以认为是策略模式,但是桥接模式一般Implementor将提供一系 列的成体系的操作,而且Implementor是具有状态和数据的静态结构。而且桥接模式Abstraction也可以独立变化。
Adapter(适配器)与Bridgede(桥接模式) 的区别
Adapter
模式和Bridge
模式具有一些共同的特征:
它们都给另一对象提供了一定程度上的间接性,因而有利于系统的灵活性。它们都涉及到从自身以外的一个接口向这个对象转发请求。
不同之处
主要在于它们各自的用
途:
- Adapter模式主要是为了解决
两个已有接口之间不匹配
的问题。它不考虑这些接口是怎样实现的,也不考虑它们各自可能会如何演化。这种方式不需要对两个独立设计的类中的任一个进行重新设计,就能够使它们协同工作。** - Bridge模式则
对抽象接口与它的(可能是多个)实现部分进行桥接
。虽然这一模式允许你修改实现它的类,它仍然为用户提供了一个稳定的接口。Bridge模式也会在系统演化时
适应新的实现。
Adapter和Bridge模式通常被用于软件生命周期的不同阶段
。
当你发现两个不兼容的类必须同时工作时,就有必要使用Adapter模式
,其目的一般是为了避免代码重复。此处耦合不可预见。相反, Bridge的使用者必须事先知道
:一个抽象将有多个实现部分,并且抽象和实现两者是独立演化的。Adapter模式在类已经设计好后实施;而Bridge模式在设计类之前实施
。这并不意味着Adapter模式不如Bridge模式,只是因为它们针对了不同的问题。你可能认为facade(参见Facade(4.5))是另外一组对象的适配器。但这种解释忽视了一个事实:即Facade定义一个新的接口,而Adapter则复用一个原有的接口。记住,适配器使两个已有的接口协同工作,而不是定义一个全新的接口
。
Adapter(适配器)和Facade(外观)的区别:
问
:我可不可以这样说,适配器模式
和外观模式
之间的差异在于:适配器包装一个类,而外观可以代表许多类?答
:不对!提醒你、适配器模式将一个或多个类接口变成客户所期望的,一个接口。虽然大多数教科书所采用的例子中适配器只适配一个类,但是你可以适配许多类来提供一个接口让客户编码。类似地,一个外观也可以只针对一个拥有复杂接口的类提供简化的接口。两种模式的差异,不在于它们“包装”了几个类,而是在于它们的意图
,适配器模式的意图是, “改变”接口符合客户的期望;而外观模式的意图是,提供子系统的一个简化接口。适配器
将一个对象包装起来改变其接口,装饰者
将一个对象包装起来以增加其他行为和责任,而外观模式
将一群对象“包装”起来以简化其接口。
Composite, Decorator与Proxy的区别
Composite(4.3)模式和Decorator(4.4)模式具有类似的结构图,这说明它们都基于递归组合来组织可变数目的对象。这一共同点可能会使你认为, decorator对象是一个退化的composite,但这一观点没有领会Decorator模式要点。相似点仅止于递归组合,同样,这是因为这两个模式的目的不同。
Decorator旨在使你能够不需要生成子类即可给对象添加职责。这就避免了静态实现所有功能组合,从而导致子类急剧增加。Composite则有不同的目的,它旨在构造类,使多个相关的对象能够以统一的方式处理,而多重对象可以被当作一个对象来处理。它重点不在于修饰,而在于表示。
尽管它们的目的截然不同,但却具有互补性。因此Composite和Decorator模式通常协同使用。在使用这两种模式进行设计时,我们无需定义新的类,仅需将一些对象插接在一起即可构建应用。这时系统中将会有一个抽象类,它有一些composite子类和decorator子类,还有一些实现系统的基本构建模块。此时, composites和decorator将拥有共同的接口。从Decorator模式的角度看, composite是一个ConcreteComponent。而从composite模式的角度看,decorator则是一个Leaf。当然,他们不一定要同时使用,正如我们所见,它们的目的有很大的差别。
另一种与Decorator模式结构相似的模式是Proxy(4.7),这两种模式都描述了怎样为对象提供一定程度上的间接引用, proxy和decorator对象的实现部分都保留了指向另一个对象的指针,它们向这个对象发送请求。然而同样,它们具有不同的设计目的。
像Decorator模式一样, Proxy模式构成一个对象并为用户提供一致的接口。但与Decorator模式不同的是, Proxy模式不能动态地添加或分离性质,它也不是为递归组合而设计的。它的目的是,当直接访问一个实体不方便或不符合需要时,为这个实体提供一个替代者,例如,实体在远程设备上,访问受到限制或者实体是持久存储的。
在Proxy模式中,实体定义了关键功能,而Proxy提供(或拒绝)对它的访问。在Decorator模式中,组件仅提供了部分功能,而一个或多个Decorator负责完成其他功能。Decorator模式适用于编译时不能(至少不方便)确定对象的全部功能的情况。这种开放性使递归组合成为Decorator模式中一个必不可少的部分。而在Proxy模式中则不是这样,因为Proxy模式强调一种关系(Proxy与它的实体之间的关系),这种关系可以静态的表达。
模式间的这些差异非常重要,因为它们针对了面向对象设计过程中一些特定的经常发生问题的解决方法。但这并不意味着这些模式不能结合使用。可以设想有一个proxy-decorator,它可以给proxy添加功能,或是一个decorator-proxy用来修饰一个远程对象。尽管这种混合可能有用(我们手边还没有现成的例子),但它们可以分割成一些有用的模式。
四,23种设计模式之外的设计模式
空对象(Null Object)模式
在空对象模式
(Null Object Pattern)中,一个空对象取代 NULL 对象实例的检查。Null 对象不是检查空值,而是反应一个不做任何动作的关系。这样的 Null 对象也可以在数据不可用的时候提供默认的行为。
在
空对象模式
中,我们创建一个指定各种要执行的操作的抽象类
和扩展该类的实体类
,还创建一个未对该类做任何实现的空对象类
,该空对象类将无缝地使用在需要检查空值的地方。实现
我们将创建一个定义操作(在这里,是客户的名称)的 AbstractCustomer 抽象类,和扩展了 AbstractCustomer 类的实体类。工厂类 CustomerFactory 基于客户传递的名字来返回 RealCustomer 或 NullCustomer 对象。
NullPatternDemo,我们的演示类使用 CustomerFactory 来演示空对象模式的用法。
创建一个抽象类。AbstractCustomer.java
1 | public abstract class AbstractCustomer { |
创建扩展了上述类的实体类。RealCustomer.java
1 | public class RealCustomer extends AbstractCustomer { |
NullCustomer.java
1 | public class NullCustomer extends AbstractCustomer { |
`创建 CustomerFactory 类。CustomerFactory.java```
1 | public class CustomerFactory { |
使用 CustomerFactory,基于客户传递的名字,来获取 RealCustomer 或 NullCustomer 对象。NullPatternDemo.java
1 | public class NullPatternDemo { |
执行程序,输出结果:
1 | Customers |
JDK1.8 中的Null Object 模式的使用 Optional
Optional 对象可能包含或不包含非空值的容器对象。 如果一个值存在, isPresent()将返回true和get()将返回值。
提供依赖于存在或不存在包含值的其他方法,例如orElse()/orElseGet() (如果值不存在则返回默认值)和ifPresent() (如果值存在则执行代码块)。
1 | /* |
过滤器(Filter)模式或标准(Criteria)模式
允许开发人员使用不同的标准
来过滤一组对象
,通过逻辑运算以解耦的方式把它们连接起来
。这种类型的设计模式属于结构型模式
,它`结合多个标准来获得单一标准。
这个很容易理解,不多解释。
实现
:我们将创建一个 Person 对象、Criteria 接口和实现了该接口的实体类,来过滤 Person 对象的列表
。CriteriaPatternDemo 类使用 Criteria 对象,基于各种标准和它们的结合来过滤 Person 对象的列表。Person.java
1 | public class Person { |
Criteria.java
1 | import java.util.List; |
CriteriaMale.java
1 | import java.util.ArrayList; |
CriteriaFemale.java
1 | import java.util.ArrayList; |
CriteriaSingle.java
1 | import java.util.ArrayList; |
AndCriteria.java
1 | import java.util.List; |
OrCriteria.java
1 | import java.util.List; |
1 | import java.util.ArrayList; |
嗯,更新中…..生活加油
模式 | – |
---|---|
装饰者 | 将一个接口转成另一个接 |
适配器 | 不改变接口,但加入责任 |
外观 | 让接口更简单 |
模板方法 | 封装可互换的行为,然后使用委托来决定要采用哪一个行为 |
策略 | 子类决定如何实现算法中的 |
工厂方法 | 由子类决定实例化哪个 |
《Head First设计模式》中文版读书笔记
https://liruilongs.github.io/2021/03/01/Java/《Head-First设计模式》中文版读书笔记/