软件设计模式学习(十五)享元模式

当系统中存在大量相同或相似的对象时,享元模式是一种较好的解决方案,它通过共享技术实现相同或相似的细粒度对象的复用,从而节约内存空间。享元模式提供了一个享元池用于存储已经创建好的享元对象,并通过享元工厂类将享元对象提供给客户端使用。

模式动机

使用面向对象技术开发时,很多情况下需要在系统中增加类和对象的个数,并且这些对象有些是相同或相似的。当对象太多时,将导致运行代价过高,性能下降等问题。为了避免系统中出现大量相同或相似的对象,享元模式通过共享技术实现相同或相似对象的重用,相同的对象都指向一个实例,存储这个实例的对象称为享元池。

模式设计

系统中有些对象并不完全相同,而只是相似,因此需要先找出这些对象的共同点,在享元类中封装这些共同的内容。不同的内容可以通过外部应用程序来设置,而不进行共享,在享元模式中可以共享的相同内容称为内部状态,而那些需要外部环境设置的不能共享的内容称为外部状态。

在享元模式中通常会出现工厂模式,需要创建一个享元工厂来维护一个享元池,用于存储具有相同内部状态的享元对象。实际使用中,能够共享的内部状态是有限的,因此享元对象一般都设计为较小的对象,它所包含的内部状态较少,这种状态一般称为细粒度对象。享元模式的目的就是使用共享技术来实现大量细粒度对象的复用。

模式定义

运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此又称为轻量级模式。

模式结构

  1. Flyweight(抽象享元类)

    抽象享元类声明是一个接口,通过它可以接受并作用于外部状态。在抽象享元类定义了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)

  2. ConcreteFlyweight(具体享元类)

    具体享元类实现了抽象享元接口,保存了内部状态,具体享元对象是可以共享的。可以结合单例模式来设计享元具体类,为每一个具体享元类提供唯一的享元对象。

  3. UnsharedConcreteFlyweight(非共享具体享元类)

    不能被共享的抽象享元类的子类被设计为非共享具体享元类。当需要一个非共享具体享元类的对象时可以直接通过实例化创建。在某些享元模式的层次结构中,非共享具体享元对象还可以将具体享元对象作为子节点。

  4. 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)。下面简单对享元的内部状态和外部状态进行分析:

  1. 内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,因此内部状态可以共享。
  2. 外部状态是随环境改变而改变的、不可共享的状态。享元对象的外部状态必须由客户端保存,并在享元对象被创建之后,在需要使用时再传入到享元对象内部。

典型的享元类代码如下:

public class Flyweight {

    private String intrinsicState;

    public Flyweight(String intrinsicState) {
        this.intrinsicState = intrinsicState;
    }

    public void operation(String extrinsicState) {
        ...
    }
}

实例之共享网络设备

很多网络设备都是支持共享的,如交换机、集线器等,多台计算机终端可以链接同一台网络设备,并通过该网络设备进行数据转换。但是分配给每一个终端计算机的端口是不同的,可以将端口从网络设备中抽取出来作为外部状态,需要时再设置。

  1. 抽象享元类 NetworkDevices

    public interface NetworkDevice {
    
        public String getType();
        public void use(Port port);	// 用于设置外部状态
    }
    
  2. 具体享元类 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());
        }
    }
    
  3. 具体享元类 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());
        }
    }
    
  4. 端口类 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;
        }
    }
    
  5. 享元工厂 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;
        }
    }
    
  6. 客户测试类 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 不同。

模式优缺点

优点如下:

  1. 极大减少内存中对象的数量
  2. 享元模式的外部状态相对独立,不会影响其内部状态,因此享元对象可以在不同的环境中被共享。

缺点如下:

  1. 享元模式是系统更加复杂,需要分离出内部状态和外部状态。
  2. 读取外部状态会使运行时间变长。

模式适用环境

以下情况可以使用享元模式:

  1. 一个系统有大量相同或相似对象,这类对象的大量使用造成内存的大量耗费
  2. 对象的大部分状态都可外部化,可以将这些外部状态传入对象中。
  3. 维护享元池需要耗费资源,因此应当在多次重复使用享元对象时才值得使用享元模式

单纯享元模式

即所有抽象享元类的子类都可以共享,不存在非共享具体享元类

复合享元模式

将单纯享元模式与组合模式加以组合,可以形成复合享元对象。这样的复合享元对象本身不能共享,但它们可以分解成单纯享元对象,而后者可以共享。通过复合享元模式,可以确保复合享元类 CompositeConcreteFlyweight 中所包含的每个单纯享元类 ConcreteFlyweight 都具有相同的外部状态,而这些单纯享元的内部状态往往不同。

© 版权声明
THE END
喜欢就支持一下吧!
点赞0
分享