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可以接受并作用于外部状态,示例代码如下:
/***
* 享元接口,通过这个接口享元可以接受并作用于外部状态
*/
public interface Flyweight {
/**
* 示例操作,传入外部状态
* @param extrinsicState 示例参数,外部状态
*/
public void operation(String extrinsicState);
}
|
(2)接下来看看具体的享元接口的实现,先看共享享元的实现,封装flyweight的内部状态,当然也可以提供功能方法,示例代码如下:
/**
* 享元对象
*/
public class ConcreteFlyweight implements Flyweight{
/**
* 示例,描述内部状态
*/
private String intrinsicState;
/**
* 构造方法,传入享元对象的内部状态的数据
* @param state 享元对象的内部状态的数据
*/
public ConcreteFlyweight(String state){
this.intrinsicState = state;
}
public void operation(String extrinsicState) {
//具体的功能处理,可能会用到享元内部、外部的状态
}
}
|
再看看不需要共享的享元对象的实现,并不是所有的Flyweight对象都需要共享,Flyweight接口使共享成为可能,但并不强制共享。示例代码如下:
/**
* 不需要共享的flyweight对象,
* 通常是将被共享的享元对象作为子节点,组合出来的对象
*/
public class UnsharedConcreteFlyweight implements Flyweight{
/**
* 示例,描述对象的状态
*/
private String allState;
public void operation(String extrinsicState) {
// 具体的功能处理
}
}
|
(3)在享元模式中,客户端不能直接创建共享的享元对象实例,必须通过享元工厂来创建。接下来看看享元工厂的实现,示例代码如下:
/**
* 享元工厂
*/
public class FlyweightFactory {
/**
* 缓存多个flyweight对象,这里只是示意一下
*/
private Map fsMap =
new HashMap();
/**
* 获取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;
}
}
|
(4)最后来看看客户端的实现,客户端通常会维持一个对flyweight的引用,计算或存储一个或多个flyweight的外部状态。示例代码如下:
/**
* Client对象,通常会维持一个对flyweight的引用,
* 计算或存储一个或多个flyweight的外部状态
*/
public class Client {
//具体的功能处理
}
|
20.2.4 使用享元模式重写示例
再次分析上面的授权信息,实际上重复出现的数据主要是对安全实体和权限的描述,又考虑到安全实体和权限的描述一般是不分开的,那么找出这些重复的描述,比如:人员列表的查看权限。而且这些重复的数据是可以重用的,比如给它们配上不同的人员,就可以组合成为不同的授权描述,如图20.2所示:
图20.2 授权描述示意图
图20.2就可以描述如下的信息:
张三 对 人员列表 拥有 查看的权限
李四 对 人员列表 拥有 查看的权限
王五 对 人员列表 拥有 查看的权限
|
很明显,可以把安全实体和权限的描述定义成为享元,而和它们结合的人员数据,就可以做为享元的外部数据。为了演示简单,就把安全实体对象和权限对象简化成了字符串,描述一下它们的名字。
(1)按照享元模式,也为了系统的扩展性和灵活性,给享元定义一个接口,外部使用享元还是面向接口来编程,示例代码如下:
/***
* 描述授权数据的享元接口
*/
public interface Flyweight {
/**
* 判断传入的安全实体和权限,是否和享元对象内部状态匹配
* @param securityEntity 安全实体
* @param permit 权限
* @return true表示匹配,false表示不匹配
*/
public boolean match(String securityEntity,String permit);
}
|
(2)定义了享元接口,该来实现享元对象了,这个对象需要封装授权数据中重复出现部分的数据,示例代码如下:
/**
* 封装授权数据中重复出现部分的享元对象
*/
public class AuthorizationFlyweight implements Flyweight{
/**
* 内部状态,安全实体
*/
private String securityEntity;
/**
* 内部状态,权限
*/
private String permit;
/**
* 构造方法,传入状态数据
* @param state 状态数据,包含安全实体和权限的数据,用","分隔
*/
public AuthorizationFlyweight(String state){
String ss[] = state.split(",");
securityEntity = ss[0];
permit = ss[1];
}
public String getSecurityEntity() {
return securityEntity;
}
public String getPermit() {
return permit;
}
public boolean match(String securityEntity, String permit) {
if(this.securityEntity.equals(securityEntity)
&& this.permit.equals(permit)){
return true;
}
return false;
}
}
|
(3)定义好了享元,来看看如何管理这些享元,提供享元工厂来负责享元对象的共享管理和对外提供访问享元的接口。
享元工厂一般不需要很多个,实现成为单例即可。享元工厂负责享元对象的创建和管理,基本的思路就是在享元工厂里面缓存享元对象。在Java中最常用的缓存实现方式,就是定义一个Map来存放缓存的数据,而享元工厂对外提供的访问享元的接口,基本上就是根据key值到缓存的Map中获取相应的数据,这样只要有了共享,同一份数据就可以重复使用了,示例代码如下:
/**
* 享元工厂,通常实现成为单例
*/
public class FlyweightFactory {
private static FlyweightFactory factory =
new FlyweightFactory();
private FlyweightFactory(){
}
public static FlyweightFactory getInstance(){
return factory;
}
/**
* 缓存多个flyweight对象
*/
private Map fsMap =
new HashMap();
/**
* 获取key对应的享元对象
* @param key 获取享元对象的key
* @return key对应的享元对象
*/
public Flyweight getFlyweight(String key) {
Flyweight f = fsMap.get(key);
if(f==null){
f = new AuthorizationFlyweight(key);
fsMap.put(key,f);
}
return f;
}
}
|
(4)使用享元对象
实现完享元工厂,该来看看如何使用享元对象了。按照前面的实现,需要一个对象来提供安全管理的业务功能,就是前面的那个SecurityMgr类,这个类现在在享元模式中,就充当了Client的角色,注意这个Client角色和我们平时说的测试客户端是两个概念,这个Client角色是使用享元的对象。
SecurityMgr的实现方式基本上模仿前面的实现,也会有相应的改变,变化大致如下:
-
缓存的每个人员的权限数据,类型变成了Flyweight的了
-
在原来queryByUser方法里面,通过new来创建授权对象的地方,修改成了通过享元工厂来获取享元对象,这是使用享元模式最重要的一点改变,也就是不是直接去创建对象实例,而是通过享元工厂来获取享元对象实例
示例代码如下:
/**
* 安全管理,实现成单例
*/
public class SecurityMgr {
private static SecurityMgr securityMgr = new SecurityMgr();
private SecurityMgr(){
}
public static SecurityMgr getInstance(){
return securityMgr;
}
/**
* 在运行期间,用来存放登录人员对应的权限,
* 在Web应用中,这些数据通常会存放到session中
*/
private MapFlyweight>> map =
new HashMapFlyweight>>();
/**
* 模拟登录的功能
* @param user 登录的用户
*/
public void login(String user){
//登录时就需要把该用户所拥有的权限,从数据库中取出来,放到缓存中去
Collection 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 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 queryByUser(String user){
Collection col = new ArrayList();
for(String s : TestDB.colDB){
String ss[] = s.split(",");
if(ss[0].equals(user)){
Flyweight fm = FlyweightFactory.getInstance()
.getFlyweight(ss[1]+","+ss[2]);
col.add(fm);
}
}
return col;
}
}
|
(5)所用到的TestDB没有任何变化,这里就不去赘述了
(6)客户端测试代码也没有任何变化,也不去赘述了。
运行测试一下,看看效果,主要是看看是不是能有效地减少那些重复数据对象的数量。运行结果如下:
fm==cn.javass.dp.flyweight.example3.AuthorizationFlyweight@e48e1b
fm==cn.javass.dp.flyweight.example3.AuthorizationFlyweight@e48e1b
fm==cn.javass.dp.flyweight.example3.AuthorizationFlyweight@12dacd1
f1==false
f2==true
fm==cn.javass.dp.flyweight.example3.AuthorizationFlyweight@e48e1b
fm==cn.javass.dp.flyweight.example3.AuthorizationFlyweight@e48e1b
fm==cn.javass.dp.flyweight.example3.AuthorizationFlyweight@e48e1b
|
仔细观察结果中框住的部分,会发现六条数据中,有五条的hashCode是同一个值,根据我们的实现,可以断定这是同一个对象。也就是说,现在只有两个对象实例,而前面的实现中有六个对象实例。
如同示例的那样,对于封装安全实体和权限的这些细粒度对象,既是授权分配的单元对象,也是权限检测的单元对象。可能有很多人对某个安全实体拥有某个权限,如果为每个人都重新创建一个对象来描述对应的安全实体和权限,那样就太浪费内存空间了。
通过共享封装了安全实体和权限的对象,无论多少人拥有这个权限,实际的对象实例都是只有一个,这样既减少了对象的数目,又节省了宝贵的内存空间,从而解决了前面提出的问题。
---------------------------------------------------------------------------
原创内容 跟着cc学设计系列 之 研磨设计模式
研磨设计讨论群【252780326】
原创内容,转载请注明出处【http://sishuok.com/forum/blogPost/list/0/5638.html】
---------------------------------------------------------------------------