设计模式
设计模式
小奥目录
第一章 设计模式概述
软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。
设计模式分类
(1)创建型模式
用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。分别是工厂方法、抽象工厂、建造者、原型和单例5种创建型模式。
(2)结构型模式
用于描述如何将类或者对象按某种布局组成更大的结构。分别是适配器、桥接、组合、装饰器、外观、享元、代理等7种结构型模式。
(3)行为型模式
用于描述类或者对象之间怎样相互写作共同完成单个对象无法单独完成的任务,以及怎样分配职责。分别是解释器、模板方法、职责链、命令、迭代器、中介者、备忘录、观察者、状体、策略、访问者等11种行为型模式。
第二章 UML图
统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。
UML从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等9种图。
2.1 类图概述
类图(class diagram)是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。
2.2 类图的作用
- 在软件工程中,类图是一种静态的结构图,描述了系统的类的集合,类的属性和类之间的关系,可以简化了人们对系统的理解。
- 类图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型。
2.3 类图表示法
(1)类的表示方式
在UML类图中,类使用包含类名、属性(field)和方法(method)且带有分割线的矩形来表示,比如下图中表示一个Employee图,它包含name,age和address这三个属性,以及work()方法。
属性/ 方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:
- +:表示public
- -:表示private
- #:表示protected
属性的完整表示方式是:可见性 名称: 类型 [ = 缺省值]
方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型]
注意:① 中括号里面的内容是可选的 ② UML类图还有其他的表示方式。
(2)类与类之间关系的表示方式
关联关系
关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。我们先介绍一般关联。
关联又可以分为单向关联,双向关联,自关联。
① 单向关联
在UML类图中单向关联用一个带箭头的实线表示。上图表示每个顾客都有一个地址,这通过让Customer类持有一个类型为Address的成员变量类实现。
② 双向关联
从上图中我们很容易看出,所谓的双向关联就是双方各自持有对方类型的成员变量。在UML类图中,双向关联用一个不带箭头的直线表示。上图中在customer类中维护一个List<Product>
,表示一个顾客可以购买多个商品;在Product类中维护一个customer类型的成员变量表示这个产品被哪个顾客所购买。
③ 自关联
自关联在UML类图中用一个带有箭头且指向自身的线表示。上图的意思就是Node类包含类型为Node的 成员变量,也就是“自己包含自己”。
聚合关系
聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。
聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体 对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。下图所示是大学和教师的关系图:
组合关系
组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在, 部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。
在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。下图所示是头和嘴的关系图:
依赖关系
依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。
在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。下图所示是司机 和汽车的关系图,司机驾驶汽车:
继承关系
继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一 种继承关系。
在 UML 类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使 用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子 类,其类图如下图所示:、
实现关系
实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明 的所有的抽象操作。
在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和 船实现了交通工具,其类图如图 9 所示。
第三章 软件设计原则
在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽 量根据6条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。
3.1开闭原则
对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是实现一个热插拔的效果。可以让程序的扩展性更好,易于维护和升级。
要想实现开闭原则,我们需要使用接口抽象类。因为抽象类的灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象类派生出来实现类进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。
下面以 搜狗输入法
的皮肤为例介绍开闭原则的应用。
【例】 搜狗输入法 的皮肤设计。
分析: 搜狗输入法
的皮肤是输入法背景图片、窗口颜色和声音等元素的组合。用户可以根据自己的喜爱更换自己的输入法的皮肤,也可以从网上下载新的皮肤。这些皮肤有共同的特点,可以为其定义一个 抽象类(AbstractSkin)
,而每个具体的皮肤(DefaultSpecificSkin
和 HeimaSpecificSkin
)是其子类。用户窗体可以根据需要选择或者增加新的主题,而不需要修改原代码,所以它是满足开闭原则的。
3.2里氏代换原则
里氏代换原则是面向对象设计的基本原则之一。
里氏代换原则:任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能, 但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较 差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
下面看一个里氏替换原则中经典的一个例子:
【例】正方形不是长方形。
在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何 图形相关的软件系统,就可以顺理成章的让正方形继承自长方形。
代码如下:
长方形类(Rectangle):
public class Rectangle { |
正方形(Square):
由于正方形的长和宽相同,所以在方法setLength和setWidth中,对长度和宽度都需要赋相同值。
public class Square extends Rectangle { |
类RectangleDemo是我们的软件系统中的一个组件,它有一个resize方法依赖基类Rectangle, resize方法是RectandleDemo类中的一个方法,用来实现宽度逐渐增长的效果。
public class RectangleDemo { |
我们运行一下这段代码就会发现,假如我们把一个普通长方形作为参数传入resize方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期;
假如我 们再把一个正方形作为参数传入resize方法后,就会看到正方形的宽度和长度都在不断增长,代码会 一直运行下去,直至系统产生溢出错误。所以,普通的长方形是适合这段代码的,正方形不适合。
我们得出结论:在resize方法中,Rectangle类型的参数是不能被Square类型的参数所代替,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则, 它们之间的继承关系不成立,正方形不是长方形。
如何改进呢?此时我们需要重新设计他们之间的关系。抽象出来一个四边形接口(Quadrilateral), 让Rectangle类和Square类实现Quadrilateral接口。
3.3依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
下面看一个例子来理解依赖倒转原则:
【例】组装电脑
现要组装一台电脑,需要配件cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常的运行。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等。
类图如下:
代码如下:
希捷硬盘类(XiJieHardDisk):
public class XiJieHardDisk{ |
Intel处理器(IntelCpu):
public class IntelCpu { |
金士顿内存条(KingstonMemory):
public class KingstonMemory{ |
电脑(Computer):
public class Computer { |
测试类(TestComputer):
测试类用来组装电脑。
public class TestComputer { |
上面代码可以看到已经组装了一台电脑,但是似乎组装的电脑的cpu只能是Intel的,内存条只能是金士顿的,硬盘只能是希捷的,这对用户肯定是不友好的,用户有了机箱肯定是想按照自己的喜好,选择自己喜欢的配件。
根据依赖倒转原则进行改进:
代码我们只需要修改Computer类,让Computer类依赖抽象(各个配件的接口),而不是依赖于各个 组件具体的实现类。
类图如下:
电脑(Computer):
public class Computer { |
注意,之前的具体的类要实现这些接口,如类图中所示。
面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实 现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低 了客户程序与实现细节的耦合度。
3.4 接口隔离原则
客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。
下面看一个例子来理解接口隔离原则、
【例】安全门案例
我们需要创建一个 黑马
品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防 盗功能提取成一个接口,形成一套规范。类图如下:
上面的设计我们发现了它存在的问题,黑马品牌的安全门具有防盗,防水,防火的功能。现在如果我们 还需要再创建一个传智品牌的安全门,而该安全门只具有防盗、防水功能呢?很显然如果实现 SafetyDoor接口就违背了接口隔离原则,那么我们如何进行修改呢?看如下类图:
代码如下:
AntiTheft(防盗接口):
public interface AntiTheft { |
Fireproof(防火接口):
public interface Fireproof { |
Waterproof(防水接口):
public interface Waterproof { |
HeiMaSafetyDoor(类):
public class HeiMaSafetyDoor implements AntiTheft,Fireproof,Waterproof { |
ItcastSafetyDoor(类):
public class ItcastSafetyDoor implements AntiTheft,Fireproof { |
3.5 迪米特法则
迪米特法则又叫最少知识原则。
只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发 该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象 的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
下面看一个例子来理解迪米特法则:
【例】明星与经纪人的关系实例
明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业 务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。
类图如下:
代码如下:
明星类(Star):
public class Star { |
粉丝类(Fans)
public class Fans { |
媒体公司类(Company)
public class Company { |
经纪人类(Agent)
public class Agent { |
3.6 合成复用法则
合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。 通常类的复用分为继承复用和合成复用两种。
继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
(1)继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所 以这种复用又称为“白箱”复用。
(2) 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
(3)它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
(1)它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱” 复用。
(2)对象间的耦合度低。可以在类的成员位置声明抽象。
(3)复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
下面看一个例子来理解合成复用原则
【例】汽车分类管理程序
汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色 汽车等。如果同时考虑这两种分类,其组合就很多。类图如下:
从上面类图我们可以看到使用继承复用产生了很多子类,如果现在又有新的动力源或者新的颜色的话, 就需要再定义新的类。我们试着将继承复用改为聚合复用看一下。
第四章 创建者模式
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。
这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。
创建者模式分为:
- 单例模式
- 工厂方法模式
- 抽象工程模式
- 原型模式
- 建造者模式
4.1 单例设计模式
单例模式(Singleton Design Pattern):如果一个类只允许创建一个对象或者实例,那么这个类就是一个单例类,这张设计模式就叫做单例模式。
这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供 了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
为什么要使用单例模式
(1)处理资源访问冲突。比如我们自定义的日志打印类,如果不同的方法调用返回不同的实例,那么可能最后我们输出的日志就会产生盖覆盖或者乱序的现象。
(2)表示全局唯一类。比如配置信息,在全局中,只能存在一个。
单例模式的实现
单例设计模式分为两种:
- 饿汉式:类加载就会导致该单实例对象被创建
- 懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象的时候才会创建
(1)创建方式
①饿汉式
静态常量
public class Singleton { |
优点:写法比较简单,在类加载的时候就完成了实例化,避免了线程同步问题。
缺点:在类加载的时候就完成实例化,没有达到懒加载的效果,如果不使用这个实例,就会造成内存的浪费。
静态代码块
public class Singleton { |
该方式的对象的创建是在静态代码块中,也是随着类的加载而创建,所以和静态常量的方式基本一致。
②懒汉式
线程不安全
public class Singleton { |
优点:这种方式实现了懒加载,当调用getinstance()方法获取Singleton类的对象的时候才会创建Singleton类的对象。
缺点:只能在单线程环境下使用,如果是多线程环境便会出现线程不安全问题。
线程安全(同步方法)
public class Singleton { |
优点:实现了懒加载,保证线程安全
缺点:synchronize执行效率比较低
线程安全(同步代码块)
public class Singleton { |
③双重检查
public class Singleton { |
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检 测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问 题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字,volatile 关键字可以保证可见性和有序性。
public class Singleton { |
添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程 的情况下线程安全也不会有性能问题。
④静态内部类
public class Singleton { |
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。
⑤枚举(恶汉式)
借助JDK1.5提供的枚举类来实现单例模式,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
public enum Singleton { |
单例模式的优缺点
优点:保证系统内该类只存在一个对象实例,节省了系统的资源。
缺点:要想实例化一个单例对象,必须使用相应的获取对象的方法,不能使用new
(2)存在的问题
① 序列化破坏单例模式
使上面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。
序列化和反序列化
import java.io.Serializable; |
Test类:
import java.io.FileOutputStream; |
上面程序的运行结果是false,说明序列化和反序列化已经破坏了单例设计模式。
②反射破坏单例模式
package zal.design; |
Test类:
public class Test { |
上面代码的运行结果是false,表明序列化和反序列化已经破坏了单例设计模式。
(3)问题的解决
① 序列化、反序列化方式破坏单例模式的解决
在Singleton类中添加readResolve()
方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。
import java.io.Serializable; |
原理
我们可以查看ObjectInputStream类的源码:
private final Object readObject(Class<?> type) |
② 反射破坏单例模式的解决
饿汉式
public class Singleton { |
当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。
懒汉式
public class Singleton{ |
我们仍然执行刚才的测试方法,发现仍能得到结果,并没有抛出异常。原因是我们通过反射创建实例和**静态调用方法getInstance()**获得实例的位置互换了,所以一开始通过反射创建实例调用构造器,此时构造器中的判断install != null
是无用的,所以这种方式不适用懒汉式单例模式来防止被反射破坏的。
(4)(单例应用)Runtime类
public class Runtime { |
从上面源代码中可以看出Runtime类使用的是恶汉式(静态属性)方式来实现单例模式的。
4.2 工厂模式
(1)概述
Java中万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,就会对该对象耦合严 重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。
如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象, 直接在工厂里更换该对象即可,达到了与对象解耦的目的。
设计模式之工厂模式(3种)详解及代码示例 - kosamino - 博客园 (cnblogs.com)
在工厂模式中,创建对象时不会对客户端暴露创建逻辑,并且是通过一个共同的接口来指向新创建的对象。
它属于创建型模式,但不属于23种设计模式,使用工厂方法代替new操作的一种模式。
工厂模式有三种:简单工厂模式、工厂方法模式和抽象工厂方式。
(2)简单工厂模式
简单工厂模式不是一种设计模式,反而比较像是一种编程习惯。
由一个工厂对象决定创建出哪一种产品类的实例,简单工厂的实质是由一个工厂类根据传入的类型,动态决定应该创建哪一个产品类。
适用场景:工厂类负责创建的对象比较少;客户端(应用层)只需要知道传入工厂类的参数,对于如何创建对象并不关心。
①结构
- 抽象产品:定义了产品的规范,描述了产品的主要特征和功能
- 具体产品:实现或者继承抽象产品的子类
- 具体工厂:提供了创建产品的方法,调用者通过该方法获取产品
② 实现
适用场景:调用者只需要知道传入产品的参数,对于创建对象的逻辑并不关心。
【例】咖啡店点餐系统
需求:设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCoffee】和拿铁咖啡 【LatteCoffee】);再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。
使用简单工厂模式对该需求进行改进,类图如下:
工厂类代码如下:
public class SimpleCoffeeFactory { |
工厂(factory)处理创建对象的细节,一旦有了SimpleCoffeeFactory,CoffeeStore类中的 orderCoffee()就变成此对象的客户,后期如果需要Coffee对象直接从工厂中获取即可。这样也就解除了和Coffee实现类的耦合,同时又产生了新的耦合,CoffeeStore对象和 SimpleCoffeeFactory工厂对象的耦合,工厂对象和商品对象的耦合。
后期如果再加新品种的咖啡,我们势必要需求修改SimpleCoffeeFactory的代码,违反了开闭原 则。工厂类的客户端可能有很多,比如创建美团外卖等,这样只需要修改工厂类的代码,省去其他的修 改操作。
③ 扩展
在开发中我们还可以将工厂类中的创建对象的功能定义为静态的,这个就是静态工厂模式,它也不是23种设计模式中的。
public class SimpleCoffeeFactory { |
④优缺点
优点:封装了创建对象的过程,通过参数直接获取对象,实现了对象创建和业务逻辑的分离;如果要实现新产品直接修改工厂类,扩展性更强。
缺点:工厂类不够灵活,增加新产品还要修改工厂类代码,违背了“开闭原则”。
(3)工厂方法模式
工厂方法模式完美的解决了简单工厂模式的缺点,完全遵守开闭原则。
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。
适用场景:
- 客户只知道创建产品的工厂名,而不知道具体的产品名。
- 创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口
- 客户不关心创建产品的细节,只关心产品的品牌。
①结构
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
②实现
使用工厂方法对简单工厂进行改进,类图如下:
代码如下:
【抽象工厂】
// CoffeeFactory |
【具体工厂】
//AmericanCoffeeFactory |
【咖啡店类】
//CoffeeStore |
从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了, 这样就解决了简单工厂模式的缺点。
工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的 优点,而且克服了它的缺点。
③优缺点
优点:用户只需要关系所需产品对应的工厂,无须关心创建细节;加入新产品符合开闭原则,提高扩展性。
缺点:增加产品类的时候,也要增加相应的工厂类,增加了系统的复杂性。
(3)抽象工厂模式
前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机、传 智播客只培养计算机软件专业的学生等。
这些工厂只生产同种类产品,同种类产品称为同等级产品,也就是说:工厂方法模式只考虑生产同等级 的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如电器厂既生 产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。
抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组 产品称为一个产品族,下图所示横轴是产品等级,也就是同一类产品;纵轴是产品族,也就是同一品牌的产品,同一品牌的产品产自同一个工厂。
①概念
是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就 能得到同族的不同等级的产品的模式结构。
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生 产多个等级的产品。
使用条件:
- 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品
- 系统一次只可能消费其中某一族产品,即同族的产品一起适用。
适用场景:
- 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
- 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
- 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
②结构
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。
③实现
【例】咖啡店业务改变
现咖啡店业务发生改变,不仅要生产咖啡还要生产甜点,如提拉米苏、抹茶慕斯等,要是按照工厂方法模式,需要定义提拉米苏类、抹茶慕斯类、提拉米苏工厂、抹茶慕斯工厂、甜点工厂类,很容易发生类爆炸情况。其中拿铁咖啡、美式咖啡是一个产品等级,都是咖啡;提拉米苏、抹茶慕斯也是一个产品等级;拿铁咖啡和提拉米苏是同一产品族(也就是都属于意大利风味),美式咖啡和抹茶慕斯是同一产品族(也就是都属于美式风味)。所以这个案例可以使用抽象工厂模式实现。类图如下:
【抽象工厂】
public interface DessertFactory { |
【具体工厂】
// 美式甜点工厂 |
如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。
③优缺点
优点:当一个产品族中的多个对象被设计成一起工作时,他能保证客户端始终只使用同一个产品族中的对象。
缺点:当产品族需要增加一个新的产品时,所有的工厂类都需要被修改。
(5)模式扩展
① 简单工厂+配置文件解除耦合
可以通过工厂模式+配置文件的方式解除工厂对象和产品对象的耦合。在工厂类中加载配置文件中的全 类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。
第一步:定义配置文件。
为了演示方便,我们使用properties配置文件,名称为bean.properties
american=com.itheima.pattern.factory.config_factory.AmericanCoffee |
第二步,改进工厂类。
public class CoffeeFactory { |
静态成员变量用来存储创建的对象(键存储的是名称,值存储的是对应的对象),而读取配置文件以及 创建对象写在静态代码块中,目的就是只需要执行一次。
(6)(工厂应用)Collection.iterator方法
public class Demo { |
使用迭代器遍历集合,获取集合中的元素。而单列集合获取迭代器的方法 就使用到了工厂方法模式。我们看通过类图看看结构:
Collection接口是抽象工厂类,ArrayList是具体的工厂类;Iterator接口是抽象商品类,ArrayList的Iter内部类是具体的商品类 ,在具体的工厂类中iterator()方法创建具体的商品类的对象。
另外:
(1) DateFormat类中的getInstance()方法使用的是工厂模式
(2)Calendar类中的getInstance()方法使用的是工厂模式
4.3 原型模式
(1)概述
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象。
(2)结构
原型模式包含如下角色:
- 抽象原型类:规定了具体原型对象必须实现的的 clone() 方法。
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
接口类图如下:
(3)实现
原型模式的克隆分为浅克隆和深克隆。
- 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原 有属性所指向的对象的内存地址。
- 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
Java中的Object类中提供了 clone() 方法来实现浅克隆。 Cloneable 接口是上面的类图中的抽 象原型类,而实现了Cloneable接口的子实现类就是具体的原型类。代码如下:
【RealizeType具体原型类】
public class RealizeType implements Cloneable{ |
【PrototypeTest测试访问类】
public class PrototypeTest { |
具体的原型对象创建完毕! |
(4)案例
【例】用原型模式生成“三好学生”奖状
同一学校的“三好学生”奖状除了获奖人姓名不同,其他都相同,可以使用原型模式复制多个“三好学 生”奖状出来,然后在修改奖状上的名字即可。
类图如下:
代码如下:
【奖状类 Citation】
public class Citation implements Cloneable { |
【测试类CitationTest】
public class CitationTest { |
张三同学:在2023学年第一学期中表现优秀,被评为三好学生。特发此状! |
(5)使用场景
- 对象的创建非常复杂,可以使用原型模式快速创建对象
- 性能和安全要求比较高
(6)扩展(深克隆)
将上面“三好学生”奖状案例中的Citation类的name属性改为Student类的属性,代码如下:
【奖状类 Citation】
public class Citation implements Cloneable { |
【测试类CitationTest】
public class CitationTest { |
stu和stu1是同一个对象?true |
stu对象和stu1对象是同一个对象,就会产生将stu1对象中name属性值改为“李四”,两个 Citation(奖状)对象中显示的都是李四。这就是浅克隆的效果,对具体原型类(Citation)中的 引用类型的属性进行引用的复制。这种情况需要使用深克隆,而进行深克隆需要使用对象流。
代码如下:
【测试类CitationTest】
public class CitationTest { |
stu和stu1是同一个对象?false |
注意:Citation类和Student类必须实现Serializable接口,否则会抛 NotSerializableException异常。
4.4 建造者模式
(1)概述
将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。
- 分离了部件的构造(由Builder来负责)和装配(由Director负责)。从而可以构造出复杂的对象。这个模式适用于:某个对象的构建过程复杂的情况。
- 由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建 器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法、装配算法的解耦,实现了更 好的复用。
- 建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对 象的类型就可以得到该对象,而无须知道其内部的具体构造细节。
(2)结构
建造者(Builder)模式包含如下角色:
- 抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的 部件对象的创建。
- 具体建造者类(ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
- 产品类(Product):要创建的复杂对象。
- 指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
类图如下:
(3)实例
【例】创建共享单车
生产自行车是一个复杂的过程,它包含了车架,车座等组件的生产。而车架又有碳纤维,铝合金等材质 的,车座有橡胶,真皮等材质。对于自行车的生产就可以使用建造者模式。
这里Bike是产品,包含车架,车座等组件;Builder是抽象建造者,MobikeBuilder和 OfoBuilder是具体的建造者;Director是指挥者。类图如下
具体代码如下:
【Bike自行车类】
public class Bike { |
【Builder抽象建造者类】
public abstract class Builder{ |
【xxxBuilder具体建造者类】
public class MobikeBuilder extends Builder { |
【Director指挥者类】
public class Director { |
【Client测试类】
public class Client { |
上面示例是 Builder模式的常规用法,指挥者类 Director 在建造者模式中具有很重要的作用,它 用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况 下需要简化系统结构,可以把指挥者类和抽象建造者进行结合。
// 抽象 builder 类 |
这样做确实简化了系统结构,但同时也加重了抽象建造者类的职责,也不是太符合单一职责原则,如果 construct() 过于复杂,建议还是封装到 Director 中。
(4)优缺点
优点:
- 建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取 得比较好的稳定性。
- 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得 相同的创建过程可以创建不同的产品对象。
- 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过 程更加清晰,也更方便使用程序来控制创建过程。
- 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不 用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。
缺点:
造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适 合使用建造者模式,因此其使用范围受到一定的限制。
(5)使用场景
建造者(Builder)模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合在一起的算法却相对稳定,所以它通常在以下场合使用。
- 创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的。
- 创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式,即产品的构建过程和最终的表 示是独立的。
(6) 模式扩展
建造者模式除了上面的用途外,在开发中还有一个常用的使用方式,就是当一个类构造器需要传入很多参数时,如果创建这个类的实例,代码可读性会非常差,而且很容易引入错误,此时就可以利用建造者模式进行重构。
重构前代码如下:
public class Phone { |
上面在客户端代码中构建Phone对象,传递了四个参数,如果参数更多呢?代码的可读性及使用的成本 就是比较高。
重构后代码:
public class Phone { |
重构后的代码在使用起来更方便,某种程度上也可以提高开发效率。从软件设计上,对程序员的要求比较高。
(7)建造者模式对比
① 工厂方法模式VS建造者模式
工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,意在通过一步一 步地精确构造创建出一个复杂的对象。
举一个简单的例子来说,如果要制造一辆汽车,如果使用工厂方法模式,则直接生产出来的是一辆汽车;如果使用建造者模式,则需要组装发动机、轮胎、车框等部分,最后再显示汽车。
②抽象工厂模式VS建造者模式
抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。
建造者模式则是要求按照指定的步骤建造产品,它的主要目的是通过组装零配件而产生一个新产品。
如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组 装工厂,通过对部件的组装可以返回一辆完整的汽车。
第五章 结构型模式
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式, 前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。
由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型 模式具有更大的灵活性。
结构型模式分为以下七种:
- 代理模式
- 适配器模式
- 装饰者模式
- 桥接模式
- 外观模式
- 组合模式
- 享元模式
5.1 代理模式
(1)概述
由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接 引用目标对象,代理对象作为访问对象和目标对象之间的中介。
Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生 成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。
(2)结构
代理(Proxy)模式分为三种角色:
- 抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对 象,是最终要引用的对象。
- 代理(Proxy)类 : 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访 问、控制或扩展真实主题的功能。
(3)静态代理
【例】火车站卖票
如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。类图如下:
【SellTickets卖票接口】
//卖票接口 |
【TrainStation火车站】
public class TrainStation implements SellTickets { |
【PoroxyPoint代售点】
public class ProxyPoint implements SellTickets { |
【Client】测试类
public class Client { |
从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。
(4)动态代理
接下来我们使用动态代理实现上面案例,先说说JDK提供的动态代理。Java中提供了一个动态代理类 Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法 (newProxyInstance方法)来获取代理对象。
【SellTickets卖票接口】
public interface SellTickets { |
【TrainStation火车站】
public class TrainStation implements SellTickets { |
【ProxyFactory代理工厂】
public class ProxyFactory { |
【Client】测试类
public class Client { |
思考:ProxyFactory是代理类吗?
ProxyFactory不是代理模式中所说的代理类,它是一个代理工厂,工厂中提供的一个方法用于获取代理对象。而代理类是在程序运行过程中的动态的在内存中生成的类。
可以通过上述测试类中的object.getClass()
获取动态代理类的全限定类名class com.sun.proxy.$Proxy0
。
我们通过Arthas工具jad
查看代理类的结构:
public final class $Proxy0 extends Proxy implements SellTickets { |
- 代理类($Proxy0)实现了SellTickets接口,也就是说,使用动态代理时,真实类和代理类实现同样的接口。
- 代理类($Proxy0)将我们提供的匿名内部类对象传递给了父类。
动态代理的执行流程
- 在测试类中通过代理对象调用sell()方法
- 根据多态的特性,执行的是代理类($Proxy0)中的sell()方法
- 代理类($Proxy0)中的sell()方法中又调用了InvocationHandler接口的自实现类对象的invoke方法
- invoke方法通过反射执行了真实对象所属类(TrainStation)中的sell()方法
(5)CGLIB动态代理
同样是上面的案例,我们再次使用CGLIB代理实现。
如果没有定义SellTickets接口,只定义了TrainStation(火车站类)。很显然JDK代理是无法使用 了,因为JDK动态代理要求必须定义接口,对接口进行代理。
CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提 供了很好的补充。
CGLIB是第三方提供的包,所以需要引入jar包的坐标:
<dependency> |
【TrainStation火车站】
public class TrainStation{ |
【ProxyFactory代理工厂类】
public class ProxyFactory implements MethodInterceptor { |
(6)三种代理的对比
JDK代理和CGLIB代理
使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在 JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者 方法进行代理,因为CGLib原理是动态生成被代理类的子类。
在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代 理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率 低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代 理,如果没有接口使用CGLIB代理。
动态代理和静态代理
动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题。
(7)优缺点
优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
缺点:
- 增加了系统的复杂度;
(8)使用场景
远程(Remote)代理
本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中 可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一 个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。
防火墙(Firewall)代理
当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。
保护(Protect or Access)代理
控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。
5.2 适配器模式
(1)概述
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能 一起工作。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现 有组件库中的相关组件的内部结构,所以应用相对较少些。
(2) 结构
适配器模式(Adapter)包含以下主要角色:
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成 目标接口,让客户按目标接口的格式访问适配者。
(3)类适配器模式
实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。
【例】读卡器
现有一台电脑只能读取SD卡,而要读取TF卡中的内容的话就需要使用到适配器模式。创建一个读卡 器,将TF卡中的内容读取出来。
类图如下:
代码如下:
【SDCard接口】
public interface SDCard { |
【SDCardImpl实现类】
public class SDCardImpl implements SDCard { |
【Computer电脑类】
public class Computer { |
【TFCard接口】
public interface TFCard { |
【TFCardImpl实现类】
public class TFCardImpl implements TFCard { |
【SDAdapterTF适配器类(SD兼容TF)】
public class SDAdapterTF extends TFCardImpl implements SDCard { |
【Test测试类】
public class Client { |
类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用。
(4)对象适配器模式
实现方式:对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。
【例】读卡器
我们使用对象适配器模式将读卡器的案例进行改写。类图如下:
代码如下:
类适配器模式的代码,我们只需要修改适配器类(SDAdapterTF)和测试类。
【SDAdapterTF适配器类对象】
public class SDAdapterTF implements SDCard { |
【Test测试类】
public class Client { |
注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。
(5) 应用场景
- 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
- 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。
5.3 装饰者模式
(1)概述
我们先来看一个快餐店的例子。
快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱, 每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。
使用继承的方式存在的问题:
扩展性不好
如果要再加一种配料(火腿肠),我们就会发现需要给FriedRice和FriedNoodles分别定义一 个子类。如果要新增一个快餐品类(炒河粉)的话,就需要定义更多的子类。
产生过多的子类
装饰者模式是指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。
(2)结构
装饰(Decorator)模式中的角色:
- 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(ConcreteDecorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
(3)案例
我们使用装饰者模式对快餐店案例进行改进,体会装饰者模式的精髓。
类图如下:
代码:
【FastFood抽象类】
public abstract class FastFood { |
【FriedRice炒饭类】
public class FriedRice extends FastFood{ |
【FriedNoodles炒面类】
public class FriedNoodles extends FastFood { |
【Garnish配料类】
public abstract class Garnish extends FastFood{ |
【Egg配料类】
public class Egg extends Garnish{ |
【Bacon配料类】
public class Bacon extends Garnish { |
【Client测试类】
public class Client { |
(4)优缺点
优点:
- 装饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象 来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭 原则,继承是静态的附加责任,装饰者则是动态的附加责任。
- 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以 动态扩展一个实现类的功能。
(5)使用场景
当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
不能采用继承的情况主要有两类:
① 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目 呈爆炸性增长;
② 第二类是因为类定义不能继承(如final类)
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
当对象的功能要求可以动态地添加,也可以再动态地撤销时。
(6)实例
IO流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream, BufferedReader,BufferedWriter。
以BufferedWrite举例来说明,如下代码:
//创建BufferedWriter对象 |
BufferedWrite使用装饰者模式对Writer打的子实现类进行了增强,添加了缓冲区,提高了读写效率。
(7)代理和装饰的区别
静态代理和装饰者模式的区别:
- 相同点:
- 都要实现与目标类相同的业务接口
- 在两个类中都要声明目标对象
- 都可以在不修改目标类的前提下增强目标方法
- 不同点:
- 目的不同 装饰者是为了增强目标对象 静态代理是为了保护和隐藏目标对象
- 获取目标对象构建的地方不同 装饰者是由外界传递进来,可以通过构造方法传递 静态代理 是在代理类内部创建,以此来隐藏目标对象
5.4 桥接模式
(1)概述
现在有一个需求,需要创建不同的图形,并且每个图形都有可能会有不同的颜色。我们可以利用继承的 方式来设计类的关系:
我们可以发现有很多的类,假如我们再增加一个形状或再增加一种颜色,就需要创建更多的类。
试想,在一个有多种可能会变化的维度的系统中,用继承方式会造成类爆炸,扩展起来不灵活。每次在 一个维度上新增一个具体实现都要增加多个子类。为了更加灵活的设计系统,我们此时可以考虑使用桥 接模式。
桥接模式将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
(2)结构
桥接(Bridge)模式包含以下主要角色:
- 抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法, 并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。
(3)案例
【例】视频播放器
需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI、WMV等。该播放器包含了两个维度,适合使用桥接模式。
类图如下:
代码如下:
【VideoFile接口】
public interface VideoFile { |
【AVIFile视频类】
public class AVIFile implements VideoFile { |
【REVBBFile视频类】
public class REVBBFile implements VideoFile{ |
【OperatingSystemVersion操作系统版本抽象类】
public abstract class OperatingSystemVersion { |
【Windows具体类】
public class Windows extends OperatingSystemVersion{ |
【Mac具体类】
public class Mac extends OperatingSystemVersion{ |
【Client测试类】
public class Client { |
优点:
- 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。 如:如果现在还有一种视频文件类型wmv,我们只需要再定义一个类实现VideoFile接口即可, 其他类不需要发生变化。
- 实现细节对客户透明
(4)使用场景
- 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
- 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。
- 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。避免在两个层次之间 建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
5.5 外观模式
(1)概述
外观模式又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
外观(Facade)模式是“迪米特法则”的典型应用。
(2) 结构
外观(Facade)模式包含以下主要角色:
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口
- 子系统(Sub Systen)角色:实现系统的部分功能,客户可以通过外观角色访问它
(3)案例
【例】智能家电控制
小明的爷爷已经60岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、 关闭电视、关闭空调;操作起来都比较麻烦。所以小明给爷爷买了智能音箱,可以通过语音直接控制这 些智能家电的开启和关闭。类图如下:
代码如下:
【Light灯类】
public class Light { |
【TV电视类】
public class TV { |
【AirCondition空调类】
public class AirCondition { |
【SmartAppliancesFacade智能控制类】
public class SmartAppliancesFacade { |
【Client测试类】
public class Client { |
优点:
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
缺点:
- 不符合开闭原则,修改很麻烦
(4)使用场景
- 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系。
- 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问。
- 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独 立性和可移植性。
5.6 组合模式
(1)概述
组合模式又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对 象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
(2)结构
组合模式主要包括三种角色:
- 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
- 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一 个树形结构。
- 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。
(3)案例
【例】软件菜单
我们在访问别的一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单 项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜 单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。
类图如下:
代码如下:
【MenuComponent菜单组件类】
// 不管是菜单还是菜单项,都应该继承该类 |
这里的MenuComponent定义为抽象类,因为有一些共有的属性和行为要在该类中实现,Menu和 MenuItem类就可以只覆盖自己感兴趣的方法,而不用搭理不需要或者不感兴趣的方法,举例来说, Menu类可以包含子菜单,因此需要覆盖add()、remove()、getChild()方法,但是MenuItem就不 应该有这些方法。这里给出的默认实现是抛出异常,你也可以根据自己的需要改写默认实现。
【Menu菜单类】
public class Menu extends MenuComponent { |
Menu菜单类实现除了getName方法之外的其他所有方法,因为Menu菜单类具有添加菜单项、移除菜单项和获取子菜单的功能。
【MenuItem菜单项】
public class MenuItem extends MenuComponent{ |
MenuItem是菜单项,不能再有子菜单,所以添加菜单,移除菜单和获取子菜单的功能并不能实现。
【Client测试类】
public class Client { |
打印结果:
学习 |
(4)分类
在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式 两种形式。
透明组合模式
透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中 MenuComponent 声明了 add 、 remove 、 getChild 方法,这样做的好处是确保所有的构件 类都有相同的接口。透明组合模式也是组合模式的标准形式。
透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能 有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有 意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应 的错误处理代码)
安全组合模式
在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点 Menu 类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
(5)优点
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结 构,简化了客户端代码。
- 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭 原则”。
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归 组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。
(6)应用场景
组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显 示,多级目录呈现等树形结构数据的操作。
5.7 享元模式
(1)概述
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建 的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。
(2)结构
享元(Flyweight )模式中存在以下两种状态:
- 内部状态,即不会随着环境的改变而改变的可共享部分。
- 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
- 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法 来设置外部数据(外部状态)。
- 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元 类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享 元类提供唯一的享元对象。
- 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元 对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在 的话,则创建一个新的享元对象。
(3)案例
【例】俄罗斯方块
下面的图片是众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块 都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。
类图如下:
代码如下:
【AbstractBox抽象方块类】
俄罗斯方块有不同的形状,我们可以对这些形状向上抽取出AbstractBox,用来定义共性的属性和行为。
public abstract class AbstractBox { |
【具体方块类】
public class IBox extends AbstractBox{ |
【方块工厂类】
提供一个方块工厂类,用来管理享元对象(也就是AbstractBox子类对象),该工厂类对象只需要一个,所以可以使用单例模式。
public class BoxFactory { |
之后如果我们想要生产一个具体的方块,比如L形方块,我们就可以直接调用BoxFactory.getInstance()
获取工厂,通过工厂获取指定的方块实例,并且多次调用返回的是一个实例。
(4)优缺点
优点:
- 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
- 享元模式中的外部状态相对独立,且不影响内部状态
缺点:
- 为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
第六章 行为型模式
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单 个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚 合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象 行为模式比类行为模式具有更大的灵活性。
行为型模式分为:
- 模板方法模式
- 策略模式
- 命令模式
- 职责链模式
- 状态模式
- 观察者模式
- 中介者模式
- 迭代器模式
- 访问者模式
- 备忘录模式
- 解释器模式
以上11种行为型模式,除了模板方法模式和解释器模式是类行为模式,其他全部属于对象行为型模式。
6.1 模板方法模式
(1)概述
模板方法模式定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情 况下重定义该算法的某些特定步骤。
(2)结构
模板方法(Template Method)模式包含以下主要角色:
- 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
- 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
- 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三 种:
- 抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
- 具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现, 其子类可以进行覆盖也可以直接继承。
- 钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
- 具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
(3) 案例
【例】炒菜
炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:
代码如下:
【AbstractClass抽象模板类】
public abstract class AbstractClass { |
【炒菜具体子类】
public class ConcreteClass_BaoCai extends AbstractClass{ |
【测试类】
public class Client { |
为了防止恶意操作,一般模板方法上都加上final关键字。
(4)优缺点
优点:
- 提高代码复用性:将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
- 实现了反向控制:通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽 象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构, 它提高了代码阅读的难度。
(5)适用场景
- 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分 抽象出来,供子类实现。
- 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
6.2 策略模式
策略模式该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
(1)结构
策略模式的主要角色如下:
- 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
(2)案例
【例】促销活动
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动, 由促销员将促销活动展示给客户。类图如下:
代码如下:
【Strategy共同促销策略接口】所有促销活动的共同接口
public interface Strategy { |
【具体策略角色】每个节日具体的促销活动
public class StrategyA implements Strategy{ |
【环境角色】用于连接上下文,即把促销活动推销给用户
public class SalesMan { |
(3)优缺点
优点:
- 策略类之间可以相互切换。由于策略类都实现同一个接口,所以使它们之间可以自由切换。
- 易于扩展。增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合开闭原则。
- 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。
缺点:
- 客户端必须知道所有的策略类,并决定使用哪一个策略类。
- 策略模式将产生很多的策略类,可以使用享元模式在一定程度上减少对象的数量。
(4)适用场景
- 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条 件分支移入它们各自的策略类中以代替这些条件语句。
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
- 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结 构。
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。
6.3 命令模式
命令模式将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。
(1)结构
命令模式包含以下主要角色:
- 抽象命令类(Command)角色: 定义命令的接口,声明执行的方法。
- 具体命令(Concrete Command)角色:具体的命令,实现命令接口;通常会持有接收者,并调 用接收者的功能来完成命令要执行的操作。
- 实现者/接收者(Receiver)角色: 接收者,真正执行命令的对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能。
- 调用者/请求者(Invoker)角色: 要求命令对象执行请求,通常会持有命令对象,可以持有很 多的命令对象。这个是客户端真正触发命令并要求命令执行相应操作的地方,也就是说相当于使用 命令对象的入口。
(2)案例实现
【点餐场景】
顾客把订单交给服务员,服务员拿到订单给厨师下达命令。厨师根据订单准备餐。
将上面的案例用代码实现,那我们就需要分析命令模式的角色在该案例中由谁来充当。
- 服务员:调用者角色,发起命令的角色。
- 厨师:接收者角色,真正执行命令的角色。
- 订单:命令中包含的订单。
类图如下:
【Command抽象命令类】
public interface Command { |
【OrderCommand具体命令类】将命令执行者和命令封装到一起。
public class OrderCommand implements Command { |
【Order具体命令执行类】
public class Order { |
【SeniorChef命令执行者】
public class SeniorChef { |
【Waiter命令传递者】
public class Waiter { |
【Client测试类】
public class Client { |
(3) 优缺点
优点:
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
- 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”, 对扩展比较灵活。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类。
- 系统结构更加复杂。
(4)适用场景
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
(5)Runnable应用
Runable是一个典型命令模式,Runnable担当命令的角色,Thread充当的是调用者,start方法就 是其执行方法。
public interface Runnable { |
public synchronized void start() { |
会调用一个native方法start0(),调用系统方法,开启一个线程。而接收者是对程序员开放的,可以 自己定义接收者。
如下代码:
public class Receiver { |
6.4 责任链模式
责任链模式又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对 象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
(1) 结构
职责链模式主要包含以下角色:
- 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
- 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请 求,如果可以处理请求则处理,否则将该请求转给它的后继者。
- 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节 和请求的传递过程。
(2)案例实现
现需要开发一个请假流程控制系统。请假一天以下的假只需要小组长同意即可;请假1天到3天的假还需 要部门经理同意;请求3天到7天还需要总经理同意才行。
类图如下:
代码如下:
【LeaveRequest请假条实例类】
public class LeaveRequest { |
【Handler抽象审批类】
public abstract class Handler { |
【具体审批类】
public class GroupLeader extends Handler{ |
【Client测试类】
public class Client { |
(3) 优缺点
优点:
- 降低了对象之间的耦合度。该模式降低了请求发送者和接收者的耦合度。
- 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
- 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。
- 责任链简化了对象之间的连接。一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多 的 if 或者 if···else 语句。
- 责任分担。每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围, 符合类的单一职责原则。
缺点:
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理, 该请求可能一直传到链的末端都得不到处理。
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而 导致系统出错,如可能会造成循环调用。
6.5 状态模式
【例】通过按钮来控制一个电梯的状态,一个电梯有开门状态,关门状态,停止状态,运行状态。每一 种状态改变,都有可能要根据其他状态来更新处理。例如,如果电梯门现在处于运行时状态,就不能进 行开门操作,而如果电梯门是停止状态,就可以执行开门操作。
类图如下:
代码如下:
【ILift状态接口】
public interface ILift { |
【Lift电梯实例】
public class Lift implements ILift{ |
【Client测试类】
public class Client { |
问题分析:
- 使用了大量的switch…case这样的判断(if…else也是一样),使程序的可阅读性变差。
- 扩展性很差。如果新加了断电的状态,我们需要修改上面判断逻辑
状态模式对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变 时改变其行为。
(1)结构
状态模式包含以下主要角色。
- 环境(Context)角色:也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并 将与状态相关的操作委托给当前状态对象来处理。
- 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
- 具体状态(Concrete State)角色:实现抽象状态所对应的行为。
(2)案例实现
对上述电梯的案例使用状态模式进行改进。类图如下:
代码如下:
【LiftState抽象状态类】
public abstract class LiftState { |
【具体状态类】
// 开启状态类 |
【Context环境角色类】
public class Context { |
【Client测试类】
public class Client { |
(3)优缺点
优点:
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态 即可改变对象的行为。
- 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
缺点:
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对”开闭原则”的支持并不太好。
(4)使用场景
- 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
- 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。
6.6 观察者模式
观察者模式又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者 对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自 动更新自己。
(1)结构
在观察者模式中有如下角色:
- Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
- ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
- Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
- ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知 时更新自身的状态。
(2)案例实现
【例】微信公众号
在使用微信公众号时,大家都会有这样的体验,当你关注的公众号中有新内容更新的话,它就会推送给关注公众号的微信用户端。我们使用观察者模式来模拟这样的场景,微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了程序猿这个公众号。
类图如下:
代码如下:
【Observer抽象观察者类】
public interface Observer { |
【WeiXinUser具体观察者类】
public class WeiXinUser implements Observer{ |
【Subject抽象主题类】
public interface Subject { |
【SubscriptionSubject具体主题类】
public class SubscriptionSubject implements Subject{ |
【Client测试类】
public class Client { |
(3)优缺点
优点:
- 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系
- 被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制】
缺点:
- 如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
- 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃
(4)使用场景
- 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
- 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时。
(5)JDK中提供的实现
在 Java 中,通过 java.util.Observable
类和 java.util.Observer
接口定义了观察者模 式,只要实现它们的子类就可以编写观察者模式实例。
Observable类
Observable 类是抽象目标类(被观察者),它有一个 Vector 集合成员变量,用于保存所有要通 知的观察者对象,下面来介绍它最重要的 3 个方法。
void addObserver(Observer o)
方法:用于将新的观察者对象添加到集合中。void notifyObservers(Object arg)
方法:调用集合中的所有观察者对象的 update方法,通知它们数据发生改变。通常越晚加入集合的观察者越先得到通知。void setChange()
方法:用来设置一个 boolean 类型的内部标志,注明目标对象发生了变 化。当它为true时,notifyObservers() 才会通知观察者。
Observer接口
Observer 接口是抽象观察者,它监视目标对象的变化,当目标对象发生变化时,观察者得到通知, 并调用 update 方法,进行相应的工作。
【例】警察抓小偷
警察抓小偷也可以使用观察者模式来实现,警察是观察者,小偷是被观察者。代码如下:
小偷是一个被观察者,所以需要继承Observable类:
public class Thief extends Observable { |
警察是一个观察者,所以需要让其实现Observer接口
public class Policemen implements Observer { |
【Client测试类】
public class Client { |
6.7 中介模式
一般来说,同事类之间的关系是比较复杂的,多个同事类之间互相关联时,他们之间的关系会呈现为复 杂的网状结构,这是一种过度耦合的架构,即不利于类的复用,也不稳定。例如在下左图中,有六个同 事类对象,假如对象1发生变化,那么将会有4个对象受到影响。如果对象2发生变化,那么将会有5个 对象受到影响。也就是说,同事类之间直接关联的设计是不好的。
如果引入中介者模式,那么同事类之间的关系将变为星型结构,从下右图中可以看到,任何一个类的变 动,只会影响的类本身,以及中介者,这样就减小了系统的耦合。一个好的设计,必定不会把所有的对 象关系处理逻辑封装在本类中,而是使用一个专门的类来管理那些不属于自己的行为。
中介模式又叫调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以 独立地改变它们之间的交互。
(1)结构
中介者模式包含以下主要角色:
- 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
- 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
- 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽 象方法,实现所有相互影响的同事类的公共功能。
- 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交 互时,由中介者对象负责后续的交互。
(2)案例实现
【例】租房
现在租房基本都是通过房屋中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。房屋中介充当租房者与房屋所有者之间的中介者。
类图如下:
代码如下:
【Mediator抽象中介者类】
public abstract class Mediator { |
【Person抽象同事类】
public abstract class Person { |
【HouseOwner具体同事类】
// 房屋拥有者 |
【MediatorStructure具体中介类】
public class MediatorStructure extends Mediator{ |
【Client测试类】
public class Client { |
(3)优缺点
优点:
- 松散耦合。中介者模式通过把多个同事对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦 合,基本上可以做到互补依赖。这样一来,同事对象就可以独立地变化和复用,而不再像以前那样 “牵一处而动全身”了。
- 集中控制交互。多个同事对象的交互,被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只 需要修改中介者对象就可以了,当然如果是已经做好的系统,那么就扩展中介者对象,而各个同事 类不需要做修改。
- 一对多关联转变为一对一的关联。没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现。
缺点:
- 当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。
(4)使用场景
- 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。
- 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。
6.8 迭代器模式
迭代器模式提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
(1)结构
迭代器模式主要包含以下角色:
- 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口。
- 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
- 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、 next() 等方法。
- 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对 象的遍历,记录遍历的当前位置。
(2)案例
【例】定义一个可以存储学生对象的容器对象,将遍历该容器的功能交由迭代器实现,涉及到的类如下:
代码如下:
【迭代器接口】
public interface StudentIterator { |
【具体的迭代器类】
public class StudentIteratorImpl implements StudentIterator { |
【抽象容器类】
public interface StudentAggregate { |
【具体容器类】
public class StudentAggregateImpl implements StudentAggregate{ |
【测试类】
public class Client { |
(3)优缺点
优点:
- 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模 式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
- 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方 法,这样可以简化聚合类的设计。
- 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码, 满足 “开闭原则” 的要求。
缺点:
- 增加了类的个数,这在一定程度上增加了系统的复杂性。
(4)适用场景
- 当需要为聚合对象提供多种遍历方式时。
- 当需要为遍历不同的聚合结构提供一个统一的接口时。
- 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。
(5)集合源码应用
迭代器模式在JAVA的很多集合类中被广泛应用,接下来看看JAVA源码中是如何使用迭代器模式的。
List<String> list = new ArrayList<>(); |
看完这段代码是不是很熟悉,与我们上面代码基本类似。单列集合都使用到了迭代器,我们以 ArrayList举例来说明
- List:抽象聚合类
- ArrayList:具体的聚合类
- Iterator:抽象迭代器
- list.iterator():返回的是实现了 Iterator 接口的具体迭代器对象
6.9 访问者模式
访问者模式封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这 些元素的新的操作。
(1)结构
访问者模式包含以下主要角色:
- 抽象访问者(Visitor)角色:定义了对每一个元素 (Element) 访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
- 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为。
- 抽象元素(Element)角色:定义了一个接受访问者的方法( accept ),其意义是指,每一个元素都要可以被访问者访问。
- 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通 常情况下是使用访问者提供的访问该元素类的方法。
- 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表 述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素 ( Element ),并且可以迭代这些元素,供访问者访问。
(2)案例实现
【例】给宠物喂食
现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。
- 访问者角色:给宠物喂食的人
- 具体访问者角色:主人、其他人
- 抽象元素角色:动物抽象类
- 具体元素角色:宠物狗、宠物猫
- 结构对象角色:主人家
类图如下:
代码如下:
【抽象访问者接口】
public interface Person { |
【具体访问者实例】
public class Owner implements Person { |
【抽象节点】
public interface Animal { |
【具体节点】
public class Dog implements Animal{ |
【对象结构】
public class Home { |
【测试】
public class Client { |
(3)优缺点
优点:
- 扩展性好。在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
- 复用性好。通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
- 分离无关行为。通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的 功能都比较单一。
缺点:
- 对象结构变化很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这 违背了“开闭原则”。
- 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。
(4)使用场景
- 对象结构相对稳定,但其操作算法经常变化的程序。
- 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
(5)扩展
访问者模式用到了一种双分派的技术。
① 分派
变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的 真实类型又叫做变量的实际类型。比如 Map map = new HashMap()
,map变量的静态类型是 Map ,实际类型是 HashMap 。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派 (Dispatch)又分为两种,即静态分派和动态分派。
静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来 说并不陌生,方法重载就是静态分派。
动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。
② 动态分派
通过方法的重写支持动态分派。
public class Animal { |
重写,运行执行的子类中的方法。Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不 知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
③ 静态分派
通过方法重载支持静态分派。
public class Animal { } |
这是为什么呢?因为重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。
④ 双分派
所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别, 还要根据参数的运行时区别。
public class Animal { |
在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也 将自己this作为参 数传递进去,这里就完成了第二次分派,这里的Execute类中有多个重载的方法,而传递进行的是this, 就是具体的实际类型的对象。
说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定, 我们可以对上面的程序进行修改。
上述案例的运行结果如下:
animal |
双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动 态的,所以重载就是动态的了。
6.10 备忘录模式
备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,很多软件都提供了撤销 (Undo)操作,如 Word、记事本、Photoshop、IDEA等软件在编辑时按 Ctrl+Z 组合键时能撤销 当前操作,使文档恢复到之前的状态;还有在 浏览器 中的后退键、数据库事务管理中的回滚操作、玩 游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。
备忘录模式又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态, 以便以后当需要时能将该对象恢复到原先保存的状态。
(1)结构
备忘录模式的主要角色如下:
- 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据 的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
- 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
- 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备 忘录的内容进行访问与修改。
备忘录有两个等效的接口:
- 窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。
- 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。
(2)案例实现
【例】游戏挑战BOSS
游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打Boss前和后一定会不一样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗之前的状态。
要实现上述案例,有两种方式:
- “白箱”备忘录模式
- “黑箱”备忘录模式
① 白箱备忘录模式
备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公 开。类图如下:
【游戏角色类】
public class GameRole { |
【游戏状态备忘类】
public class RoleStateMemento { |
【角色状态管理者类】
public class RoleStateCaretaker { |
【测试类】
public class Client { |
白箱备忘录模式是破坏封装性的。但是通过程序员自律,同样可以在一定程度上实现模式 的大部分用意。
② 黑箱备忘录模式
备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。
将 RoleStateMemento 设为 GameRole 的内部类,从而将 RoleStateMemento 对象封装在 GameRole 里面;在外面提供一个标识接口 Memento 给 RoleStateCaretaker 及其他对象使用。 这样 GameRole 类看到的是 RoleStateMemento 所有的接口,而 RoleStateCaretaker 及其他对 象看到的仅仅是标识接口 Memento 所暴露出来的接口,从而维护了封装型。类图如下:
代码如下:
【窄接口 Memento】作为一个标识接口。
public interface Memento { } |
【发起人类 GameRole】在内部定义备忘录内部类 RoleStateMemento (该内部类设置为私有的)
package zal.design; |
【负责人角色类 RoleStateCaretaker】
负责人角色类 RoleStateCaretaker 能够得到的备忘录对象是以 Memento 为接口的,由于这个接 口仅仅是一个标识接口,因此负责人角色不可能改变这个备忘录对象的内容。
public class RoleStateCaretaker { |
【测试类】
public class Client { |
(3)优缺点
优点:
- 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
- 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
- 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录 中,并由管理者进行管理,这符合单一职责原则。
缺点:
- 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
(4)适用场景
- 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
- 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,idea等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。
6.11 解释器模式
【例】设计一个软件用来进行加减计算。我们第一想法就是使用工具类,提供对应的加法和减法的工 具方法。
//用于两个整数相加 |
上面的形式比较单一、有限,如果形式变化非常多,这就不符合要求,因为加法和减法运算,两个运算符与数值可以有无限种组合方式。比如 1+2+3+4+5、1+2+3-4等等。
显然,现在需要一种翻译识别机器,能够解析由数字以及 + - 符号构成的合法的运算序列。如果把运算符和数字都看作节点的话,能够逐个节点的进行读取解析运算,这就是解释器模式的思维。
在解释器模式中,我们需要将待解决的问题,提取出规则,抽象为一种“语言”。比如加减法运算,规则为:由数值和+-符号组成的合法序列,“1+3-2” 就是这种语言的句子。
解释器就是要解析出来语句的含义。但是如何描述规则呢?
文法(语法)规则
文法是用于描述语言的语法结构的形式规则。
expression ::= value | plus | minus |
注意: 这里的符号“::=”表示“定义为”的意思,竖线 | 表示或,左右的其中一个,引号内为字 符本身,引号外为语法。
上面规则描述为 :
表达式可以是一个值,也可以是plus或者minus运算,而plus和minus又是由表达式结合运算符构 成,值的类型为整型数。
抽象语法树
在计算机科学中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree), 是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示 源代码中的一种结构。
用树形来表示符合文法规则的句子。
(1)结构
解释器模式包含以下主要角色。
- 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
- 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
- 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法 中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
- 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有 解释器共享的数据,后面的解释器可以从这里获取这些值。
- 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
(2)案例实现
【例】设计实现加减法的软件
代码如下:
【AbstractExpression抽象表达式】
public abstract class AbstractExpression { |
【Value终结符表达式】
public class Value extends AbstractExpression{ |
【plus、Minus非终结符表达式】
public class Plus extends AbstractExpression { |
【Variable终结符表达式】
public class Variable extends AbstractExpression{ |
【Context环境类】
public class Context { |
【测试类】
public class Client { |
(3)优缺点
优点:
- 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。 每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。
- 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。
- 增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原 有表达式类代码无须修改,符合 “开闭原则”。
缺点:
- 对于复杂文法难以维护。在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个 数将会急剧增加,导致系统难以管理和维护。
- 执行效率较低。由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而 且代码的调试过程也比较麻烦。
(4)适用场景
- 当语言的文法较为简单,且执行效率不是关键问题时。
- 当问题重复出现,且可以用一种简单的语言来进行表达时。
- 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候。