设计模式-享元模式
概述
当一个软件系统在执行时产生的对象数量太多。将导致执行代价过高。带来系统性能下降等问题。比如在一个文本字符串中存在非常多反复的字符,假设每个字符都用一个单独的对象来表示,将会占用较多的内存空间,那么我们怎样去避免系统中出现大量同样或类似的对象。同一时候又不影响client程序通过面向对象的方式对这些对象进行操作?享元模式正为解决这一类问题而诞生。享元模式通过共享技术实现同样或类似对象的重用,在逻辑上每个出现的字符都有一个对象与之相应,然而在物理上它们却共享同一个享元对象,这个对象能够出如今一个字符串的不同地方,同样的字符对象都指向同一个实例,在享元模式中,存储这些共享实例对象的地方称为享元池(Flyweight Pool)。我们能够针对每个不同的字符创建一个享元对象。将其放在享元池中,须要时再从享元池取出。如图14-2所看到的:
内部状态&外部状态
享元模式以共享的方式高效地支持大量细粒度对象的重用,享元对象能做到共享的关键是区分了内部状态(Intrinsic State)和外部状态(Extrinsic State)。以下将对享元的内部状态和外部状态进行简单的介绍:
内部状态
内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,内部状态能够共享。
如字符的内容。不会随外部环境的变化而变化,不管在不论什么环境下字符“a”始终是“a”,都不会变成“b”。
外部状态
外部状态是随环境改变而改变的、不能够共享的状态。
享元对象的外部状态通常由client保存。并在享元对象被创建之后。须要使用的时候再传入到享元对象内部。一个外部状态与还有一个外部状态之间是相互独立的。如字符的颜色。能够在不同的地方有不同的颜色,比如有的“a”是红色的,有的“a”是绿色的,字符的大小也是如此,有的“a”是五号字,有的“a”是四号字。并且字符的颜色和大小是两个独立的外部状态。它们能够独立变化。相互之间没有影响。client能够在使用时将外部状态注入享元对象中。
正因为区分了内部状态和外部状态,我们能够将具有同样内部状态的对象存储在享元池中,享元池中的对象是能够实现共享的。须要的时候就将对象从享元池中取出,实现对象的复用。通过向取出的对象注入不同的外部状态。能够得到一系列类似的对象,而这些对象在内存中实际上仅仅存储一份。
定义
享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统仅仅使用少量的对象,而这些对象都非常类似。状态变化非常小。能够实现对象的多次复用。
因为享元模式要求能够共享的对象必须是细粒度对象。因此它又称为轻量级模式,它是一种对象结构型模式。
享元模式结构
享元模式结构较为复杂,一般结合工厂模式一起使用,在它的结构图中包括了一个享元工厂类。其结构图如图14-3所看到的:
在享元模式结构图中包括例如以下几个角色:
● Flyweight(抽象享元类):一般是一个接口或抽象类。在抽象享元类中声明了详细享元类公共的方法,这些方法能够向外界提供享元对象的内部数据(内部状态),同一时候也能够通过这些方法来设置外部数据(外部状态)。
● ConcreteFlyweight(详细享元类):它实现了抽象享元类,事实上例称为享元对象;在详细享元类中为内部状态提供了存储空间。通常我们能够结合单例模式来设计详细享元类。为每个详细享元类提供唯一的享元对象。
● UnsharedConcreteFlyweight(非共享详细享元类):并非全部的抽象享元类的子类都须要被共享。不能被共享的子类可设计为非共享详细享元类;当须要一个非共享详细享元类的对象时能够直接通过实例化创建。
● FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的详细享元对象存储在一个享元池中。享元池一般设计为一个存储“键值对”的集合(也能够是其它类型的集合),能够结合工厂模式进行设计;当用户请求一个详细享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(假设不存在的话)。返回新创建的实例并将其存储在享元池中。
典型代码
在享元模式中引入了享元工厂类。享元工厂类的作用在于提供一个用于存储享元对象的享元池。当用户须要对象时,首先从享元池中获取。假设享元池中不存在。则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。典型的享元工厂类的代码例如以下:
class FlyweightFactory {
//定义一个HashMap用于存储享元对象。实现享元池
private HashMap flyweights = newHashMap();
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) {
......
}
}
实例应用
软件公司欲开发一个多功能文档编辑器,在文本文档中能够插入图片、动画、视频等多媒体资料,为了节约系统资源,同样的图片、动画和视频在同一个文档中仅仅需保存一份,可是能够多次反复出现,并且它们每次出现时位置和大小均可不同。试使用享元模式设计该文档编辑器。
/**
* (外部状态)文件属性
*/
public class FileStatus {
// 坐标
private int x;
private int y;
// 大小
private int width;
private int height;
public FileStatus(int x, int y, int width, int height) {
super();
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
}
/**
* 文件类:抽象享元类
*/
public abstract class File {
public abstract String getTyep();
public void display(FileStatus status) {
System.out.println("文件类型:" + this.getTyep());
System.out.println("文件大小:(" + status.getX() + "," + status.getY() + ")");
System.out.println("文件坐标:(" + status.getWidth() + "," + status.getHeight() + ")");
}
}
/**
* 图片文件:详细享元类
*/
public class ImageFile extends File {
@Override
public String getTyep() {
return "图片";
}
}
/**
* 动画文件:详细享元类
*/
public class AnimationFile extends File {
@Override
public String getTyep() {
return "动画";
}
}
/**
* 视频文件:详细享元类
*/
public class VideoFile extends File {
@Override
public String getTyep() {
return "视频";
}
}
/**
* 文件工厂:享元工厂类,适用单例模式进行设计
*/
public class FileFactory {
private Map<String, File> map;
static class InstanceHolder {
private static FileFactory instance = new FileFactory();
}
private FileFactory() {
map = new HashMap<String, File>();
File imageFile = new ImageFile();
File animationFile = new AnimationFile();
File videoFile = new VideoFile();
map.put("image", imageFile);
map.put("anim", animationFile);
map.put("video", videoFile);
}
public static FileFactory getInstance() {
return InstanceHolder.instance;
}
public File getFile(String type) {
return map.get(type);
}
}
client代码:
public class Main {
public static void main(String[] args) {
File file1, file2, file3, file4, file5, file6;
FileFactory factory;
// 获取享元工厂对象
factory = FileFactory.getInstance();
// 通过享元工厂获取图片文件对象
file1 = factory.getFile("image");
file2 = factory.getFile("image");
System.out.println("推断两个图片文件是否同样:" + (file1 == file2));
// 通过享元工厂获取视频文件对象
file3 = factory.getFile("video");
file4 = factory.getFile("video");
System.out.println("推断两个视频文件是否同样:" + (file3 == file4));
// 通过享元工厂获取动画文件对象
file5 = factory.getFile("anim");
file6 = factory.getFile("anim");
System.out.println("推断两个动画文件是否同样:" + (file5 == file6));
// 显示文件
file1.display(new FileStatus(1,1,10,10));
file2.display(new FileStatus(2,2,20,20));
file3.display(new FileStatus(3,3,30,30));
file4.display(new FileStatus(4,4,40,40));
file5.display(new FileStatus(5,5,50,50));
file6.display(new FileStatus(6,6,60,60));
}
}
结果:
推断两个图片文件是否同样:true
推断两个视频文件是否同样:true
推断两个动画文件是否同样:true
文件类型:图片
文件大小:(1,1)
文件坐标:(10,10)
文件类型:图片
文件大小:(2,2)
文件坐标:(20,20)
文件类型:视频
文件大小:(3,3)
文件坐标:(30,30)
文件类型:视频
文件大小:(4,4)
文件坐标:(40,40)
文件类型:动画
文件大小:(5,5)
文件坐标:(50,50)
文件类型:动画
文件大小:(6,6)
文件坐标:(60,60)
单纯享元模式与复合享元模式
标准的享元模式结构图中既包括能够共享的详细享元类。也包括不能够共享的非共享详细享元类。
可是在实际使用过程中,我们有时候会用到两种特殊的享元模式:单纯享元模式和复合享元模式。以下将对这两种特殊的享元模式进行简单的介绍:
1.单纯享元模式
在单纯享元模式中,全部的详细享元类都是能够共享的。不存在非共享详细享元类。
单纯享元模式的结构如图14-6所看到的:
2.复合享元模式
将一些单纯享元对象使用组合模式加以组合,还能够形成复合享元对象,这种复合享元对象本身不能共享。可是它们能够分解成单纯享元对象。而后者则能够共享。复合享元模式的结构如图14-7所看到的:
通过复合享元模式。能够确保复合享元类CompositeConcreteFlyweight中所包括的每个单纯享元类ConcreteFlyweight都具有同样的外部状态,而这些单纯享元的内部状态往往能够不同。假设希望为多个内部状态不同的享元对象设置同样的外部状态,能够考虑使用复合享元模式。
关于享元模式的补充
1.与其它模式的联用
享元模式通常须要和其它模式一起联用。几种常见的联用方式例如以下:
(1)在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用简单工厂模式来生成享元对象。
(2)在一个系统中。通常仅仅有唯一一个享元工厂,因此能够使用单例模式进行享元工厂类的设计。
(3)享元模式能够结合组合模式形成复合享元模式,统一对多个享元对象设置外部状态。
2.享元模式与String类
JDK类库中的String类使用了享元模式,我们通过例如以下代码来加以说明:
class Demo {
public static void main(String args[]) {
String str1 = "abcd";
String str2 = "abcd";
String str3 = "ab" + "cd";
String str4 = "ab";
str4 += "cd";
System.out.println(str1 == str2);
System.out.println(str1 == str3);
System.out.println(str1 == str4);
str2 += "e";
System.out.println(str1 == str2);
}
}
在Java语言中,假设每次执行类似String str1=”abcd”的操作时都创建一个新的字符串对象将导致内存开销非常大,因此假设第一次创建了内容为”abcd”的字符串对象str1。下一次再创建内容同样的字符串对象str2时会将它的引用指向”abcd”。不会又一次分配内存空间,从而实现了”abcd”在内存中的共享。上述代码输出结果例如以下:
true
true
false
false
能够看出,前两个输出语句均为true,说明str1、str2、str3在内存中引用了同样的对象;假设有一个字符串str4,其初值为”ab”。再对它进行操作str4 += “cd”,此时尽管str4的内容与str1同样,可是因为str4的初始值不同,在创建str4时又一次分配了内存,所以第三个输出语句结果为false;最后一个输出语句结果也为false。说明当对str2进行改动时将创建一个新的对象,改动工作在新对象上完毕。而原来引用的对象并没有发生不论什么改变。str1仍然引用原有对象。而str2引用新对象,str1与str2引用了两个全然不同的对象。
总结
当系统中存在大量同样或者类似的对象时,享元模式是一种较好的解决方式,它通过共享技术实现同样或类似的细粒度对象的复用。从而节约了内存空间,提高了系统性能。相比其它结构型设计模式,享元模式的使用频率并不算太高,可是作为一种以“节约内存,提高性能”为出发点的设计模式,它在软件开发中还是得到了一定程度的应用。
享元模式的长处
享元模式的主要长处例如以下:
长处1
能够极大降低内存中对象的数量。使得同样或类似对象在内存中仅仅保存一份,从而能够节约系统资源。提高系统性能。
长处2
享元模式的外部状态相对独立,并且不会影响其内部状态。从而使得享元对象能够在不同的环境中被共享。
享元模式的缺点
享元模式的主要缺点例如以下:
缺点1
享元模式使得系统变得复杂,须要分离出内部状态和外部状态。这使得程序的逻辑复杂化。
缺点2
为了使对象能够共享,享元模式须要将享元对象的部分状态外部化。而读取外部状态将使得执行时间变长。
适用场景
在以下情况下能够考虑使用享元模式:
场景1
一个系统有大量同样或者类似的对象。造成内存的大量耗费。
场景2
对象的大部分状态都能够外部化,能够将这些外部状态传入对象中。
场景3
在使用享元模式时须要维护一个存储享元对象的享元池。而这须要耗费一定的系统资源,因此,应当在须要多次反复使用享元对象时才值得使用享元模式。