享元(Flyweight)模式
享元模式在客户对象间提供共享对象,并且为共享对象创建职责,以便普通对象不需要考虑共享对象创建的问题。
通常情况下,任何时候都只能有一个客户对象引用该共享对象。当某个客户对象改变该共享对象的状态时,该共享对象不需要通知其他客户对象,然而有时候可能需要让多个客户对象同时共享访问某个对象。
存在这么一种需要在多个客户对象间共享对象的场景:当你需要管理成千上万个小对象时,例如在线电子书中的字符。在这种场景下,出于性能的考虑,需要在不同的客户对象间安全地传输这些细粒度的对象。例如:某本书需要A对象,当有很多A对象时,就需要采用某种方法对其建模。
注: 享元模式的意图是通过共享来有效地支持大量细粒度的对象。
不变性
享元模式让多个客户对象间共享访问限定数量的对象。
为了实现这个目的,你必须要考虑到当某个对象改变了该共享对象的状态时,该状态变化会影响到每个访问它的对象。
当多个客户对象要共享访问某个对象时,若要保证对象间不会相互影响,一种最简单而又常用的做法是,限制客户对象调用任何可能改变共享对象的方法。你可以通过将对象设置为不变(immutable)类型来达到这 一目的。然而,这样做会导致对象在创建后无法改变。Java中最常见的不可变对象是String类。当创建一个String对象后,无论你还是其他客户对象都无法改变该对象的字符。
抽取享元中不可变的部分
对于Oozinoz公司,化学药品的使用程序就像文档中的字符一样普遍。工厂中的采购部门,工程部门,制造部门,安全部门都会关心成千上万的化学药品的流动情况。成批的化学药品都被建模到Substance类的实例中,如图13.1所示。
Substance类有很多方法可以访问它的属性,还有getMoles()方法用来返回摩尔数--即化学成分的分子数量。一个Substance对象代表特定的摩尔数。Oozinoz公司用Mixture类来建化学药品的组成成分。例如,下图,展示了黑火药的对象图。
假设Oozinoz公司的化学药品越来越多,因而决定使用享元模式来减少程序中Substance对象的数量。为了将Substance对象变成享元模式,首先需要须知的事情就是将类中不变的部分与变化的部分分离出来。假设你要重构Substance类,可以将不变的部分抽取到Chemical类中。
抽出对象中不变的部分仅仅只完成了享元模式的一半。另一半包括创建享元工厂,实例化享元,以及让客户对象共享享元对象。我们还需要确保客户对象应使用享元工厂创建享元对象,而不是自己创建。
为了创建化学药品的享元对象,需要定义一个chemicalFactory类作为享元工厂。该类包含一个静态方法,负责接收一个名称,返回对应的化学药品。你可以将化学药品存在一个哈希表中,在初始化工厂的时候创建它们。下图展示了ChemicalFactory类的设计。
ChemicalFactory类的代码使用静态构造函数将Chemical对象存储在哈希表中。
创建完化学药品工厂后,需要执行一些步骤确保让开发人员使用工厂创建享元对象,而不是直接创建Chemical对象。一种简单的方式是借助Chemical类的可见性
一种方法是将Chemical的构造函数定义为私有的。这就可以防止ChemicalFactory类实例化Chemical类对象。
若要防止开发人员实例化Chemical类自身,可以将Chemical和ChemicalFactory类放在同一个包中,然后将Chemical类的构造函数设定为默认的访问限制(即包级别的访问限制)。
访问修饰符还不能完全控制享元对象的实例化,需要确保ChemicalFactory是唯一一个能创建Chemical实例的类。为了达到这一级别的控制,需要把Chemical类定义为ChemicalFactory的内部类,并将Chemical定义为接口,内部的嵌套类定义为该接口的实现,从而简化对内嵌类型的使用。
public interface Chemical{
String getName();
String getSymbol();
double getAtomicWeight();
}
客户对象永远不会引用该内部类,因此,可以把它定义成私有类型,以确保只有ChemicalFactory类才能访问它。
public class ChemicalFactory {
private static Map<String, Chemical> chemicals = new HashMap();
static {
chemicals.put("carbon", new ChemicalImpl("Carbon", "C", 12));
chemicals.put("sulfur", new ChemicalImpl("Sulfur", "S", 32));
chemicals.put("saltpeter", new ChemicalImpl("Saltpeter", "KN03", 101));
}
public static Chemical getChemical(String name) {
return chemicals.get(name.toLowerCase());
}
private static class ChemicalImpl implements Chemical{
private String name;
private String symbol;
private double atomicWeight;
ChemicalImpl(String name, String symbol, double atomicWeight) {
this.name = name;
this.symbol = symbol;
this.atomicWeight = atomicWeight;
}
public String getName() {
return name;
}
public String getSymbol() {
return symbol;
}
public double getAtomicWeight() {
return atomicWeight;
}
@Override
public String toString() {
return "ChemicalImpl{" +
"name='" + name + '\'' +
", symbol='" + symbol + '\'' +
", atomicWeight=" + atomicWeight +
'}';
}
}
}
这段代码解决了如下三个问题。
- ChemicalImpl嵌套类应该是私有的,这样就只有工厂类才能使用该类,嵌套类的访问范围必须是包级别或公有的,这样包含的类才可以实例化嵌套类。即使将构造函数定义为公有的,如果嵌套类本身被标记为私有,就没有其他类可以调用该构造函数。
- 工厂类的构造函数使用了一个静态的初始化器,以确保类只会构建一次化学药品列表
- getChemical()方法可以在类的哈希表中根据名称查找化学药品。示例代码使用了小写的化学器名来存储与查询化学品。