Java——设计模式(结构型模式)
一、适配器模式(不兼容结构的协调)
在适配器模式中引入了一个被称为适配器(Adapter)的包装类,而它所包装的对象称为适配者(Adaptee),即被适配的类。适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器让那些由于接口不兼容而不能交互的类可以一起工作。(实际上客户端拿到的对象,已经是适配器的对象了)
适配器模式可以将一个类的接口和另一个类的接口匹配起来,而无须修改原来的适配者接口和抽象目标类接口。适配器模式定义如下:
适配器模式(Adapter Pattern):将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起 工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。(实现不改代码,只添加类和修改配置文件完成调用其它对象方法)
- Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。
- Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。
- Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。
根据对象适配器模式结构图,在对象适配器中,客户端需要调用 request() 方法,而适配者类 Adaptee 没有该 方法,但是它所提供的 specificRequest() 方法却是客户端所需要的。为了使客户端能够使用适配者类,需要提 供一个包装类 Adapter,即适配器类。这个包装类包装了一个适配者的实例,从而将客户端与适配者衔接起 来,在适配器的 request() 方法中调用适配者的 specificRequest() 方法。因为适配器类与适配者类是关联关 系(也可称之为委派关系),所以这种适配器模式称为对象适配器模式。典型的对象适配器代码如下所示:
/** * @author x5456 */ public class AdaptorPattern { //抽象成绩操作类:目标接口 interface ScoreOperation { public int[] sort(int array[]); //成绩排序 public int search(int array[], int key); //成绩查找 } //快速排序类:适配者 class QuickSort { public int[] quickSort(int array[]) { sort(array, 0, array.length - 1); return array; } public void sort(int array[], int p, int r) { int q = 0; if (p < r) { q = partition(array, p, r); sort(array, p, q - 1); sort(array, q + 1, r); } } public int partition(int[] a, int p, int r) { int x = a[r]; int j = p - 1; for (int i = p; i <= r - 1; i++) { if (a[i] <= x) { j++; swap(a, j, i); } } swap(a, j + 1, r); return j + 1; } public void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; } } //二分查找类:适配者 class BinarySearch { public int binarySearch(int array[], int key) { int low = 0; int high = array.length - 1; while (low <= high) { int mid = (low + high) / 2; int midVal = array[mid]; if (midVal < key) { low = mid + 1; } else if (midVal > key) { high = mid - 1; } else { return 1; //找到元素返回1 } } return -1; //未找到元素返回-1 } } //操作适配器:适配器 class OperationAdapter implements ScoreOperation { private QuickSort sortObj; //定义适配者QuickSort对象 private BinarySearch searchObj; //定义适配者BinarySearch对象 public OperationAdapter() { sortObj = new QuickSort(); searchObj = new BinarySearch(); } public int[] sort(int array[]) { return sortObj.quickSort(array); //调用适配者类QuickSort的排序方法 } public int search(int array[], int key) { return searchObj.binarySearch(array, key); //调用适配者类BinarySearch的查找方法 } } } // 调用者 class Client { public static void main(String args[]) { ScoreOperation operation; //针对抽象目标接口编程 operation = (ScoreOperation) XMLUtil.getBean(); //读取配置文件,反射生成对象(只需要修改配置文件,改成OperationAdapter就行了) int scores[] = {84, 76, 50, 69, 90, 91, 88, 96}; //定义成绩数组 int result[]; int score; System.out.println("成绩排序结果:"); result = operation.sort(scores); //遍历输出成绩 for (int i : scores) { System.out.print(i + ","); } System.out.println(); System.out.println("查找成绩90:"); score = operation.search(result, 90); if (score != -1) { System.out.println("找到成绩90。"); } else { System.out.println("没有找到成绩90。"); } System.out.println("查找成绩92:"); score = operation.search(result, 92); if (score != -1) { System.out.println("找到成绩92。"); } else { System.out.println("没有找到成绩92。"); } } }
类适配器
除了对象适配器模式之外,适配器模式还有一种形式,那就是类适配器模式,类适配器模式和对象适配器模式最大的区别在于适配器和适配者之间的关系不同,对象适配器模式中适配器和适配者之间是关联关系,而类适配器模式中适配器和适配者是继承关系
class Adapter extends Adaptee implements Target { public void request() { specificRequest(); } }
由于 Java、C# 等语言不支持多重类继承,因此类适配器的使用受到很多限制,例如如果目标抽象类 Target 不 是接口,而是一个类,就无法使用类适配器;此外,如果适配者 Adapter 为最终(Final)类,也无法使用类适 配器。在 Java 等面向对象编程语言中,大部分情况下我们使用的是对象适配器,类适配器较少使用。(类适配受java中无法多继承的限制)
优点
无论是对象适配器模式还是类适配器模式都具有如下优点:
- (1) 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
- (2) 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提 高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。
- (3) 灵活性和扩展性都非常好,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有代码的基础上 增加新的适配器类,完全符合“开闭原则”。
具体来说,类适配器模式还有如下优点:
- 由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
对象适配器模式还有如下优点:
- (1) 一个对象适配器可以把多个不同的适配者适配到同一个目标;
- (2) 可以适配一个适配者的子类,由于适配器和适配者之间是关联关系,根据“里氏代换原则”,适配者的子类也 可通过该适配器进行适配。
缺点
类适配器模式的缺点如下:
- (1) 对于 Java、C# 等不支持多重类继承的语言,一次最多只能适配一个适配者类,不能同时适配多个适配者;
- (2) 适配者类不能为最终类,如在 Java 中不能为 final 类,C# 中不能为 sealed 类;
- (3) 在 Java、C# 等语言中,类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。
对象适配器模式的缺点如下:
- 与类适配器模式相比,要在适配器中置换适配者类的某些方法比较麻烦。如果一定要置换掉适配者类的一个或多个方法,可以先做一个适配者类的子类,将适配者类的方法置换掉,然后再把适配者类的子类当做真正的适配者进行适配,实现过程较为复杂。
适用场景
- (1) 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需要,甚至没有这些类的源代码。
- (2) 想创建一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一 起工作。
二、桥接模式(处理多维度变化)
引文
在正式介绍桥接模式之前,我先跟大家谈谈两种常见文具的区别,它们是毛笔和蜡笔。假如我们需要大中小 3 种 型号的画笔,能够绘制 12 种不同的颜色,如果使用蜡笔,需要准备 3×12 = 36 支,但如果使用毛笔的话,只需 要提供 3 种型号的毛笔,外加 12 个颜料盒即可,涉及到的对象个数仅为 3 + 12 = 15,远小于36,却能实现与 3 6 支蜡笔同样的功能。如果增加一种新型号的画笔,并且也需要具有 12 种颜色,对应的蜡笔需增加 12 支,而毛 笔只需增加一支。为什么会这样呢?通过分析我们可以得知:在蜡笔中,颜色和型号两个不同的变化维度(即两 个不同的变化原因)融合在一起,无论是对颜色进行扩展还是对型号进行扩展都势必会影响另一个维度;但在毛 笔中,颜色和型号实现了分离,增加新的颜色或者型号对另一方都没有任何影响。如果使用软件工程中的术 语,我们可以认为在蜡笔中颜色和型号之间存在较强的耦合性(从而违反了单一原则:一个类只干一件事,否则如果另一件事变了,那么你这个类又要修改),而毛笔很好地将二者解耦,使用起来非常灵 活,扩展也更为方便。在软件开发中,我们也提供了一种设计模式来处理与画笔类似的具有多变化维度的情 况,即本章将要介绍的桥接模式。
桥接模式(JDBC的设计采用该模式)
桥接模式是一种很实用的结构型设计模式,如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。
桥接模式用一种巧妙的方式处理多层继承存在的问题,用抽象关联取代了传统的多层继承,将类之间的静态继承关系转换为动态的对象组合关系,使得系统更加灵活,并易于扩展,同时有效控制了系统中类的个数。桥接定义如下:
桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。
桥接模式的结构与其名称一样,存在一条连接两个继承等级结构的桥,桥接模式结构如图所示:
- Abstraction(抽象类):用于定义抽象类的接口,它一般是抽象类而不是接口,其中定义了一个 Implementor(实现类接口)类型的对象并可以维护该对象,它与 Implementor 之间具有关联关系,它既可以包含抽象业务方法,也可以包含具体业务方法。
- RefinedAbstraction(扩充抽象类):扩充由 Abstraction 定义的接口,通常情况下它不再是抽象类而是具体类,它实现了在 Abstraction 中声明的抽象业务方法,在 RefinedAbstraction 中可以调用在 Implementor 中定义的业务方法。
- Implementor(实现类接口):定义实现类的接口,这个接口不一定要与 Abstraction 的接口完全一致,事 实上这两个接口可以完全不同,一般而言,Implementor 接口仅提供基本操作,而 Abstraction 定义的接口 可能会做更多更复杂的操作。Implementor 接口对这些基本操作进行了声明,而具体实现交给其子类。通过 关联关系,在 Abstraction 中不仅拥有自己的方法,还可以调用到 Implementor 中定义的方法,使用关联 关系来替代继承关系。
- ConcreteImplementor(具体实现类):具体实现 Implementor 接口,在不同的 ConcreteImplementor 中提供基本操作的不同实现,在程序运行时,ConcreteImplementor 对象将替换其父类对象,提供给抽象 类具体的业务操作方法。
桥接模式是一个非常有用的模式,在桥接模式中体现了很多面向对象设计原则的思想,包括“单一职责原则”、“开闭原则”、“合成复用原则”、“里氏代换原则”、“依赖倒转原则”等。熟悉桥接模式有助于我们深入理解这些设计原则,也有助于我们形成正确的设计思想和培养良好的设计风格。
在使用桥接模式时,我们首先应该识别出一个类所具有的两个独立变化的维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。通常情况下,我们将具有两个独立变化维度的类的一些普通业务方法和与之关系最密切的维度设计为“抽象类”层次结构(抽象部分),而将另一个维度设计为“实现类”层次结构(实现部分)。例如:对于毛笔而言,由于型号是其固有的维度,因此可以设计一个抽象的毛笔类,在该类中声明并部分实现毛笔的业务方法,而将各种型号的毛笔作为其子类;颜色是毛笔的另一个维度,由于它与毛笔之间存在一种“设置”的关系,因此我们可以提供一个抽象的颜色接口,而将具体的颜色作为实现该接口的子类。在此,型号可认为是毛笔的抽象部分,而颜色是毛笔的实现部分,结构示意图如图所示:
public class BridgingPattern { //像素矩阵类:辅助类,各种格式的文件最终都被转化为像素矩阵,不同的操作系统提供不同的方式显示像素矩阵 class Matrix { //此处代码省略 } //抽象图像类:抽象类 abstract class Image { protected ImageImp imp; public void setImageImp(ImageImp imp) { this.imp = imp; } public abstract void parseFile(String fileName); } //抽象操作系统实现类:实现类接口 interface ImageImp { public void doPaint(Matrix m); //显示像素矩阵 } //Windows操作系统实现类:具体实现类 class WindowsImp implements ImageImp { public void doPaint(Matrix m) { //调用Windows系统的绘制函数绘制像素矩阵 System.out.print("在Windows操作系统中显示图像:"); } } //Linux操作系统实现类:具体实现类 class LinuxImp implements ImageImp { public void doPaint(Matrix m) { //调用Linux系统的绘制函数绘制像素矩阵 System.out.print("在Linux操作系统中显示图像:"); } } //Unix操作系统实现类:具体实现类 class UnixImp implements ImageImp { public void doPaint(Matrix m) { //调用Unix系统的绘制函数绘制像素矩阵 System.out.print("在Unix操作系统中显示图像:"); } } //JPG格式图像:扩充抽象类 class JPGImage extends Image { public void parseFile(String fileName) { //模拟解析JPG文件并获得一个像素矩阵对象m; Matrix m = new Matrix(); imp.doPaint(m); System.out.println(fileName + ",格式为JPG。"); } } //PNG格式图像:扩充抽象类 class PNGImage extends Image { public void parseFile(String fileName) { //模拟解析PNG文件并获得一个像素矩阵对象m; Matrix m = new Matrix(); imp.doPaint(m); System.out.println(fileName + ",格式为PNG。"); } } } // 客户端调用 class Client { public static void main(String args[]) { Image image = (Image) XMLUtil.getBean("image"); ImageImp imp = (ImageImp) XMLUtil.getBean("os"); image.setImageImp(imp); image.parseFile("小龙女"); } }
适配器模式与桥接模式的联用
在某系统的报表处理模块中,需要将报表显示和数据采集分开,系统可以有多种报表显示方式也可以有多种数据采集方式,如可以从文本文件中读取数据,也可以从数据库中读取数据,还可以从 Excel 文件中获取数据。如果需要从 Excel 文件中获取数据,则需要调用与Excel 相关的 API,而这个 API 是现有系统所不具备的,该 API 由厂商提供。使用适配器模式和桥接模式设计该模块。
在设计过程中,由于存在报表显示和数据采集两个独立变化的维度,因此可以使用桥接模式进行初步设计;为了 使用 Excel 相关的 API 来进行数据采集则需要使用适配器模式。系统的完整设计中需要将两个模式联用,如图所示:
优点
(1)分离抽象接口及其实现部分。桥接模式使用“对象间的关联关系”解耦了抽象和实现之间固有的绑定关系,使 得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自维度的变化,也就是说抽象和实现不再在同 一个继承层次结构中,而是“子类化”它们,使它们各自都具有自己的子类,以便任何组合子类,从而获得多维 度组合对象。
(2)在很多情况下,桥接模式可以取代多层继承方案,多层继承方案违背了“单一职责原则”,复用性较差,且类 的个数非常多,桥接模式是比多层继承方案更好的解决方法,它极大减少了子类的个数。
(3)桥接模式提高了系统的可扩展性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合“开闭原则”。
缺点
(1)桥接模式的使用会增加系统的理解与设计难度,由于关联关系建立在抽象层,要求开发者一开始就针对抽象层进行设计与编程。
(2)桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累。
适用场景
(1)如果一个系统需要在抽象化和具体化之间增加更多的灵活性,避免在两个层次之间建立静态的继承关系,通过 桥接模式可以使它们在抽象层建立一个关联关系。
(2)“抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子 类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。
(3)一个类存在两个(或多个)独立变化的维度,且这两个(或多个)维度都需要独立进行扩展。
(4)对于那些不希望使用继承或因为多层继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
三、组合模式(树形结构的处理)
pass
四、装饰模式(扩展系统功能)
装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为,在现实生活中,这种情况也到处存在,例如一张照片,我们可以不改变照片本身,给它增加一个相框,使得它具有防潮的功能,而且用户可以根据需要给它增加不同类型的相框,甚至可以在一个小相框的外面再套一个大相框。
装饰模式是一种用于替代继承的技术,它通过一种无须定义子类的方式来给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系。在装饰模式中引入了装饰类,在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩充原有类的功能。
装饰模式定义如下:
装饰模式(Decorator Pattern):动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比 生成子类实现更为灵活。装饰模式是一种对象结构型模式。
在装饰模式中,为了让系统具有更好的灵活性和可扩展性,我们通常会定义一个抽象装饰类,而将具体的装饰类作为它的子类,装饰模式结构如图所示:
public class EncodingFilter implements Filter{ @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { //被增强的对象 HttpServletRequest req = (HttpServletRequest) request; //增强对象 EnhanceRequest enhanceRequest = new EnhanceRequest(req); chain.doFilter(enhanceRequest, response); } } class EnhanceRequest extends HttpServletRequestWrapper{ // 1>与要增强的类(HttpServletRequest类)继承/实现同一个类/接口 private HttpServletRequest request; public EnhanceRequest(HttpServletRequest request) { // 2>传入要增强的类 super(request); this.request = request; } //3>对要增强的方法(getParaameter)重写 @Override public String getParameter(String name) { String parameter = request.getParameter(name);//乱码 try { parameter = new String(parameter.getBytes("iso8859-1"),"UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return parameter; } }
注意事项
(1) 尽量保持装饰类的接口与被装饰类的接口相同,这样,对于客户端而言,无论是装饰之前的对象还是装饰之后 的对象都可以一致对待。这也就是说,在可能的情况下,我们应该尽量使用透明装饰模式。
(2) 尽量保持具体构件类 ConcreteComponent 是一个“轻”类,也就是说不要把太多的行为放在具体构件类中,我们可以通过装饰类对其进行扩展。
(3) 如果只有一个具体构件类,那么抽象装饰类可以作为该具体构件类的直接子类。
优点
(1) 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。(避免了继承->继承->继承)
(2) 可以通过一种动态的方式来扩展一个对象的功能,通过配置文件可以在运行时选择不同的具体装饰类,从而实现不同的行为。
(3) 可以对一个对象进行多次装饰,通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
(4) 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合“开闭原则”。
缺点
(1) 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。
(2) 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。
适用场景
(1) 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
(2) 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继 承的情况主要有两类:第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量 的子类,使得子类数目呈爆炸性增长;第二类是因为类已定义为不能被继承(如 Java 语言中的 final 类)。
五、外观模式(其实web调用service层使用的就是外观模式)
在软件开发中,有时候为了完成一项较为复杂的功能,一个客户类需要和多个业务类交互,而这些需要交互的业务类经常会作为一个整体出现,由于涉及到的类比较多,导致使用时代码较为复杂,此时,特别需要一个类似服务员一样的角色,由它来负责和多个业务类进行交互,而客户类只需与该类交互。外观模式通过引入一个新的外 观类(Facade)来实现该功能,外观类充当了软件系统中的“服务员”,它为多个业务类的调用提供了一个统一 的入口,简化了类与类之间的交互。在外观模式中,那些需要交互的业务类被称为子系统(Subsystem)。如果 没有外观类,那么每个客户类需要和多个子系统之间进行复杂的交互,系统的耦合度将很大,如图 2(A) 所示;而 引入外观类之后,客户类只需要直接与外观类交互,客户类与子系统之间原有的复杂引用关系由外观类来实 现,从而降低了系统的耦合度。
外观模式中,一个子系统的外部与其内部的通信通过一个统一的外观类进行,外观类将客户类与子系统的内部复杂性分隔开,使得客户类只需要与外观角色打交道,而不需要与子系统内部的很多对象打交道。
外观模式定义如下:
外观模式(Facade Pattern):为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
外观模式又称为门面模式,它是一种对象结构型模式。外观模式是迪米特法则的一种具体实现,通过引入一个新的外观角色可以降低原有系统的复杂度,同时降低客户类与子系统的耦合度。
优点
(1) 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。通过引 入外观模式,客户端代码将变得很简单,与之关联的对象也很少。
(2) 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外 观类即可。
(3) 一个子系统的修改对其他子系统没有任何影响,而且子系统内部变化也不会影响到外观对象。
缺点
(1) 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活 性。
(2) 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则。
适用场景
(1) 当要为访问一系列复杂的子系统提供一个简单入口时可以使用外观模式。
(2) 客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。
(3) 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
六、享元模式(Spring)
当一个软件系统在运行时产生的对象数量太多,将导致运行代价过高,带来系统性能下降等问题。例如在一个文 本字符串中存在很多重复的字符,如果每一个字符都用一个单独的对象来表示,将会占用较多的内存空间,那么 我们如何去避免系统中出现大量相同或相似的对象,同时又不影响客户端程序通过面向对象的方式对这些对象进 行操作?享元模式正为解决这一类问题而诞生。享元模式通过共享技术实现相同或相似对象的重用,在逻辑上每 一个出现的字符都有一个对象与之对应,然而在物理上它们却共享同一个享元对象,这个对象可以出现在一个字 符串的不同地方,相同的字符对象都指向同一个实例,在享元模式中,存储这些共享实例对象的地方称为享元池(Flyweight Pool)。我们可以针对每一个不同的字符创建一个享元对象,将其放在享元池中,需要时再从享 元池取出。
享元模式(Flyweight Pattern):用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。
享元模式结构较为复杂,一般结合工厂模式一起使用,在它的结构图中包含了一个享元工厂类,其结构图如图所示:
- Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些 方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状 态)。
- ConcreteFlyweight(具体享元类):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部 状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元 对象。
- UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,不 能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创 建。
- FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种 类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类 型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元 池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。
在享元模式中引入了享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,当用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。典型的享元工厂类的代码如下:
class FlyweightFactory { //定义一个HashMap用于存储享元对象,实现享元池 private HashMap flyweights = new HashMap(); public Flyweight getFlyweight(String key) { //如果对象存在,则直接从享元池获取 if (flyweights.containsKey(key)) { return (Flyweight) flyweights.get(key); } //如果对象不存在,先创建一个新的对象添加到享元池中,然后返回 else { Flyweight fw = newConcreteFlyweight(); flyweights.put(key, fw); return fw; } } }
享元类的设计是享元模式的要点之一,在享元类中要将内部状态和外部状态分开处理,通常将内部状态作为享元类的成员变量,而外部状态通过注入的方式添加到享元类中。典型的享元类代码如下所示:
class Flyweight { //内部状态intrinsicState作为成员变量,同一个享元对象其内部状态是一致的 private String intrinsicState; public Flyweight(String intrinsicState) { this.intrinsicState = intrinsicState; } //外部状态extrinsicState在使用时由外部设置,不保存在享元对象中,即使是同一个对象,在每一次调用时也可以传入不同的外部 public void operation(String extrinsicState) { // ...... } }
demo:
//围棋棋子类:抽象享元类 abstract class IgoChessman { public abstract String getColor(); public void display() { System.out.println("棋子颜色:" + this.getColor()); } } //黑色棋子类:具体享元类 class BlackIgoChessman extends IgoChessman { public String getColor() { return "黑色"; } } //白色棋子类:具体享元类 class WhiteIgoChessman extends IgoChessman { public String getColor() { return "白色"; } } //围棋棋子工厂类:享元工厂类,使用单例模式进行设计 class IgoChessmanFactory { private static IgoChessmanFactory instance = new IgoChessmanFactory(); private static Hashtable ht; //使用Hashtable来存储享元对象,充当享元池 private IgoChessmanFactory() { ht = new Hashtable(); IgoChessman black, white; black = new BlackIgoChessman(); ht.put("b", black); white = new WhiteIgoChessman(); ht.put("w", white); } //返回享元工厂类的唯一实例 public static IgoChessmanFactory getInstance() { return instance; } //通过key来获取存储在Hashtable中的享元对象 public static IgoChessman getIgoChessman(String color) { return (IgoChessman) ht.get(color); } } class Client { public static void main(String args[]) { IgoChessman black1, black2, black3, white1, white2; IgoChessmanFactory factory; //获取享元工厂对象 factory = IgoChessmanFactory.getInstance(); //通过享元工厂获取三颗黑子 black1 = factory.getIgoChessman("b"); black2 = factory.getIgoChessman("b"); black3 = factory.getIgoChessman("b"); System.out.println("判断两颗黑子是否相同:" + (black1 == black2)); //通过享元工厂获取两颗白子 white1 = factory.getIgoChessman("w"); white2 = factory.getIgoChessman("w"); System.out.println("判断两颗白子是否相同:" + (white1 == white2)); //显示棋子 black1.display(); black2.display(); black3.display(); white1.display(); white2.display(); } }
与其他模式的联用
(1)在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用简单工厂模式来生成享元对象。
(2)在一个系统中,通常只有唯一一个享元工厂,因此可以使用单例模式进行享元工厂类的设计。
(3)享元模式可以结合组合模式形成复合享元模式(为多个内部状态不同的 享元对象设置相同的外部状态),统一对多个享元对象设置外部状态。
优点
(1) 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。
(2) 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
缺点
(1) 享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
(2) 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。
适用场景
(1) 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
(2) 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
(3) 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
七、代理模式
http://www.runoob.com/design-pattern/proxy-pattern.html