定义:运用共享技术有效地支持大量细粒度的的对象。
类型:对象结构型模式
类图:
FlyWeight模式的结构
- 抽象享元角色(Flyweight):描述一个接口,通过这个接口可以Flyweight可以接受并作用于外部状态。
- 具体享元(ConcreteFlyweight)角色:实现Flyweight接口,并为内部状态(如果有的话)增加存储空间。ConcreteFlyweight对象必须是可共享的。它所存储的状态必须是内部的;即,它必须独立于ConcreteFlyweight对象的场景。
- 复合享元(UnsharableFlyweight)角色:复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合享元角色又称做不可共享的享元对象。这个角色一般很少使用。
- 享元工厂(FlyweightFactoiy)角色:本角色负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象请求一个享元对象的时候,享元工厂角色需要检查系统中是否已经有一个符合要求的享元对象,如果已经有了,享元工厂角色就应当提供这个已有的享元对象;如果系统中没有一个适当的享元对象的话,享元工厂角色就应当创建一个新的合适的享元对象。
- 客户端(Client)角色:维护一个对Flyweight的引用,计算或存储一个(多个)Flyweight的外部状态。
享元对象的所有状态分成两类,内部状态和外部状态。
- 内部状态(Internal State)。它不会随环境改变而改变,存储在享元对象内部,因此内蕴状态是可以共享的,对于任何一个享元对象来讲,它的值是完全相同的。我们例子中Character类的symbol属性,它代表的状态就是内蕴状态。
- 外部状态(External State)。它会随环境的改变而改变,因此是不可以共享的状态,对于不同的享元对象来讲,它的值可能是不同的。享元对象的外蕴状态必须由客户端保存,在享元对象被创建之后,需要使用的时候再传入到享元对象内部。
Flyweight执行时所需要的状态必定是内部的或者外部的状态,内部状态存储于ConcreteFlyweight对象中,而外部对象则由Client对象存储或计算,当用户调用Flyweight对象的操作时,将该状态传递给它。
现在通过一个面向对象的文本编辑器设计来说明享元模式的应用。假设我们要设计一个文本编辑器,而且它必须创建字符对象来表示文档中的每个字符,现在让我们考虑字符对象保持什么信息呢?如:字体、字体大小和位置等等信息。
一个文档通常包含许多字符对象,它们需要大容量的内存。值得我们注意的是一般字符都是由数字、字母和其他字符组成的(它们是固定的,可知的),这些字符对象可以共享字体和字体大小等信息,现在它们专有属性只剩下位置了,每个字符对象只需保持它们在文档中的位置就OK了,通过分析我们已经降低了编辑器的内存需求。
首先我们定义了一个字符享元类,其中符号、字体和字体大小都是内部状态,而位置则是外部状态。
// 抽象类 FlyWeight
abstract class ICharacter { //字符享元
protected char symbol;
protected int size;
protected String font;
public abstract void display();
}
接着我们定义具体字符A的享元类,并且对内部状态符号、字体和字体大小进行初始化,而且其他字符B到Z享元类都类似。
// ConcreteFlyweight
class CharacterA extends ICharacter { //具体字符 A
public CharacterA() {
symbol = 'A';
size = 10;
font = "宋体";
}
@Override
public void display() {
System.out.println("symbol:" + symbol + " size:" + size + " font:"
+ font);
}
}
然后,我们定义一个字符工厂类,通过一个Map<java.lang.Character, Character>来保存字符对象,使用字符值来查找字符对象是否已经创建了,如果查找的字符对象已经存在,那么直接返回该对象,反之就创建字符对象实例。
// FlyweightFactory class
class CharacterFactory {
private static Map<Character, ICharacter> chs = new HashMap<Character, ICharacter>();
private static CharacterFactory instance = new CharacterFactory();
// 单例模式
private CharacterFactory() {
};
/**
* Checked the character whether existed or not,if the character existed, then directly returns,
otherwise, instantiates a character object.
* @param key
* @return
*/
public ICharacter getCharacter(Character key) {
ICharacter character = null;
if (chs.containsKey(key)) {
character = chs.get(key);
} else {
switch (key.charValue()) {
case 'A':
character = new CharacterA();
break;
case 'B':
character = new CharacterB();
break;
case 'C':
character = new CharacterC();
break;
default:
break;
}
chs.put(key, character);
}
return character;
}
public static CharacterFactory getInstance() {
return instance;
}
}
Client代码
//Client
public class FlyWeightClient {
public static void main(String[] args) {
String text = "ABCABBCC";
char[] chs = text.toCharArray();
CharacterFactory factory = CharacterFactory.getInstance();
for (int i = 0; chs != null && i < chs.length; i++) {
char ch = chs[i];
Character key = Character.valueOf(ch);
ICharacter character = factory.getCharacter(key);
character.display();
}
}
}
适用场景
- 一个应用程序使用了大量的对象。
- 完全由于使用大量的对象,造成很大的存储开销。
- 对象的大多数状态都可变为外部状态。
- 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
- 应用程序不依赖对象标识。
FlyWeight需要注意的问题
享元模式采用对象共享的做法来降低系统中对象的个数,从而降低细粒度对象给系统带来的内存压力。在具体实现方面,我们要注意对象状态的处理,一定要正确地区分对象的内部状态和外部状态,这是实现享元模式的关键所在。
享元模式的优点在于它大幅度地降低内存中对象的数量。为了做到这一点,享元模式也付出了一定的代价:
- 享元模式为了使对象可以共享,它需要将部分状态外部化,这使得系统的逻辑变得复杂。
- 享元模式将享元对象的部分状态外部化,而读取外部状态使得运行时间会有所加长。