享元(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()方法可以在类的哈希表中根据名称查找化学药品。示例代码使用了小写的化学器名来存储与查询化学品。

results matching ""

    No results matching ""