当系统中存在大量相同或相似的对象时,享元模式是一种较好的解决方案,它通过共享技术实现相同或相似的细粒度对象的复用,从而节约内存空间。享元模式提供了一个享元池用于存储已经创建好的享元对象,并通过享元工厂类将享元对象提供给客户端使用。
模式动机
使用面向对象技术开发时,很多情况下需要在系统中增加类和对象的个数,并且这些对象有些是相同或相似的。当对象太多时,将导致运行代价过高,性能下降等问题。为了避免系统中出现大量相同或相似的对象,享元模式通过共享技术实现相同或相似对象的重用,相同的对象都指向一个实例,存储这个实例的对象称为享元池。
模式设计
系统中有些对象并不完全相同,而只是相似,因此需要先找出这些对象的共同点,在享元类中封装这些共同的内容。不同的内容可以通过外部应用程序来设置,而不进行共享,在享元模式中可以共享的相同内容称为内部状态,而那些需要外部环境设置的不能共享的内容称为外部状态。
在享元模式中通常会出现工厂模式,需要创建一个享元工厂来维护一个享元池,用于存储具有相同内部状态的享元对象。实际使用中,能够共享的内部状态是有限的,因此享元对象一般都设计为较小的对象,它所包含的内部状态较少,这种状态一般称为细粒度对象。享元模式的目的就是使用共享技术来实现大量细粒度对象的复用。
模式定义
运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此又称为轻量级模式。
模式结构
-
Flyweight(抽象享元类)
抽象享元类声明是一个接口,通过它可以接受并作用于外部状态。在抽象享元类定义了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)
-
ConcreteFlyweight(具体享元类)
具体享元类实现了抽象享元接口,保存了内部状态,具体享元对象是可以共享的。可以结合单例模式来设计享元具体类,为每一个具体享元类提供唯一的享元对象。
-
UnsharedConcreteFlyweight(非共享具体享元类)
不能被共享的抽象享元类的子类被设计为非共享具体享元类。当需要一个非共享具体享元类的对象时可以直接通过实例化创建。在某些享元模式的层次结构中,非共享具体享元对象还可以将具体享元对象作为子节点。
-
FlyweightFactory(享元工厂类)
享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中。当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池已创建的实例或者创建一个实例,返回该新创建的实例并将其存储在享元池中。
模式分析
享元模式的核心在于享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池。典型的享元工厂类代码如下:
public class FlyweightFactory {
private HashMap flyweights = new HashMap();
public Flyweight getFlyweight(String key) {
if (flyweights.containsKey(key)) {
return (Flyweight) flyweights.get(key);
} else {
Flyweight fw = new ConcreteFlyweight();
flyweights.put(key, fw);
return fw;
}
}
}
享元对象能做到共享的关键是区分内部状态(internal state)和外部状态(external state)。下面简单对享元的内部状态和外部状态进行分析:
- 内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,因此内部状态可以共享。
- 外部状态是随环境改变而改变的、不可共享的状态。享元对象的外部状态必须由客户端保存,并在享元对象被创建之后,在需要使用时再传入到享元对象内部。
典型的享元类代码如下:
public class Flyweight {
private String intrinsicState;
public Flyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
public void operation(String extrinsicState) {
...
}
}
实例之共享网络设备
很多网络设备都是支持共享的,如交换机、集线器等,多台计算机终端可以链接同一台网络设备,并通过该网络设备进行数据转换。但是分配给每一个终端计算机的端口是不同的,可以将端口从网络设备中抽取出来作为外部状态,需要时再设置。
-
抽象享元类 NetworkDevices
public interface NetworkDevice { public String getType(); public void use(Port port); // 用于设置外部状态 }
-
具体享元类 Switch
public class Switch implements NetworkDevice { private String type; public Switch(String type) { this.type = type; } public String getType() { return type; } public void use(Port port) { System.out.println("Linked by switch, type is" + this.type + ",port is" + port.getPort()); } }
-
具体享元类 Hub
public class Hub implements NetworkDevice { private String type; public Hub(String type) { this.type = type; } public String getType() { return type; } public void use(Port port) { System.out.println("Linked by Hub, type is" + this.type + ",port is" + port.getPort()); } }
-
端口类 Port
public class Port { String port; public Port(String port) { this.port = port; } public String getPort() { return port; } public void setPort(String port) { this.port = port; } }
-
享元工厂 DeviceFactory
public class DeviceFactory { private List<NetworkDevice> devices = new ArrayList<NetworkDevice>(); private int totalTerminal = 0; public DeviceFactory() { NetworkDevice nd1 = new Switch("Cisco-WS-C2950-24"); devices.add(nd1); NetworkDevice nd2 = new Hub("TP-LINK-HF8M"); devices.add(nd2); } public NetworkDevice getNetworkDevice(String type) { if (type.equalsIgnoreCase("cisco")) { totalTerminal++; return devices.get(0); } else if (type.equalsIgnoreCase("tp")) { totalTerminal++; return devices.get(1); } else { return null; } } public int getTotalDevice() { return devices.size(); } public int getTotalTerminal() { return totalTerminal; } }
-
客户测试类 Client
public class Client { public static void main(String[] args) { DeviceFactory df = new DeviceFactory(); NetworkDevice nd1 = df.getNetworkDevice("cisco"); nd1.use(new Port("1000")); NetworkDevice nd2 = df.getNetworkDevice("cisco"); nd2.use(new Port("1001")); NetworkDevice nd3 = df.getNetworkDevice("cisco"); nd3.use(new Port("1002")); NetworkDevice nd4 = df.getNetworkDevice("tp"); nd4.use(new Port("1003")); NetworkDevice nd5 = df.getNetworkDevice("tp"); nd5.use(new Port("1004")); System.out.println("Total Device: " + df.getTotalDevice()); System.out.println("Total Terminal: " + df.getTotalTerminal()); } }
在客户端代码中,在调用享元对象的 use() 方法时,传入了一个 Port 类型对象,在该对象中封装了端口号,作为共享网络设备的外部状态,同一个网络设备具有多个不同的端口号。
从运行结果可以得知,在调用享元对象的 use() 方法时,由于设置了不同的端口号,因此相同的享元对象虽然具有相同的内部状态 type,但是它们的外部状态 port 不同。
模式优缺点
优点如下:
- 极大减少内存中对象的数量
- 享元模式的外部状态相对独立,不会影响其内部状态,因此享元对象可以在不同的环境中被共享。
缺点如下:
- 享元模式是系统更加复杂,需要分离出内部状态和外部状态。
- 读取外部状态会使运行时间变长。
模式适用环境
以下情况可以使用享元模式:
- 一个系统有大量相同或相似对象,这类对象的大量使用造成内存的大量耗费
- 对象的大部分状态都可外部化,可以将这些外部状态传入对象中。
- 维护享元池需要耗费资源,因此应当在多次重复使用享元对象时才值得使用享元模式
单纯享元模式
即所有抽象享元类的子类都可以共享,不存在非共享具体享元类
复合享元模式
将单纯享元模式与组合模式加以组合,可以形成复合享元对象。这样的复合享元对象本身不能共享,但它们可以分解成单纯享元对象,而后者可以共享。通过复合享元模式,可以确保复合享元类 CompositeConcreteFlyweight 中所包含的每个单纯享元类 ConcreteFlyweight 都具有相同的外部状态,而这些单纯享元的内部状态往往不同。