结构型-享元(Flyweight)

3/31/2022 设计模式

摘要

JDK:1.8.0_202

# 一:场景问题

# 1.1 权限控制

考虑这样一个问题,给系统加入权限控制,一个安全的系统,需要对用户的每一次操作都要做权限检测,包括功能和数据,以确保只有获得相应授权的人,才能执行相应的功能,操作相应的数据。

举个例子来说吧:普通人员都有能查看到本部门人员列表的权限,但是在人员列表中每个人员的薪资数据,普通人员是不可以看到的;而部门经理在查看本部门人员列表的时候,就可以看到每个人员相应的薪资数据。

举个例子:普通人员都有能查看到本部门人员列表的权限,但是在人员列表中每个人员的薪资数据,普通人员是不可以看到的;而部门经理在查看本部门人员列表的时候,就可以看到每个人员相应的薪资数据。

# 1.2 不使用模式的解决方案

1. 模拟数据库数据

张三  对  人员列表   拥有    查看的权限
李四  对  人员列表   拥有    查看的权限
李四  对  薪资数据   拥有    查看的权限
李四  对  薪资数据   拥有    修改的权限
1
2
3
4

2. 思路选择

由于操作人员进行授权操作过后,各人员被授予的权限是记录在数据库中的,刚开始有开发人员提出,每次用户操作系统的时候,都直接到数据库里面去动态查询,以判断该人员是否拥有相应的权限,但很快就被否决掉了,试想一下,用户操作那么频繁,每次都到数据库里面动态查询,这会严重加剧数据库服务器的负担,使系统变慢。

为了加快系统运行的速度,开发小组决定采用一定的缓存,当每个人员登录的时候,就把该人员能操作的权限获取到,存储在内存中,这样每次操作的时候,就直接在内存里面进行权限的校验,速度会大大加快,这是典型的以空间换时间的做法。

3. 实现示例

定义描述授权数据的数据对象:

/**
 * 描述授权数据的数据model
 */
public class AuthorizationModel {

    /**
     * 人员
     */
    private String user;

    /**
     * 安全实体
     */
    private String securityEntity;

    /**
     * 权限
     */
    private String permit;

    public AuthorizationModel() {

    }

    public AuthorizationModel(String user, String securityEntity, String permit) {
        this.user = user;
        this.securityEntity = securityEntity;
        this.permit = permit;
    }

    public String getUser() {
        return user;
    }

    public AuthorizationModel setUser(String user) {
        this.user = user;
        return this;
    }

    public String getSecurityEntity() {
        return securityEntity;
    }

    public AuthorizationModel setSecurityEntity(String securityEntity) {
        this.securityEntity = securityEntity;
        return this;
    }

    public String getPermit() {
        return permit;
    }

    public AuthorizationModel setPermit(String permit) {
        this.permit = permit;
        return this;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

模拟的内存数据:

/**
 * 供测试用,在内存中模拟数据库中的值
 */
public class TestDB {
    /**
     * 用来存放授权数据的值
     */
    public static Collection<AuthorizationModel> colDB = new ArrayList<>();

    static {
        //通过静态块来填充模拟的数据
        colDB.add(new AuthorizationModel("张三", "人员列表", "查看"));
        colDB.add(new AuthorizationModel("李四", "人员列表", "查看"));
        colDB.add(new AuthorizationModel("李四", "薪资数据", "查看"));
        colDB.add(new AuthorizationModel("李四", "薪资数据", "修改"));
        //增加更多的授权数据
        colDB.addAll(IntStream.range(0, 3).mapToObj(e -> new AuthorizationModel("张三" + e, "人员列表", "查看")).collect(Collectors.toList()));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

登录和权限控制的业务:

/**
 * 安全管理,实现成单例
 */
public class SecurityMgr {

    private static final SecurityMgr securityMgr = new SecurityMgr();

    /**
     * 在运行期间,用来存放登录人员对应的权限
     * 在Web应用中,这些数据通常会存放到Session中
     */
    private Map<String, Collection<AuthorizationModel>> map = new HashMap<>();

    private SecurityMgr() {

    }

    public static SecurityMgr getInstance() {
        return securityMgr;
    }

    /**
     * 模拟登录的功能
     *
     * @param user 登录的用户
     */
    public void login(String user) {
        // 登录时就需要把该用户所拥有的权限,从数据库中取出来,放到缓存中去
        Collection<AuthorizationModel> col = queryByUser(user);
        map.put(user, col);
    }

    /**
     * 判断某用户对某个安全实体是否拥有某权限
     *
     * @param user           被检测权限的用户
     * @param securityEntity 安全实体
     * @param permit         权限
     * @return true表示拥有相应权限,false表示没有相应权限
     */
    public boolean hasPermit(String user, String securityEntity, String permit) {
        Collection<AuthorizationModel> col = map.get(user);
        if (col == null || col.size() == 0) {
            System.out.println(user + "没有登录或是没有被分配任何权限");
            return false;
        }
        for (AuthorizationModel am : col) {
            //输出当前实例,看看是否同一个实例对象
            System.out.println("am==" + am);
            if (am.getSecurityEntity().equals(securityEntity) && am.getPermit().equals(permit)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 从数据库中获取某人所拥有的权限
     *
     * @param user 需要获取所拥有的权限的人员
     * @return 某人所拥有的权限
     */
    private Collection<AuthorizationModel> queryByUser(String user) {
        return TestDB.colDB.stream().filter(e -> e.getUser().equals(user)).collect(Collectors.toList());
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

客户端:

public class Client {
    public static void main(String[] args) {
        // 需要先登录,然后再判断是否有权限
        SecurityMgr mgr = SecurityMgr.getInstance();
        mgr.login("张三");
        mgr.login("李四");
        boolean f1 = mgr.hasPermit("张三", "薪资数据", "查看");
        boolean f2 = mgr.hasPermit("李四", "薪资数据", "查看");

        System.out.println("f1==" + f1);
        System.out.println("f2==" + f2);

        IntStream.range(0, 3).forEach(e -> mgr.login("张三" + e));
        IntStream.range(0, 3).forEach(e -> mgr.hasPermit("张三" + e, "薪资数据", "查看"));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

运行结果:

输出结果中的f1为false,表示张三对薪资数据没有查看的权限;而f2为true,表示李四对对薪资数据有查看的权限,是正确的,基本完成了功能。

# 1.3 有何问题

在Java中,默认的equals方法比较的是内存地址,而equals方法和hashCode方法的关系是:equals方法返回true的话,那么这两个对象实例的hashCode必须相同;而hashCode相同,equals方法并不一定返回true,也就是说两个对象实例不一定是同一对象实例。换句话说,如果hashCode不同的话,铁定不是同一个对象实例。

这就引出一个问题了,就是对象实例数目太多,为什么这么说呢?看看就描述这么几条数据,数数看有多少个对象实例呢?目前是一条数据就有一个对象实例,这很恐怖,数据库的数据量是很大的,如果有几万条,几十万条,岂不是需要几万个,甚至几十万个对象实例,这会耗费掉大量的内存。

另外,这些对象的粒度都很小,都是简单的描述某一个方面的对象,而且很多数据是重复的,在这些大量重复的数据上耗费掉了很多的内存

把上面的问题描述出来就是:在系统当中,存在大量的细粒度对象,而且存在大量的重复数据,严重耗费内存,如何解决?

# 二:解决方案

用来解决上述问题的一个合理的解决方案就是享元模式。

享元模式:运用共享技术有效地支持大量细粒度的对象。

仔细观察和分析上面的授权信息,会发现有一些数据是重复出现的,比如:人员列表、薪资数据、查看、修改等等。至于人员相关的数据,考虑到每个描述授权的对象都是和某个人员相关的,所以存放的时候,会把相同人员的授权信息组织在一起,就不去考虑人员数据的重复性了。

现在造成内存浪费的主要原因:就是细粒度对象太多,而且有大量重复的数据。如果能够有效的减少对象的数量,减少重复的数据,那么就能够节省不少内存。一个基本的思路就是缓存这些包含着重复数据的对象,让这些对象只出现一次,也就只耗费一份内存了

但是请注意,并不是所有的对象都适合缓存,因为缓存的是对象的实例,实例里面存放的主要是对象属性的值。因此,如果被缓存的对象的属性值经常变动,那就不适合缓存了,因为真实对象的属性值变化了,那么缓存里面的对象也必须要跟着变化,否则缓存中的数据就跟真实对象的数据不同步,可以说是错误的数据了。

因此,需要分离出被缓存对象实例中,哪些数据是不变且重复出现的,哪些数据是经常变化的,真正应该被缓存的数据是那些不变且重复出现的数据,把它们称为对象的内部状态,而那些变化的数据就不缓存了,把它们称为对象的外部状态

这样在实现的时候,把内部状态分离出来共享,称之为享元,通过共享享元对象来减少对内存的占用把外部状态分离出来,放到外部,让应用在使用的时候进行维护,并在需要的时候传递给享元对象使用为了控制对内部状态的共享,并且让外部能简单的使用共享数据,提供一个工厂来管理享元,把它称为享元工厂

# 2.2 模式结构和说明

classDiagram direction RL FlyweightFactory <.. Client FlyweightFactory o--> Flyweight Flyweight <|-- ConcreteFlyweight Flyweight <|-- UnsharedConcreteFlyweight ConcreteFlyweight <.. Client UnsharedConcreteFlyweight <.. Client class FlyweightFactory{ -Map<String, Flyweight> fsMap +operation(String) void } class Flyweight{ <<interface>> +operation(String)* void } class ConcreteFlyweight{ -String intrinsicState +operation(String) void } class UnsharedConcreteFlyweight{ -String allState +operation(String) void }
  • Flyweight:享元接口,通过这个接口flyweight可以接受并作用与外部状态。通过这个接口传入外部的状态,在享元对象的方法处理中可能会使用这些外部的数据。
  • ConcreteFlyweight:具体的享元实现对象,必须是可共享的,需要封装flyweight的内部状态。
  • UnsharedConcreteFlyweight:非共享的享元实现对象,并不是所有的Flyweight实现对象都需要共享。非共享的享元实现对象通常是对共享享元对象的组合对象。
  • FlyweightFactory:享元工厂,主要用来创建并管理共享的享元对象,并对外提供访问共享享元的接口。
  • Client:享元客户端,主要的工作是维持一个对flyweight的引用,计算或存储享元对象的外部状态,当然这里可以访问共享和不共享的flyweight对象。

# 2.3 示例代码

享元接口定义:

/**
 * 享元接口,通过这个接口享元可以接受并作用于外部状态
 */
public interface Flyweight {

    /**
     * 示例操作,传入外部状态
     *
     * @param extrinsicState 示例参数,外部状态
     */
    void operation(String extrinsicState);
}
1
2
3
4
5
6
7
8
9
10
11
12

享元接口的实现,共享享元的实现:

/**
 * 享元对象
 */
public class ConcreteFlyweight implements Flyweight {

    /**
     * 示例,描述内部状态
     */
    private String intrinsicState;

    /**
     * 构造方法,传入享元对象的内部状态的数据
     *
     * @param state 享元对象的内部状态的数据
     */
    public ConcreteFlyweight(String state) {
        this.intrinsicState = state;
    }

    public void operation(String extrinsicState) {
        // 具体的功能处理,可能会用到享元内部、外部的状态
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

享元接口的实现,不需要共享的享元对象的实现:

/**
 * 不需要共享的flyweight对象,
 * 通常是将被共享的享元对象作为子节点,组合出来的对象
 */
public class UnsharedConcreteFlyweight implements Flyweight {
    /**
     * 示例,描述对象的状态
     */
    private String allState;

    public void operation(String extrinsicState) {
        // 具体的功能处理
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

享元工厂:

/**
 * 享元工厂
 */
public class FlyweightFactory {

    /**
     * 缓存多个flyweight对象,这里只是示意一下
     */
    private Map<String, Flyweight> fsMap = new HashMap<String, Flyweight>();

    /**
     * 获取key对应的享元对象
     *
     * @param key 获取享元对象的key,只是示意
     * @return key 对应的享元对象
     */
    public Flyweight getFlyweight(String key) {
        //这个方法里面基本的实现步骤如下:
        //1:先从缓存里面查找,是否存在key对应的Flyweight对象
        Flyweight f = fsMap.get(key);

        //2:如果存在,就返回相对应的Flyweight对象
        if (f == null) {
            //3:如果不存在
            //3.1:创建一个新的Flyweight对象
            f = new ConcreteFlyweight(key);
            //3.2:把这个新的Flyweight对象添加到缓存里面
            fsMap.put(key, f);
            //3.3:然后返回这个新的Flyweight对象
        }

        return f;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

客户端:

/**
 * Client对象,通常会维持一个对flyweight的引用,
 * 计算或存储一个或多个flyweight的外部状态
 */
public class Client {
    //具体的功能处理
}
1
2
3
4
5
6
7

# 2.4 重写案例

再次分析上面的授权信息,实际上重复出现的数据主要是对安全实体和权限的描述,又考虑到安全实体和权限的描述一般是不分开的,那么找出这些重复的描述,比如:人员列表的查看权限。而且这些重复的数据是可以重用的,比如给它们配上不同的人员,就可以组合成为不同的授权描述

很明显,可以把安全实体和权限的描述定义成为享元,而和它们结合的人员数据,就可以做为享元的外部数据

享元接口:

/**
 * 描述授权数据的享元接口
 */
public interface Flyweight {

    /**
     * 判断传入的安全实体和权限,是否和享元对象内部状态匹配
     *
     * @param securityEntity 安全实体
     * @param permit         权限
     * @return true表示匹配,false表示不匹配
     */
    boolean match(String securityEntity, String permit);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

享元对象,这个对象需要封装授权数据中重复出现部分的数据:

/**
 * 封装授权数据中重复出现部分的享元对象
 */
public class AuthorizationFlyweight implements Flyweight {

    /**
     * 内部状态,安全实体
     */
    private String securityEntity;

    /**
     * 内部状态,权限
     */
    private String permit;

    public AuthorizationFlyweight(String securityEntity, String permit) {
        this.securityEntity = securityEntity;
        this.permit = permit;
    }

    public String getSecurityEntity() {
        return securityEntity;
    }

    public String getPermit() {
        return permit;
    }

    public boolean match(String securityEntity, String permit) {
        return this.securityEntity.equals(securityEntity) && this.permit.equals(permit);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

享元工厂:

/**
 * 享元工厂,通常实现成为单例
 */
public class FlyweightFactory {

    private static FlyweightFactory factory = new FlyweightFactory();

    /**
     * 缓存多个 flyweight 对象
     */
    private Map<String, Flyweight> fsMap = new HashMap<>();

    private FlyweightFactory() {
    }

    public static FlyweightFactory getInstance() {
        return factory;
    }

    /**
     * 获取key对应的享元对象
     *
     * @param securityEntity 安全实体
     * @param permit         权限
     * @return key对应的享元对象
     */
    public Flyweight getFlyweight(String securityEntity, String permit) {
        Flyweight f = fsMap.get(securityEntity + ":" + permit);
        if (f == null) {
            f = new AuthorizationFlyweight(securityEntity, permit);
            fsMap.put(securityEntity + ":" + permit, f);
        }
        return f;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

模拟的内存数据:

/**
 * 供测试用,在内存中模拟数据库中的值
 */
public class TestDB {
    /**
     * 用来存放授权数据的值
     */
    public static Collection<AuthorizationModel> colDB = new ArrayList<>();

    static {
        //通过静态块来填充模拟的数据
        colDB.add(new AuthorizationModel("张三", "人员列表", "查看"));
        colDB.add(new AuthorizationModel("李四", "人员列表", "查看"));
        colDB.add(new AuthorizationModel("李四", "薪资数据", "查看"));
        colDB.add(new AuthorizationModel("李四", "薪资数据", "修改"));
        //增加更多的授权数据
        colDB.addAll(IntStream.range(0, 3).mapToObj(e -> new AuthorizationModel("张三" + e, "人员列表", "查看")).collect(Collectors.toList()));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

登录和权限控制的业务:

变化大致如下:

缓存的每个人员的权限数据,类型变成了Flyweight的了;

在原来queryByUser方法里面,通过new来创建授权对象的地方,修改成了通过享元工厂来获取享元对象,这是使用享元模式最重要的一点改变,也就是不是直接去创建对象实例,而是通过享元工厂来获取享元对象实例;

/**
 * 安全管理,实现成单例
 */
public class SecurityMgr {

    private static SecurityMgr securityMgr = new SecurityMgr();

    /**
     * 在运行期间,用来存放登录人员对应的权限,
     * 在web应用中,这些数据通常会存放在Session中
     */
    private Map<String, Collection<Flyweight>> map = new HashMap<>();

    private SecurityMgr() {
    }

    public static SecurityMgr getInstance() {
        return securityMgr;
    }

    /**
     * 模拟登录的功能
     *
     * @param user 登录的用户
     */
    public void login(String user) {
        //登录时就需要把该用户所拥有的权限,从数据库中取出来,放到缓存中去
        Collection<Flyweight> col = queryByUser(user);
        map.put(user, col);
    }

    /**
     * 判断某用户对某个安全实体是否拥有某权限
     *
     * @param user           被检测权限的用户
     * @param securityEntity 安全实体
     * @param permit         权限
     * @return true表示拥有相应权限,false表示没有相应权限
     */
    public boolean hasPermit(String user, String securityEntity, String permit) {
        Collection<Flyweight> col = map.get(user);
        if (col == null || col.size() == 0) {
            System.out.println(user + "没有登录或是没有被分配任何权限");
            return false;
        }
        for (Flyweight fm : col) {
            //输出当前实例,看看是否同一个实例对象
            System.out.println("fm==" + fm);
            if (fm.match(securityEntity, permit)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 从数据库中获取某人所拥有的权限
     *
     * @param user 需要获取所拥有的权限的人员
     * @return 某人所拥有的权限
     */
    private Collection<Flyweight> queryByUser(String user) {
        List<AuthorizationModel> temp = TestDB.colDB.stream().filter(e -> e.getUser().equals(user)).collect(Collectors.toList());
        return temp.stream().map(e -> FlyweightFactory.getInstance().getFlyweight(e.getSecurityEntity(), e.getPermit())).collect(Collectors.toList());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

客户端:

public class Client {
    public static void main(String[] args) {
        // 需要先登录,然后再判断是否有权限
        SecurityMgr mgr = SecurityMgr.getInstance();
        mgr.login("张三");
        mgr.login("李四");
        boolean f1 = mgr.hasPermit("张三", "薪资数据", "查看");
        boolean f2 = mgr.hasPermit("李四", "薪资数据", "查看");

        System.out.println("f1==" + f1);
        System.out.println("f2==" + f2);

        IntStream.range(0, 3).forEach(e -> mgr.login("张三" + e));
        IntStream.range(0, 3).forEach(e -> mgr.hasPermit("张三" + e, "薪资数据", "查看"));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

运行结果

可以发现六条数据中,有五条的hashCode是同一个值,根据我们的实现,可以断定这是同一个对象。也就是说,现在只有两个对象实例,而前面的实现中有六个对象实例。

如同示例的那样,对于封装安全实体和权限的这些细粒度对象,既是授权分配的单元对象,也是权限检测的单元对象。可能有很多人对某个安全实体拥有某个权限,如果为每个人都重新创建一个对象来描述对应的安全实体和权限,那样就太浪费内存空间了。

通过共享封装了安全实体和权限的对象,无论多少人拥有这个权限,实际的对象实例都是只有一个,这样既减少了对象的数目,又节省了宝贵的内存空间,从而解决了前面提出的问题。

# 三:模式讲解

# 3.1 认识享元模式

1. 变与不变

享元模式设计的重点就在于分离变与不变,把一个对象的状态分成内部状态和外部状态,内部状态是不变的,外部状态是可变的。然后通过共享不变的部分,达到减少对象数量、并节约内存的目的。在享元对象需要的时候,可以从外部传入外部状态给共享的对象,共享对象会在功能处理的时候,使用自己内部的状态和这些外部的状态。

事实上,分离变与不变是软件设计上最基本的方式之一,比如预留接口,为什么在这个地方要预留接口,一个常见的原因就是这里存在变化,可能在今后需要扩展、或者是改变已有的实现,因此预留接口做为“可插入性的保证”。

2. 共享与不共享

在享元模式中,享元对象又有共享与不共享之分,这种情况通常出现在跟组合模式合用的情况,通常共享的是叶子对象,一般不共享的部分是由共享部分组合而成的,由于所有细粒度的叶子对象都已经缓存了,那么缓存组合对象就没有什么意义了。这个在后面给大家一个示例。

3. 内部状态和外部状态

享元模式的内部状态,通常指的是包含在享元对象内部的、对象本身的状态,通常是独立于使用享元的场景的信息,一般创建过后就不再变化的状态,因此可以共享。

外部状态指的是享元对象之外的状态,取决于使用享元的场景,会根据使用场景而变化,因此不可共享。如果享元对象需要这些外部状态的话,可以从外部传递到享元对象里面,比如通过方法的参数来传递。

也就是说享元模式真正缓存和共享的数据是享元的内部状态,而外部状态是不应该被缓存共享的

另外一点,内部状态和外部状态是独立的,外部状态的变化不应该影响到内部状态

4. 实例池

在享元模式中,为了创建和管理共享的享元部分,引入了享元工厂,享元工厂中一般都包含有享元对象的实例池,享元对象就是缓存在这个实例池中的

简单介绍一点实例池的知识,所谓实例池,指的是缓存和管理对象实例的程序,通常实例池会提供对象实例的运行环境,并控制对象实例的生命周期

工业级的实例池实现上有两个最基本的难点,一个就是动态控制实例数量,一个就是动态分配实例来提供给外部使用。这些都是需要算法来做保证的。

假如实例池里面已有了3个实例,但是客户端请求非常多,有些忙不过来,那么实例池的管理程序就应该判断出来,到底几个实例才能满足现在的客户需求,理想状况是刚刚好,就是既能够满足应用的需要,又不会造成对象实例的浪费,假如经过判断5个实例正好,那么实例池的管理程序就应该能动态的创建2个新的实例。

这样运行了一段时间,客户端的请求减少了,这个时候实例池的管理程序又应该动态的判断,究竟几个实例是最好的,多了明显浪费资源,假如经过判断只需要1个实例就可以了,那么实例池的管理程序应该销毁掉多余的4个实例,以释放资源。这就是动态控制实例数量

对于动态分配实例,也说明一下吧,假如实例池里面有3个实例,这个时候来了一个新的请求,到底调度哪一个实例去执行客户的请求呢,如果有空闲实例,那就是它了,要是没有空闲实例呢,是新建一个实例,还是等待运行中的实例,等它运行完了就来处理这个请求呢?具体如何调度,也是需要算法来保障的。

回到享元模式中来,享元工厂中的实例池可没有这么复杂,因为共享的享元对象基本上都是一个实例,一般不会出现同一个享元对象有多个实例的情况,这样就不用去考虑动态创建和销毁享元对象实例的功能;另外只有一个实例,也就不存在动态调度的麻烦,反正就是它了

这也主要是因为享元对象封装的多半是对象的内部状态,这些状态通常是不变的,有一个实例就够了,不需要动态控制生命周期,也不需要动态调度,它只需要做一个缓存而已,没有上升到真正的实例池那么个高度。

5. 谁来初始化共享对象

在享元模式中,通常是在第一次向享元工厂请求获取共享对象的时候,进行共享对象的初始化,而且多半都是在享元工厂内部实现,不会从外部传入共享对象。当然可以从外部传入一些创建共享对象需要的值,享元工厂可以按照这些值去初始化需要共享的对象,然后就把创建好的共享对象的实例放入享元工厂内部的缓存中,以后再请求这个共享对象的时候就不用再创建了。

# 3.2 优缺点

1. 减少对象数量,节省内存空间

可能有的朋友认为共享对象会浪费空间,但是如果这些对象频繁使用,那么其实是节省空间的。因为占用空间的大小等于每个对象实例占用的大小再乘以数量,对于享元对象来讲,基本上就只有一个实例,大大减少了享元对象的数量,并节省不少的内存空间

节省的空间取决于以下几个因素:因为共享而减少的实例数目、每个实例本身所占用的空间。假如每个对象实例占用2个字节,如果不共享数量是100个,而共享过后就只有一个了,那么节省的空间约等于:(100-1) X 2 字节。

2. 维护共享对象,需要额外开销

如同前面演示的享元工厂,在维护共享对象的时候,如果功能复杂,会有很多额外的开销,比如有一个线程来维护垃圾回收。

# 3.3 思考享元模式

1. 享元模式的本质

享元模式的本质:分离与共享。

分离的是对象状态中变与不变的部分,共享的是对象中不变的部分。享元模式的关键之处就在于分离变与不变,把不变的部分作为享元对象的内部状态,而变化部分就作为外部状态,由外部来维护,这样享元对象就能够被共享,从而减少对象数量,并节省大量的内存空间。

理解了这个本质后,在使用享元模式的时候,就会去考虑,哪些状态需要分离?如何分离?分离后如何处理?哪些需要共享?如何管理共享的对象?外部如何使用共享的享元对象?是否需要不共享的对象?等等问题。

把这些问题都思考清楚,找到相应的解决方法,那么享元模式也就应用起来了,可能是标准的应用,也可能是变形的应用,但万变不离其宗。

2. 何时选用享元模式

建议在如下情况中,选用享元模式:

如果一个应用程序使用了大量的细粒度对象,可以使用享元模式来减少对象数量;

如果由于使用大量的对象,造成很大的存储开销,可以使用享元模式来减少对象数量,并节约内存;

如果对象的大多数状态都可以转变为外部状态,比如通过计算得到,或是从外部传入等,可以使用享元模式来实现内部状态和外部状态的分离;

如果不考虑对象的外部状态,可以用相对较少的共享对象取代很多组合对象,可以使用享元模式来共享对象,然后组合对象来使用这些共享对象;

# 3.4 相关模式

1. 享元模式与单例模式

这两个模式可以组合使用。

通常情况下,享元模式中的享元工厂可以实现成为单例。另外,享元工厂里面缓存的享元对象,都是单实例的,可以看成是单例模式的一种变形控制,在享元工厂里面来单例享元对象。

2. 享元模式与组合模式

这两个模式可以组合使用。

在享元模式里面,存在不需要共享的享元实现,这些不需要共享的享元通常是对共享的享元对象的组合对象,也就是说,享元模式通常会和组合模式组合使用,来实现更复杂的对象层次结构。

3. 享元模式与状态模式

这两个模式可以组合使用。

可以使用享元模式来共享状态模式中的状态对象,通常在状态模式中,会存在数量很大的、细粒度的状态对象,而且它们基本上都是可以重复使用的,都是用来处理某一个固定的状态的,它们需要的数据通常都是由上下文传入,也就是变化部分都分离出去了,所以可以用享元模式来实现这些状态对象。

4. 享元模式与策略模式

这两个模式可以组合使用。

可以使用享元模式来实现策略模式中的策略对象,跟状态模式一样,在策略模式中也存在大量细粒度的策略对象,它们需要的数据同样是从上下文传入的,所以可以使用享元模式来实现这些策略对象。

# 四:JDK

# 五:参考文献

最后更新: 10/22/2022, 10:41:05 AM