Java Code DesignPartten

设计模式-享元模式

Posted on 2020-10-10,6 min read

定义

  • Use sharing to support large numbers of fine-grained objects efficiently.(使用共享对象可有效地支持大量的细粒度的对象。)

  • 享元模式提出了两个要求:细粒度的对象和共享对象。将对象的信息分为两个部分:内部状态(intrinsic)和外部状态(extrinsic)

    • 内部状态:对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变。它们可以作为一个对象爱的动态附加信息,不必直接存储在具体的某个对象中,属于可以共享的部分
    • 外部状态:外部状态是对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态。它是一批对象的统一标识,是唯一的一个索引值。
  • 组成:

    • Flyweight(抽象享元对象):简单的来说是一个产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现
    • ConcreteFlyweight(具体享元对象):具体的一个产品类。实现抽象角色定义的业务。该角色中需要注意的是内部状态处理应该与环境无关,不应该出现一个操作改变了内部状态,同时还修改了外部状态
    • unsharedConcreteFlyweight(不可共享的享元角色):不存在外部状态或者安全要求(如线程安全)不能够使用共享技术的对象,该对象一般不会出现在享元工厂中。
    • FlyweightFactory(享元工厂):职责非常简单,构造一个池容器,同时提供从池中获得对象的方法。
  • 通用类图

享元模式通用类图

优点

  • 大大减少应用程序创建的对象,降低程序内存占用,增强程序的性能。

缺点

  • 提高了系统复杂性,需要分离出外部状态和内部状态,而且外部状态具有固化特性,不应该随内部状态改变而改变,否则将导致系统的逻辑混乱。
  • 一般享元模式在多线程环境下使用的多,在多线程情况下要注意同步问题。

使用场景

  • 场景:
    • 系统中存在大量的相似对象
    • 细粒度的对象都具备接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
    • 需要缓冲池的场景。

代码

享元模式

类图

本项目使用类图

代码

抽象享元角色(抽象类

package com.pattern.flyweight;

public abstract class Flyweight {
    /**
     * 内部状态,即多个实例可能拥有的共享数据
     */
    private String intrinsic;
    /**
     * 外部状态,具体来说就是对象依赖的一个标识
     */
    protected final String extrinsic;

    /**
     * 享元角色必须接受外部状态
     * @param extrinsic
     */
    public Flyweight(String extrinsic) {
        this.extrinsic = extrinsic;
    }
    // 享元角色可能拥有的业务操作
    public abstract void operate();

    /**
     * 获取内部状态(共享变量)。注意多线程环境下的线程安全问题。
     * @return
     */
    public String getIntrinsic() {
        return intrinsic;
    }

    /**
     * 设置内部状态(共享变量)。注意多线程环境下的线程安全问题。
     * @param intrinsic
     */
    public void setIntrinsic(String intrinsic) {
        this.intrinsic = intrinsic;
    }
}

具体享元角色(继承自抽象享元角色

package com.pattern.flyweight;

public class ConcreteFlyweight extends Flyweight {
    /**
     * 享元角色必须接受外部状态
     *
     * @param extrinsic
     */
    public ConcreteFlyweight(String extrinsic) {
        super(extrinsic);
    }

    @Override
    public void operate() {
        System.out.println("具体享元角色自己有的业务");
    }
}

享元工厂(工厂类

package com.pattern.flyweight;

import java.util.HashMap;

/**
 * 享元工厂
 */
public class FlyweightFactory {
    private static HashMap<String, Flyweight> pool = new HashMap<>(16);
    public static Flyweight getFlyweight(String extrinsic){
        Flyweight flyweight = null;
        if(pool.containsKey(extrinsic)){
            // 如果在池中有对象直接返回
            flyweight = pool.get(extrinsic);
        } else {
            // 如果在池中没有对象就新创建
            flyweight = new ConcreteFlyweight(extrinsic);
            pool.put(extrinsic,flyweight);
        }
        return flyweight;
    }
}

测试代码

package com.pattern;

import com.pattern.flyweight.Flyweight;
import com.pattern.flyweight.FlyweightFactory;
import org.junit.Test;

public class FlyweightTest {
    @Test
    public void test(){
        FlyweightFactory.getFlyweight("共享变量1");
        FlyweightFactory.getFlyweight("共享变量2");
        FlyweightFactory.getFlyweight("共享变量3");
        FlyweightFactory.getFlyweight("共享变量4");
        FlyweightFactory.getFlyweight("共享变量1").operate();
    }
}

结果

具体享元角色自己有的业务

Process finished with exit code 0

享元模式的线程安全问题

在上面的代码中存在多处线程不安全的问题,如:享元工厂的getFlyweight方法,以及抽象享元角色中的getter和setter方法,它们在多线程的环境下都会导致线程不安全的问题。具体根据实际情况来解决。如:享元工厂的getFlyweight就可以使用DCL单例模式类似的方法来解决。

注意

  • 在多线程的环境下需要注意享元模式多线程照成的线程不安全的问题
  • 外部状态尽量使用基本类型,否则会带来性能的影响,如使用HashMap作为容器,那么作为使用自定义类作为外部状态,那么需要复写equals方法和hashCode方法(如果把一个对象作为Map类的键值,一定要确保重写了equal和hashCode方法,否则会出现通过键值搜索失败的情况。如:get、contains等方法会返回失败的结果),而且执行效率也不高。
  • 在Java API中大量使用了享元模式
  • 虽然享元模式可以实现对象池,但是两者之间还是有比较大的区别。对象池着重在对象的复用上,池中的每个对象是可替换的;而享元模式主要解决对象的共享问题,如何建立多个可共享的细粒度对象是其关注的重点。

下一篇: 设计模式-观察者模式→

loading...