20.2 解 决 方 案
20.2.1 使用享元模式来解决问题
用来解决上述问题的一个合理的解决方案就是享元模式。那么什么是享元模式呢?
1.享元模式的定义
运用共享技术有效地支持大量细粒度的对象。
2.应用享元模式来解决的思路
仔细观察和分析上面的授权信息,会发现有一些数据是重复出现的,比如:人员列表、薪资数据、查看、修改等。至于人员相关的数据,考虑到每个描述授权的对象都是和某个人员相关的,所以存放的时候,会把相同人员的授权信息组织在一起,就不去考虑人员数据的重复性了。
现在造成内存浪费的主要原因:就是细粒度对象太多,而且有大量重复的数据。如果能够有效地减少对象的数量,减少重复的数据,那么就能够节省不少内存。一个基本的思路就是缓存这些包含着重复数据的对象,让这些对象只出现一次,也就只耗费一份内存了。
注意
但是请注意,并不是所有的对象都适合缓存,因为缓存的是对象的实例,实例里面存放的主要是对象属性的值。因此,如果被缓存的对象的属性值经常变动,那就不适合缓存了,因为真实对象的属性值变化了,那么缓存中的对象也必须要跟着变化,否则缓存中的数据就跟真实对象的数据不同步,可以说是错误的数据了。
因此,需要分离出被缓存对象实例中,哪些数据是不变且重复出现的,哪些数据是经常变化的,真正应该被缓存的数据是那些不变且重复出现的数据,把它们称为对象的内部状态,而那些变化的数据就不缓存了,把它们称为对象的外部状态。
这样在实现的时候,把内部状态分离出来共享,称之为享元,通过共享享元对象来减少对内存的占用。把外部状态分离出来,放到外部,让应用在使用的时候进行维护,并在需要的时候传递给享元对象使用。为了控制对内部状态的共享,并且让外部能简单地使用共享数据,提供一个工厂来管理享元,把它称为享元工厂。
20.2.2 享元模式的结构和说明
享元模式的结构如图20.1所示。
图20.1 享元模式的结构图
■ Flyweight:享元接口,通过这个接口Flyweight可以接受并作用于外部状态。通过这个接口传入外部的状态,在享元对象的方法处理中可能会使用这些外部的数据。
■ ConcreteFlyweight:具体的享元实现对象,必须是可共享的,需要封装Flyweight的内部状态。
■ UnsharedConcreteFlyweight:非共享的享元实现对象,并不是所有的Flyweight实现对象都需要共享。非共享的享元实现对象通常是对共享享元对象的组合对象。
■ FlyweightFactory:享元工厂,主要用来创建并管理共享的享元对象,并对外提供访问共享享元的接口。
■ Client:享元客户端,主要的工作是维持一个对Flyweight的引用,计算或存储享元对象的外部状态,当然这里可以访问共享和不共享的Flyweight对象。
20.2.3 享元模式示例代码
(1)先看看享元的接口定义。通过这个接口Flyweight可以接受并作用于外部状态。示例代码如下:
(2)接下来看看具体的享元接口的实现。
先看看共享享元的实现。封装Flyweight的内部状态,当然也可以提供功能方法。示例代码如下:
再来看看不需要共享的享元对象的实现。并不是所有的Flyweight对象都需要共享,Flyweight接口使共享成为可能,但并不强制共享。示例代码如下:
(3)在享元模式中,客户端不能直接创建共享的享元对象实例,必须通过享元工厂来创建。下面来看看享元工厂的实现。示例代码如下:
(4)最后来看看客户端的实现。客户端通常会维持一个对Flyweight的引用,计算或存储一个或多个Flyweight的外部状态。示例代码如下:
20.2.4 使用享元模式重写示例
再次分析上面的授权信息。实际上重复出现的数据主要是对安全实体和权限的描述,又考虑到安全实体和权限的描述一般是不分开的,那么找出这些重复的描述,比如,人员列表的查看权限。而且这些重复的数据是可以重用的,比如给它们配上不同的人员,就可以组合成为不同的授权描述,如图20.2所示。
图20.2 授权描述示意图
图20.2就可以描述如下的信息:
张三 对 人员列表 拥有 查看的权限
李四 对 人员列表 拥有 查看的权限
王五 对 人员列表 拥有 查看的权限
很明显,可以把安全实体和权限的描述定义成为享元,而和它们结合的人员数据,就可以作为享元的外部数据。为了演示简单,就把安全实体对象和权限对象简化成了字符串,描述一下它们的名字。
(1)按照享元模式,也为了系统的扩展性和灵活性,给享元定义一个接口,外部使用享元还是面向接口来编程。示例代码如下:
(2)定义了享元接口,该来实现享元对象了,这个对象需要封装授权数据中重复出现部分的数据。示例代码如下:
(3)定义好了享元,来看看如何管理这些享元。提供享元工厂来负责享元对象的共享管理和对外提供访问享元的接口。
享元工厂一般不需要很多个,实现成为单例即可。享元工厂负责享元对象的创建和管理,基本的思路就是在享元工厂中缓存享元对象。在Java中最常用的缓存实现方式,就是定义一个Map来存放缓存的数据,而享元工厂对外提供的访问享元的接口,基本上就是根据key值到缓存的Map中获取相应的数据,这样只要有了共享,同一份数据就可以重复使用了。示例代码如下:
(4)使用享元对象。
实现完享元工厂,该来看看如何使用享元对象了。按照前面的实现,需要一个对象来提供安全管理的业务功能,就是前面的那个SecurityMgr类,这个类现在在享元模式中,就充当了Client的角色。注意这个Client角色和我们平时说的测试客户端是两个概念,这个Client角色是使用享元的对象。
SecurityMgr的实现方式基本上模仿前面的实现,也会有相应的改变,变化大致如下。
■ 缓存的每个人员的权限数据,类型变成了Flyweight的。
■ 在原来queryByUser方法中,通过new来创建授权对象的地方修改成了通过享元工厂来获取享元对象,这是使用享元模式最重要的一点改变,也就是不是直接去创建对象实例,而是通过享元工厂来获取享元对象实例。
示例代码如下:
(5)所用到的TestDB没有任何变化,这里不再赘述。
(6)客户端测试代码也没有任何变化,也不再赘述。
运行测试一下,看看效果。主要是看看是不是能有效地减少那些重复数据对象的数量。运行结果如下:
仔细观察结果中蓝色的部分,会发现六条数据中,有五条的hashCode是同一个值,根据我们的实现,可以断定这是同一个对象。也就是说,现在只有两个对象实例,而前面的实现中有六个对象实例。
如同示例的那样,对于封装安全实体和权限的这些细粒度对象,既是授权分配的单元对象,也是权限检测的单元对象。可能有很多人对某个安全实体拥有某个权限,如果为每个人都重新创建一个对象来描述对应的安全实体和权限,那样就太浪费内存空间了。
通过共享封装了安全实体和权限的对象,无论多少人拥有这个权限,实际的对象实例都是只有一个,这样既减少了对象的数目,又节省了宝贵的内存空间,从而解决了前面提出的问题。