#0 系列目录#java
#1 场景问题# ##1.1 加入权限控制## 考虑这样一个问题,给系统加入权限控制,这基本上是全部的应用系统都有的功能了。算法
对于应用系统而言,通常先要登陆系统,才可使用系统的功能,登陆事后,用户的每次操做都须要通过权限系统的控制,确保该用户有操做该功能的权限
,同时还要控制该用户对数据的访问权限、修改权限等等。总之一句话,一个安全的系统,须要对用户的每一次操做都要作权限检测,包括功能和数据,以确保只有得到相应受权的人,才能执行相应的功能,操做相应的数据
。数据库
举个例子来讲吧:普通人员都有能查看到本部门人员列表的权限,可是在人员列表中每一个人员的薪资数据,普通人员是不能够看到的;而部门经理在查看本部门人员列表的时候,就能够看到每一个人员相应的薪资数据。编程
如今就要来实现为系统加入权限控制的功能,该怎么实现呢?设计模式
为了让你们更好的理解后面讲述的知识,先介绍一点权限系统的基础知识。几乎全部的权限系统都分红两个部分,一个是受权部分,一个是验证部分
,为了理解它们,首先解释两个基本的名词:安全实体和权限
。缓存
安全实体:就是被权限系统检测的对象,好比工资数据。安全
权限:就是须要被校验的权限对象,好比查看、修改等。服务器
安全实体和权限一般要一块儿描述才有意义
,好比有这么个描述:“如今要检测登陆人员对工资数据是否有查看的权限”, “工资数据”这个安全实体和“查看”这个权限必定要一块儿描述
。若是只出现安全实体描述,那就变成这样:“如今要检测登陆人员对工资数据”,对工资数据干什么呀,没有后半截,一看就知道不完整;固然只有权限描述也不行,那就变成:“如今要检测登陆人员是否有查看的权限”,对谁的查看权限啊,也不完整。因此安全实体和权限一般要一块儿描述。session
了解了上面两个名词,来看看什么是受权和验证:数据结构
所谓受权是指:把对某些安全实体的某些权限分配给某些人员的过程。
所谓验证是指:判断某我的员对某个安全实体是否拥有某个或某些权限的过程。
也就是说,受权过程便是权限的分配过程,而验证过程则是权限的匹配过程
。在目前应用系统的开发中,多数是利用数据库来存放受权过程产生的数据,也就是说:受权是向数据库里面添加数据、或是维护数据的过程,而匹配过程就变成了从数据库中获取相应数据进行匹配的过程了。
为了让问题相对简化一点,就不去考虑权限的另外两个特征,一个是继承性,一个是最近匹配原则
,都什么意思呢,仍是解释一下:
好比:某个大楼和楼内的房间都是安全实体,很明显大楼这个安全实体会包含楼内的房间这些安全实体,能够认为大楼是楼内房间的父级实体。如今来考虑一个具体的权限——进入某个房间的权限。若是这个房间没有门,也就是谁均可以进入,至关于这个房间对应的安全实体,没有进入房间的权限限制,那么是否是说全部的人均可以进入这个房间呢?固然不是,某人能进入这个房间的前提是:这我的要有权限进入这个大楼,也就是说,这个时候房间这个安全实体,它自己没有进入权限的限制,可是它会继承父级安全实体的进入权限。
继续上面权限继承性的例子,若是如今这个大楼是坐落在某个机关大院内,这就演变成了,要进入某个房间,首先要有进入大楼的权限,要进入大楼又须要有能进入机关大院的权限。
所谓最近匹配原则就是,若是某个房间没有门,也就意味着这个房间没有进入的权限限制,那么它就会向上继续寻找并匹配,看看大楼有没有进入的权限限制,若是有就使用这个权限限制,终止寻找;若是没有,继续向上寻找,直到找到一个匹配的为止。若是最后大院也没有进入的权限限制,那就变成全部人均可以进入到这个房间里面来了。
##1.2 不使用模式的解决方案##
系统的受权工做已经完成,受权数据记录在数据库里面,具体的数据结构就不去展开了,反正里面记录了人员对安全实体所拥有的权限。假如如今系统中已有以下的受权数据:
张三 对 人员列表 拥有 查看的权限 李四 对 人员列表 拥有 查看的权限 李四 对 薪资数据 拥有 查看的权限 李四 对 薪资数据 拥有 修改的权限
因为操做人员进行受权操做事后,各人员被授予的权限是记录在数据库中的,刚开始有开发人员提出,每次用户操做系统的时候,都直接到数据库里面去动态查询,以判断该人员是否拥有相应的权限,但很快就被否决掉了,试想一下,用户操做那么频繁,每次都到数据库里面动态查询,这会严重加重数据库服务器的负担,使系统变慢。
为了加快系统运行的速度,开发小组决定采用必定的缓存,当每一个人员登陆的时候,就把该人员能操做的权限获取到,存储在内存中,这样每次操做的时候,就直接在内存里面进行权限的校验,速度会大大加快,这是典型的以空间换时间的作法。
(1)首先定义描述受权数据的数据对象,示例代码以下:
/** * 描述受权数据的数据model */ public class AuthorizationModel { /** * 人员 */ private String user; /** * 安全实体 */ private String securityEntity; /** * 权限 */ private String permit; public String getUser() { return user; } public void setUser(String user) { this.user = user; } public String getSecurityEntity() { return securityEntity; } public void setSecurityEntity(String securityEntity) { this.securityEntity = securityEntity; } public String getPermit() { return permit; } public void setPermit(String permit) { this.permit = permit; } }
(2)为了测试方便,作一个模拟的内存数据库,把受权数据存储在里面,用最简单的字符串存储的方式。示例代码以下:
/** * 供测试用,在内存中模拟数据库中的值 */ public class TestDB { /** * 用来存放受权数据的值 */ public static Collection<String> colDB = new ArrayList<String>(); static{ //经过静态块来填充模拟的数据 colDB.add("张三,人员列表,查看"); colDB.add("李四,人员列表,查看"); colDB.add("李四,薪资数据,查看"); colDB.add("李四,薪资数据,修改"); //增长更多的受权数据 for(int i=0;i<3;i++){ colDB.add("张三"+i+",人员列表,查看"); } } }
(3)接下来实现登陆和权限控制的业务,示例代码以下:
/** * 安全管理,实现成单例 */ public class SecurityMgr { private static SecurityMgr securityMgr = new SecurityMgr(); private SecurityMgr(){ } public static SecurityMgr getInstance(){ return securityMgr; } /** * 在运行期间,用来存放登陆人员对应的权限, * 在Web应用中,这些数据一般会存放到session中 */ private Map<String,Collection<AuthorizationModel>> map = new HashMap<String,Collection<AuthorizationModel>>(); /** * 模拟登陆的功能 * @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){ Collection<AuthorizationModel> col = new ArrayList<AuthorizationModel>(); for(String s : TestDB.colDB){ String ss[] = s.split(","); if(ss[0].equals(user)){ AuthorizationModel am = new AuthorizationModel(); am.setUser(ss[0]); am.setSecurityEntity(ss[1]); am.setPermit(ss[2]); col.add(am); } } return col; } }
(4)好很差用呢,写个客户端来测试一下,示例代码以下:
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); for(int i=0;i<3;i++){ mgr.login("张三"+i); mgr.hasPermit("张三"+i,"薪资数据","查看"); } } }
运行结果以下:
am==cn.javass.dp.flyweight.example1.AuthorizationModel@1eed786 am==cn.javass.dp.flyweight.example1.AuthorizationModel@187aeca am==cn.javass.dp.flyweight.example1.AuthorizationModel@e48e1b f1==false f2==true am==cn.javass.dp.flyweight.example1.AuthorizationModel@12dacd1 am==cn.javass.dp.flyweight.example1.AuthorizationModel@119298d am==cn.javass.dp.flyweight.example1.AuthorizationModel@f72617
输出结果中的f1为false,表示张三对薪资数据没有查看的权限;而f2为true,表示李四对对薪资数据有查看的权限,是正确的,基本完成了功能。
##1.3 有何问题## 看了上面的实现,很简单,并且还考虑了性能的问题,在内存中缓存了每一个人相应的权限数据,使得每次判断权限的时候,速度大大加快,实现得挺不错,难道有什么问题吗?
仔细想一想,问题就来了,既有缓存这种方式固有的问题,也有咱们本身实现上的问题
。先说说缓存固有的问题吧,这个不在本次讨论之列,你们了解一下。
这些数据应该被缓存多久,若是是Web应用,这种跟登陆人员相关的权限数据,可能是放在session中进行缓存,这样session超时的时候,就会被清除掉。若是不是Web应用呢?就得本身来控制了,另外就算是在Web应用中,也不必定非要缓存到session超时才清除。总之,控制缓存数据应该被缓存多长时间,是实现高效缓存的一个问题点。
这里的同步是指的数据同步,不是多线程的同步。好比:上面的受权数据是存放在数据库里的,运行的时候缓存到内存里面,若是真实的受权数据在运行期间发生了改变,那么缓存里的数据就应该和数据库的数据同步,以保持一致,不然数据就错了。如何合理的同步数据,也是实现高效缓存的一个问题点。
对于缓存的数据,有些操做从里面取值,有些操做向缓存里面添加值,有些操做在清除过时的缓存数据,有些操做在进行缓存和真实数据的同步,在一个多线程的环境下,如何合理的对缓存进行并发控制,也是实现高效缓存的一个问题点。
先简单提这么几个,事实上,实现合理、高效的缓存也不是一件很轻松的事情,好在这些问题,都不在咱们此次的讨论之列,这里的重心仍是来说述模式,而不是缓存实现。
再来看看前面实现上的问题,仔细观察在上面输出结果中框住的部分,这些值是输出对象实例获得的,默认输出的是对象的hashCode值,而默认的hashCode值能够用来判断是否是同一对象实例
。在Java中,默认的equals方法比较的是内存地址,而equals方法和hashCode方法的关系是:equals方法返回true的话,那么这两个对象实例的hashCode必须相同;而hashCode相同,equals方法并不必定返回true,也就是说两个对象实例不必定是同一对象实例
。换句话说,若是hashCode不一样的话,铁定不是同一个对象实例
。
仔细看看上面输出结果,框住部分的值是不一样的,代表这些对象实例确定不是同一个对象实例,而是多个对象实例
。这就引出一个问题了,就是对象实例数目太多,为何这么说呢?看看就描述这么几条数据,数数看有多少个对象实例呢?目前是一条数据就有一个对象实例,这很恐怖,数据库的数据量是很大的,若是有几万条,几十万条,岂不是须要几万个,甚至几十万个对象实例,这会耗费掉大量的内存。
另外,这些对象的粒度都很小,都是简单的描述某一个方面的对象,并且不少数据是重复的,在这些大量重复的数据上耗费掉了不少的内存
。好比在前面示例的数据中就会发现有重复的部分,见下面框住的部分:
张三 对 人员列表 拥有 查看的权限 李四 对 人员列表 拥有 查看的权限 李四 对 薪资数据 拥有 查看的权限 李四 对 薪资数据 拥有 修改的权限
前面讲过,对于安全实体和权限通常要联合描述,所以对于“人员列表 这个安全实体 的 查看权限 限制”,就算是受权给不一样的人员,这个描述是同样的。假设在某极端状况下,要把“人员列表 这个安全实体 的 查看权限 限制”受权给一万我的,那么数据库里面会有一万条记录,按照前面的实现方式,会有一万个对象实例,而这些实例里面,有大部分的数据是重复的,并且会重复一万次,你以为这是否是个很大的问题呢?
把上面的问题描述出来就是:在系统当中,存在大量的细粒度对象,并且存在大量的重复数据,严重耗费内存,如何解决?
#2 解决方案# ##2.1 享元模式来解决## 用来解决上述问题的一个合理的解决方案就是享元模式。那么什么是享元模式呢?
仔细观察和分析上面的受权信息,会发现有一些数据是重复出现的,好比:人员列表、薪资数据、查看、修改等等。至于人员相关的数据,考虑到每一个描述受权的对象都是和某我的员相关的,因此存放的时候,会把相同人员的受权信息组织在一块儿,就不去考虑人员数据的重复性了。
如今形成内存浪费的主要缘由:就是细粒度对象太多,并且有大量重复的数据
。若是可以有效的减小对象的数量,减小重复的数据,那么就可以节省很多内存。一个基本的思路就是缓存这些包含着重复数据的对象,让这些对象只出现一次,也就只耗费一分内存了
。
可是请注意,并非全部的对象都适合缓存,由于缓存的是对象的实例,实例里面存放的主要是对象属性的值
。所以,若是被缓存的对象的属性值常常变更,那就不适合缓存了,由于真实对象的属性值变化了,那么缓存里面的对象也必需要跟着变化,不然缓存中的数据就跟真实对象的数据不一样步,能够说是错误的数据了。
所以,须要分离出被缓存对象实例中,哪些数据是不变且重复出现的,哪些数据是常常变化的,真正应该被缓存的数据是那些不变且重复出现的数据,把它们称为对象的内部状态,而那些变化的数据就不缓存了,把它们称为对象的外部状态
。
这样在实现的时候,把内部状态分离出来共享,称之为享元,经过共享享元对象来减小对内存的占用
。把外部状态分离出来,放到外部,让应用在使用的时候进行维护,并在须要的时候传递给享元对象使用
。为了控制对内部状态的共享,而且让外部能简单的使用共享数据,提供一个工厂来管理享元,把它称为享元工厂
。
##2.2 模式结构和说明## 享元模式的结构如图20.1所示:
Flyweight:享元接口
,经过这个接口flyweight能够接受并做用于外部状态。经过这个接口传入外部的状态,在享元对象的方法处理中可能会使用这些外部的数据。
ConcreteFlyweight:具体的享元实现对象
,必须是可共享的,须要封装flyweight的内部状态。
UnsharedConcreteFlyweight:非共享的享元实现对象
,并非全部的Flyweight实现对象都须要共享。非共享的享元实现对象一般是对共享享元对象的组合对象。
FlyweightFactory:享元工厂
,主要用来建立并管理共享的享元对象,并对外提供访问共享享元的接口。
Client:享元客户端
,主要的工做是维持一个对flyweight的引用,计算或存储享元对象的外部状态,固然这里能够访问共享和不共享的flyweight对象。
##2.3 享元模式示例代码##
/*** * 享元接口,经过这个接口享元能够接受并做用于外部状态 */ public interface Flyweight { /** * 示例操做,传入外部状态 * @param extrinsicState 示例参数,外部状态 */ public void operation(String extrinsicState); }
/** * 享元对象 */ 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) { // 具体的功能处理 } }
客户端不能直接建立共享的享元对象实例,必须经过享元工厂来建立
。接下来看看享元工厂的实现,示例代码以下:/** * 享元工厂 */ 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; } }
/** * Client对象,一般会维持一个对flyweight的引用, * 计算或存储一个或多个flyweight的外部状态 */ public class Client { //具体的功能处理 }
##2.4 使用享元模式重写示例## 再次分析上面的受权信息,实际上重复出现的数据主要是对安全实体和权限的描述,又考虑到安全实体和权限的描述通常是不分开的,那么找出这些重复的描述
,好比:人员列表的查看权限。并且这些重复的数据是能够重用的,好比给它们配上不一样的人员,就能够组合成为不一样的受权描述
,如图20.2所示:
图20.2就能够描述以下的信息:
张三 对 人员列表 拥有 查看的权限 李四 对 人员列表 拥有 查看的权限 王五 对 人员列表 拥有 查看的权限
很明显,能够把安全实体和权限的描述定义成为享元,而和它们结合的人员数据,就能够作为享元的外部数据。为了演示简单,就把安全实体对象和权限对象简化成了字符串,描述一下它们的名字。
也为了系统的扩展性和灵活性,给享元定义一个接口,外部使用享元仍是面向接口来编程
,示例代码以下:/*** * 描述受权数据的享元接口 */ public interface Flyweight { /** * 判断传入的安全实体和权限,是否和享元对象内部状态匹配 * @param securityEntity 安全实体 * @param permit 权限 * @return true表示匹配,false表示不匹配 */ public boolean match(String securityEntity,String permit); }
该来实现享元对象了,这个对象须要封装受权数据中重复出现部分的数据
,示例代码以下:/** * 封装受权数据中重复出现部分的享元对象 */ 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; } }
提供享元工厂来负责享元对象的共享管理和对外提供访问享元的接口
。享元工厂通常不须要不少个,实现成为单例便可
。享元工厂负责享元对象的建立和管理,基本的思路就是在享元工厂里面缓存享元对象
。在Java中最经常使用的缓存实现方式,就是定义一个Map来存放缓存的数据,而享元工厂对外提供的访问享元的接口,基本上就是根据key值到缓存的Map中获取相应的数据,这样只要有了共享,同一份数据就能够重复使用了,示例代码以下:
/** * 享元工厂,一般实现成为单例 */ public class FlyweightFactory { private static FlyweightFactory factory = new FlyweightFactory(); private FlyweightFactory(){ } public static FlyweightFactory getInstance(){ return factory; } /** * 缓存多个flyweight对象 */ private Map<String,Flyweight> fsMap = new HashMap<String,Flyweight>(); /** * 获取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; } }
实现完享元工厂,该来看看如何使用享元对象了。按照前面的实现,须要一个对象来提供安全管理的业务功能,就是前面的那个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 Map<String,Collection<Flyweight>> map = new HashMap<String,Collection<Flyweight>>(); /** * 模拟登陆的功能 * @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){ Collection<Flyweight> col = new ArrayList<Flyweight>(); 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; } }
所用到的TestDB没有任何变化,这里就不去赘述了。
客户端测试代码也没有任何变化,也不去赘述了。
运行测试一下,看看效果,主要是看看是否是能有效地减小那些重复数据对象的数量。运行结果以下:
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是同一个值,根据咱们的实现,能够判定这是同一个对象
。也就是说,如今只有两个对象实例,而前面的实现中有六个对象实例。
如同示例的那样,对于封装安全实体和权限的这些细粒度对象,既是受权分配的单元对象,也是权限检测的单元对象
。可能有不少人对某个安全实体拥有某个权限,若是为每一个人都从新建立一个对象来描述对应的安全实体和权限,那样就太浪费内存空间了。
经过共享封装了安全实体和权限的对象,不管多少人拥有这个权限,实际的对象实例都是只有一个
,这样既减小了对象的数目,又节省了宝贵的内存空间,从而解决了前面提出的问题。
#3 模式讲解# ##3.1 认识享元模式##
享元模式设计的重点就在于分离变与不变,把一个对象的状态分红内部状态和外部状态,内部状态是不变的,外部状态是可变的
。而后经过共享不变的部分,达到减小对象数量、并节约内存的目的。在享元对象须要的时候,能够从外部传入外部状态给共享的对象,共享对象会在功能处理的时候,使用本身内部的状态和这些外部的状态。
事实上,分离变与不变是软件设计上最基本的方式之一
,好比预留接口,为何在这个地方要预留接口,一个常见的缘由就是这里存在变化,可能在从此须要扩展、或者是改变已有的实现,所以预留接口作为“可插入性的保证”。
在享元模式中,享元对象又有共享与不共享之分,这种状况一般出如今跟组合模式合用的状况,一般共享的是叶子对象,通常不共享的部分是由共享部分组合而成的
,因为全部细粒度的叶子对象都已经缓存了,那么缓存组合对象就没有什么意义了。这个在后面给你们一个示例。
享元模式的内部状态,一般指的是包含在享元对象内部的、对象自己的状态,一般是独立于使用享元的场景的信息
,通常建立事后就再也不变化的状态,所以能够共享。
外部状态指的是享元对象以外的状态,取决于使用享元的场景,会根据使用场景而变化,所以不可共享
。若是享元对象须要这些外部状态的话,能够从外部传递到享元对象里面,好比经过方法的参数来传递。
也就是说享元模式真正缓存和共享的数据是享元的内部状态,而外部状态是不该该被缓存共享的
。
另一点,内部状态和外部状态是独立的,外部状态的变化不该该影响到内部状态。
在享元模式中,为了建立和管理共享的享元部分,引入了享元工厂,享元工厂中通常都包含有享元对象的实例池,享元对象就是缓存在这个实例池中的
。
简单介绍一点实例池的知识,所谓实例池,指的是缓存和管理对象实例的程序,一般实例池会提供对象实例的运行环境,并控制对象实例的生命周期
。
工业级的实例池实现上有两个最基本的难点,一个就是动态控制实例数量
,一个就是动态分配实例来提供给外部使用
。这些都是须要算法来作保证的。
假如实例池里面已有了3个实例,可是客户端请求很是多,有些忙不过来,那么实例池的管理程序就应该判断出来,到底几个实例才能知足如今的客户需求,理想情况是刚恰好,就是既可以知足应用的须要,又不会形成对象实例的浪费,假如通过判断5个实例正好,那么实例池的管理程序就应该能动态的建立2个新的实例。
这样运行了一段时间,客户端的请求减小了,这个时候实例池的管理程序又应该动态的判断,究竟几个实例是最好的,多了明显浪费资源,假如通过判断只须要1个实例就能够了,那么实例池的管理程序应该销毁掉多余的4个实例,以释放资源。这就是动态控制实例数量。
对于动态分配实例,也说明一下吧,假如实例池里面有3个实例,这个时候来了一个新的请求,到底调度哪个实例去执行客户的请求呢,若是有空闲实例,那就是它了,要是没有空闲实例呢,是新建一个实例,仍是等待运行中的实例,等它运行完了就来处理这个请求呢?具体如何调度,也是须要算法来保障的。
回到享元模式中来,享元工厂中的实例池可没有这么复杂,由于共享的享元对象基本上都是一个实例,通常不会出现同一个享元对象有多个实例的状况
,这样就不用去考虑动态建立和销毁享元对象实例的功能;另外只有一个实例,也就不存在动态调度的麻烦,反正就是它了
。
这也主要是由于享元对象封装的多半是对象的内部状态,这些状态一般是不变的,有一个实例就够了,不须要动态控制生命周期,也不须要动态调度,它只须要作一个缓存而已,没有上升到真正的实例池那么个高度。
享元模式的使用上,有两种状况,一种是没有“不须要共享”的享元对象
,就如同前面的示例那样,只有共享享元对象的状况;还有一种是既有共享享元对象,又有不须要共享的享元对象的状况
,这种状况后面再示例。
这里看看只有共享享元对象的状况下,享元模式的调用顺序,如图20.3所示:
在享元模式中,一般是在第一次向享元工厂请求获取共享对象的时候,进行共享对象的初始化,并且多半都是在享元工厂内部实现,不会从外部传入共享对象
。固然能够从外部传入一些建立共享对象须要的值,享元工厂能够按照这些值去初始化须要共享的对象,而后就把建立好的共享对象的实例放入享元工厂内部的缓存中,之后再请求这个共享对象的时候就不用再建立了。
##3.2 不须要共享的享元实现## 可能有些朋友看到这个标题会很疑惑,享元不就是要共享的对象吗?不共享,叫什么享元啊?
确实有不须要共享的享元实现,这种状况多出如今组合结构中,对于使用已经缓存的享元组合出来的对象,就没有必要再缓存了,也就是把已经缓存的享元当作叶子结点,组合出来的组合对象就不须要再被缓存了。也把这种享元称为复合享元
。
好比上面的权限描述,若是出现组合权限描述,在这个组合对象里面包含不少个共享的权限描述,那么这个组合对象就不用缓存了,这个组合对象的存在只是为了在受权的时候更加方便
。
具体点说吧,好比要给某人分配“薪资数据”这个安全实体的“修改”权限,那么必定会把“薪资数据”的“查看权限”也分配给这我的,若是按照前面的作法,这就须要分配两个对象,为了方便,干脆把这两个描述组合起来,打包成一个对象,命名成为“操做薪资数据”,那么分配权限的时候,能够这么描述:
把 “操做薪资数据” 分配给 张三
这句话的意思就至关于
把 “薪资数据” 的 “查看”权限 分配给 张三 把 “薪资数据” 的 “修改”权限 分配给 张三
这样一来,“操做薪资数据”就至关因而一个不须要共享的享元
,它实际由享元“薪资数据 的 查看 权限”,和享元“薪资数据 的 修改 权限”这两个享元组合而成,所以“操做薪资数据”自己也就不须要再共享了。
这样分配权限的时候就会简单一点。
可是这种组合对象,在权限系统中通常不用于验证
,也就是说验证的时候仍是一个一个进行判断,由于在存储受权信息的时候是一条一条存储的
。但也不排除有些时候始终要检查多个权限,干脆把这些权限打包,而后直接验证是否有这个组合权限,只是这种状况应用得比较少而已。
仍是用示例来讲明吧,在上面已经实现的系统里面添加不须要共享的享元实现。此时系统结构如图20.4所示:
/*** * 描述受权数据的享元接口 */ public interface Flyweight { /** * 判断传入的安全实体和权限,是否和享元对象内部状态匹配 * @param securityEntity 安全实体 * @param permit 权限 * @return true表示匹配,false表示不匹配 */ public boolean match(String securityEntity,String permit); /** * 为flyweight添加子flyweight对象 * @param f 被添加的子flyweight对象 */ public void add(Flyweight f); }
所以在叶子对象里面抛出不支持的例外就行了
,示例代码以下:/** * 封装受权数据中重复出现部分的享元对象 */ public class AuthorizationFlyweight implements Flyweight{ public void add(Flyweight f) { throw new UnsupportedOperationException("对象不支持这个功能"); } }
其实就是组合共享享元对象的对象
,这个组合对象中,须要保存全部的子对象,另外它在实现match方法的时候,是经过递归的方式,在整个组合结构中进行匹配。示例代码以下:/** * 不须要共享的享元对象的实现,也是组合模式中的组合对象 */ public class UnsharedConcreteFlyweight implements Flyweight{ /** * 记录每一个组合对象所包含的子组件 */ private List<Flyweight> list = new ArrayList<Flyweight>(); public void add(Flyweight f) { list.add(f); } public boolean match(String securityEntity, String permit) { for(Flyweight f : list){ //递归调用 if(f.match(securityEntity, permit)){ return true; } } return false; } }
首先是受权数据要区分是单条的受权,仍是组合的受权,这个在每条受权数据后面添加一个标识来描述
而后增长一个描述组合数据的记录,使用一个Map来存放
具体的示例代码以下:
/** * 供测试用,在内存中模拟数据库中的值 */ public class TestDB { /** * 用来存放单独受权数据的值 */ public static Collection<String> colDB = new ArrayList<String>(); /** * 用来存放组合受权数据的值, * key为组合数据的id,value为该组合包含的多条受权数据的值 */ public static Map<String,String[]> mapDB = new HashMap<String,String[]>(); static{ //经过静态块来填充模拟的数据,增长一个标识来代表是否组合受权数据 colDB.add("张三,人员列表,查看,1"); colDB.add("李四,人员列表,查看,1"); colDB.add("李四,操做薪资数据,,2"); mapDB.put("操做薪资数据",new String[]{"薪资数据,查看","薪资数据,修改"}); //增长更多的受权数据 for(int i=0;i<3;i++){ colDB.add("张三"+i+",人员列表,查看,1"); } } }
享元工厂不须要变化,这里就不去赘述了
接下来该实现安全管理的类了,这个类至关于享元模式的Client角色,此次在这个类里面,不单纯使用共享的享元对象,它还会使用不须要共享的享元对象
主要的变化集中在queryByUser方法里面,本来只是经过享元工厂来获取共享的享元对象便可,可是此次还须要在这里建立不须要共享的享元对象
。示例代码以下:
public class SecurityMgr { private static SecurityMgr securityMgr = new SecurityMgr(); private SecurityMgr(){ } public static SecurityMgr getInstance(){ return securityMgr; } /** * 在运行期间,用来存放登陆人员对应的权限, * 在Web应用中,这些数据一般会存放到session中 */ private Map<String,Collection<Flyweight>> map = new HashMap<String,Collection<Flyweight>>(); /** * 模拟登陆的功能 * @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); System.out.println("如今测试"+securityEntity+"的"+permit+"权限,map.size="+map.size()); 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){ Collection<Flyweight> col = new ArrayList<Flyweight>(); for(String s : TestDB.colDB){ String ss[] = s.split(","); if(ss[0].equals(user)){ Flyweight fm = null; if(ss[3].equals("2")){ //表示是组合 fm = new UnsharedConcreteFlyweight(); //获取须要组合的数据 String tempSs[] = TestDB.mapDB.get(ss[1]); for(String tempS : tempSs){ Flyweight tempFm = FlyweightFactory.getInstance().getFlyweight(tempS); //把这个对象加入到组合对象中 fm.add(tempFm); } }else{ fm = FlyweightFactory.getInstance().getFlyweight(ss[1]+","+ss[2]); } col.add(fm); } } return col; } }
public class Client { public static void main(String[] args) throws Exception{ //须要先登陆,而后再判断是否有权限 SecurityMgr mgr = SecurityMgr.getInstance(); mgr.login("张三"); mgr.login("李四"); boolean f1 = mgr.hasPermit("张三","薪资数据","查看"); boolean f2 = mgr.hasPermit("李四","薪资数据","查看"); boolean f3 = mgr.hasPermit("李四","薪资数据","修改"); System.out.println("f1=="+f1); System.out.println("f2=="+f2); System.out.println("f3=="+f3); for(int i=0;i<3;i++){ mgr.login("张三"+i); mgr.hasPermit("张三"+i,"薪资数据","查看"); } } }
能够运行测试一下,看看效果,结果示例以下:
如今测试薪资数据的查看权限,map.size=2 fm==cn.javass.dp.flyweight.example4.AuthorizationFlyweight@12dacd1 如今测试薪资数据的查看权限,map.size=2 fm==cn.javass.dp.flyweight.example4.AuthorizationFlyweight@12dacd1 fm==cn.javass.dp.flyweight.example4.UnsharedConcreteFlyweight@1ad086a 如今测试薪资数据的修改权限,map.size=2 fm==cn.javass.dp.flyweight.example4.AuthorizationFlyweight@12dacd1 fm==cn.javass.dp.flyweight.example4.UnsharedConcreteFlyweight@1ad086a f1==false f2==true f3==true 如今测试薪资数据的查看权限,map.size=3 fm==cn.javass.dp.flyweight.example4.AuthorizationFlyweight@12dacd1 如今测试薪资数据的查看权限,map.size=4 fm==cn.javass.dp.flyweight.example4.AuthorizationFlyweight@12dacd1 如今测试薪资数据的查看权限,map.size=5 fm==cn.javass.dp.flyweight.example4.AuthorizationFlyweight@12dacd1
##3.3 对享元对象的管理## 虽然享元模式对于共享的享元对象实例的管理要求,没有实例池对实例管理的要求那么高,可是也仍是有不少自身的特色功能,好比:引用计数、垃圾清除等。所谓垃圾,就是在缓存中存在,可是再也不须要被使用的缓存中的对象
。
所谓引用计数,就是享元工厂可以记录每一个享元被使用的次数;而垃圾清除,则是大多数缓存管理都有的功能,缓存不能只往里面放数据,在不须要这些数据的时候,应该把这些数据从缓存中清除,释放相应的内存空间,以节省资源
。
在前面的示例中,共享的享元对象是不少人共享的,基本上能够一直存在于系统中,不用清除。可是垃圾清除是享元对象管理的一个很常见功能,仍是经过示例给你们讲一下,看看如何实现这些常见的功能。
要实现引用计数,就在享元工厂里面定义一个Map,它的key值跟缓存享元对象的key是同样的,而value就是被引用的次数,这样当外部每次获取该享元的时候,就把对应的引用计数取出来加上1,而后再记录回去。
要实现垃圾回收就比较麻烦点,首先要能肯定哪些是垃圾?其次是什么时候回收?还有由谁来回收?如何回收?解决了这些问题,也就能实现垃圾回收了。
为了肯定哪些是垃圾,一个简单的方案是这样的,定义一个缓存对象的配置对象,在这个对象中描述了缓存的开始时间和最长不被使用的时间
,这个时候判断是垃圾的计算公式以下:当前的时间 - 缓存的开始时间 >= 最长不被使用的时间。固然,每次这个对象被使用的时候,就把那个缓存开始的时间更新为使用时的当前时间,也就是说若是一直有人用的话,这个对象是不会被判断为垃圾的。
什么时候回收的问题,固然是判断出来是垃圾了就能够回收了。
关键是谁来判断垃圾,还有谁来回收垃圾的问题。一个简单的方案是定义一个内部的线程,这个线程在享元工厂被建立的时候就启动运行
。由这个线程每隔必定的时间来循环缓存中全部对象的缓存配置,看看是不是垃圾,若是是垃圾,那就能够启动回收了。
怎么回收呢?这个比较简单,就是直接从缓存的map对象中删除掉相应的对象,让这些对象没有引用的地方,那么这些对象就能够等着被虚拟机的垃圾回收来回收掉了
。
(1)分析了这么多,仍是看代码示例会比较清楚,先看缓存配置对象,示例代码以下:
/** * 描述享元对象缓存的配置对象 */ public class CacheConfModel{ /** * 缓存开始计时的开始时间 */ private long beginTime; /** * 缓存对象存放的持续时间,实际上是最长不被使用的时间 */ private double durableTime; /** * 缓存对象须要被永久存储,也就是不须要从缓存中删除 */ private boolean forever; public boolean isForever() { return forever; } public void setForever(boolean forever) { this.forever = forever; } public long getBeginTime() { return beginTime; } public void setBeginTime(long beginTime) { this.beginTime = beginTime; } public double getDurableTime() { return durableTime; } public void setDurableTime(double durableTime) { this.durableTime = durableTime; } }
(2)对享元对象的管理的工做,是由享元工厂来完成的,所以上面的功能,也集中在享元工厂里面来实现,在上一个例子的基础之上,来实现这些功能,改进后的享元工厂相对而言稍复杂一点,大体有以下改变:
添加一个Map,来缓存被共享对象的缓存配置的数据;
添加一个Map,来记录缓存对象被引用的次数;
为了测试方便,定义了一个常量来描述缓存的持续时间;
提供获取某个享元被使用的次数的方法;
在获取享元的对象里面,就要设置相应的引用计数和缓存设置了,示例采用的是内部默认设置一个缓存设置,其实也能够改造一下获取享元的方法,从外部传入缓存设置的数据;
提供一个清除缓存的线程,实现判断缓存数据是否已是垃圾了,若是是,那就把它从缓存中清除掉;
基本上从新实现了享元工厂,示例代码以下:
/** * 享元工厂,一般实现成为单例 * 加入实现垃圾回收和引用计数的功能 */ public class FlyweightFactory { private static FlyweightFactory factory = new FlyweightFactory(); private FlyweightFactory(){ //启动清除缓存值的线程 Thread t = new ClearCache(); t.start(); } public static FlyweightFactory getInstance(){ return factory; } /** * 缓存多个flyweight对象 */ private Map<String,Flyweight> fsMap = new HashMap<String,Flyweight>(); /** * 用来缓存被共享对象的缓存配置,key值和上面map的同样 */ private Map<String,CacheConfModel> cacheConfMap = new HashMap<String,CacheConfModel>(); /** * 用来记录缓存对象被引用的次数,key值和上面map的同样 */ private Map<String,Integer> countMap = new HashMap<String,Integer>(); /** * 默认保存6秒钟,主要为了测试方便,这个时间能够根据应用的要求设置 */ private final long DURABLE_TIME = 6*1000L; /** * 获取某个享元被使用的次数 * @param key 享元的key * @return 被使用的次数 */ public synchronized int getUseTimes(String key){ Integer count = countMap.get(key); if(count==null){ count = 0; } return count; } /** * 获取key对应的享元对象 * @param key 获取享元对象的key * @return key对应的享元对象 */ public synchronized Flyweight getFlyweight(String key) { Flyweight f = fsMap.get(key); if(f==null){ f = new AuthorizationFlyweight(key); fsMap.put(key,f); //同时设置引用计数 countMap.put(key, 1); //同时设置缓存配置数据 CacheConfModel cm = new CacheConfModel(); cm.setBeginTime(System.currentTimeMillis()); cm.setForever(false); cm.setDurableTime(DURABLE_TIME); cacheConfMap.put(key, cm); }else{ //表示还在使用,那么应该从新设置缓存配置 CacheConfModel cm = cacheConfMap.get(key); cm.setBeginTime(System.currentTimeMillis()); //设置回去 this.cacheConfMap.put(key, cm); //同时计数加1 Integer count = countMap.get(key); count++; countMap.put(key, count); } return f; } /** * 删除key对应的享元对象,连带清除对应的缓存配置和引用次数的记录,不对外 * @param key 要删除的享元对象的key */ private synchronized void removeFlyweight(String key){ this.fsMap.remove(key); this.cacheConfMap.remove(key); this.countMap.remove(key); } /** * 维护清除缓存的线程,内部使用 */ private class ClearCache extends Thread{ public void run(){ while(true){ Set<String> tempSet = new HashSet<String>(); Set<String> set = cacheConfMap.keySet(); for(String key : set){ CacheConfModel ccm = cacheConfMap.get(key); //比较是否须要清除 if((System.currentTimeMillis() - ccm.getBeginTime()) >= ccm.getDurableTime()){ //能够清除,先记录下来 tempSet.add(key); } } //真正清除 for(String key : tempSet){ FlyweightFactory.getInstance().removeFlyweight(key); } System.out.println("now thread="+fsMap.size() +",fsMap=="+fsMap.keySet()); //休息1秒再从新判断 try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
注意:getUseTimes、removeFlyweight和getFlyweight这几个方法是加了同步的,缘由是在多线程环境下使用它们,容易出现并发错误,好比一个线程在获取享元对象,而另外一个线程在删除这个缓存对象
。
(3)要想看出引用计数的效果来,SecurityMgr须要进行一点修改,至少不要再缓存数据了,要直接从享元工厂中获取数据,不然就没有办法准确引用计数了,大体改变以下:
去掉了放置登陆人员对应权限数据的缓存;
不须要实现登陆功能,在这个示意程序里面,登陆方法已经不用实现任何功能,所以直接去掉;
原来经过map获取值的地方,直接经过queryByUser获取就行了;
示例代码以下:
public class SecurityMgr { private static SecurityMgr securityMgr = new SecurityMgr(); private SecurityMgr(){ } public static SecurityMgr getInstance(){ return securityMgr; } /** * 判断某用户对某个安全实体是否拥有某权限 * @param user 被检测权限的用户 * @param securityEntity 安全实体 * @param permit 权限 * @return true表示拥有相应权限,false表示没有相应权限 */ public boolean hasPermit(String user,String securityEntity,String permit){ Collection<Flyweight> col = this.queryByUser(user); if(col==null || col.size()==0){ System.out.println(user+"没有登陆或是没有被分配任何权限"); return false; } for(Flyweight fm : col){ if(fm.match(securityEntity, permit)){ return true; } } return false; } /** * 从数据库中获取某人所拥有的权限 * @param user 须要获取所拥有的权限的人员 * @return 某人所拥有的权限 */ private Collection<Flyweight> queryByUser(String user){ Collection<Flyweight> col = new ArrayList<Flyweight>(); for(String s : TestDB.colDB){ String ss[] = s.split(","); if(ss[0].equals(user)){ Flyweight fm = null; if(ss[3].equals("2")){ //表示是组合 fm = new UnsharedConcreteFlyweight(); //获取须要组合的数据 String tempSs[] = TestDB.mapDB.get(ss[1]); for(String tempS : tempSs){ Flyweight tempFm = FlyweightFactory.getInstance().getFlyweight(tempS); //把这个对象加入到组合对象中 fm.add(tempFm); } }else{ fm = FlyweightFactory.getInstance().getFlyweight(ss[1]+","+ss[2]); } col.add(fm); } } return col; } }
(4)仍是写个客户端来试试看,上面的享元工厂可否实现对享元对象的管理,尤为是对于垃圾回收和计数方面的功能
,对于垃圾回收的功能不须要新加任何的测试代码,而对于引用计数的功能,须要写代码来调用才能看到效果,示例代码以下:
public class Client { public static void main(String[] args) throws Exception{ SecurityMgr mgr = SecurityMgr.getInstance(); boolean f1 = mgr.hasPermit("张三","薪资数据","查看"); boolean f2 = mgr.hasPermit("李四","薪资数据","查看"); boolean f3 = mgr.hasPermit("李四","薪资数据","修改"); for(int i=0;i<3;i++){ mgr.hasPermit("张三"+i,"薪资数据","查看"); } //特别提醒:这里查看的引用次数,不是指测试使用的次数,指的是 //SecurityMgr的queryByUser方法经过享元工厂去获取享元对象的次数 System.out.println("薪资数据,查看 被引用了"+FlyweightFactory.getInstance().getUseTimes("薪资数据,查看")+"次"); System.out.println("薪资数据,修改 被引用了"+FlyweightFactory.getInstance().getUseTimes("薪资数据,修改")+"次"); System.out.println("人员列表,查看 被引用了"+FlyweightFactory.getInstance().getUseTimes("人员列表,查看")+"次"); } }
进行缓存的垃圾回收功能的是个线程在运行,因此你不终止该线程运行,程序会一直运行下去,运行部分结果以下:
薪资数据,查看 被引用了2次 薪资数据,修改 被引用了2次 人员列表,查看 被引用了6次 now thread=3,fsMap==[人员列表,查看, 薪资数据,查看, 薪资数据,修改] now thread=3,fsMap==[人员列表,查看, 薪资数据,查看, 薪资数据,修改] now thread=3,fsMap==[人员列表,查看, 薪资数据,查看, 薪资数据,修改] now thread=3,fsMap==[人员列表,查看, 薪资数据,查看, 薪资数据,修改] now thread=3,fsMap==[人员列表,查看, 薪资数据,查看, 薪资数据,修改] now thread=3,fsMap==[人员列表,查看, 薪资数据,查看, 薪资数据,修改] now thread=0,fsMap==[] now thread=0,fsMap==[]
##3.4 享元模式的优缺点##
可能有的朋友认为共享对象会浪费空间,可是若是这些对象频繁使用,那么实际上是节省空间的
。由于占用空间的大小等于每一个对象实例占用的大小再乘以数量,对于享元对象来说,基本上就只有一个实例,大大减小了享元对象的数量,并节省很多的内存空间
。
节省的空间取决于如下几个因素:由于共享而减小的实例数目、每一个实例自己所占用的空间。假如每一个对象实例占用2个字节,若是不共享数量是100个,而共享事后就只有一个了,那么节省的空间约等于:(100-1) X 2 字节。
如同前面演示的享元工厂,在维护共享对象的时候,若是功能复杂,会有不少额外的开销
,好比有一个线程来维护垃圾回收。
##3.5 思考享元模式##
享元模式的本质:分离与共享
。
分离的是对象状态中变与不变的部分,共享的是对象中不变的部分
。享元模式的关键之处就在于分离变与不变,把不变的部分做为享元对象的内部状态,而变化部分就做为外部状态,由外部来维护,这样享元对象就可以被共享,从而减小对象数量,并节省大量的内存空间。
理解了这个本质后,在使用享元模式的时候,就会去考虑,哪些状态须要分离?如何分离?分离后如何处理?哪些须要共享?如何管理共享的对象?外部如何使用共享的享元对象?是否须要不共享的对象?等等问题。
把这些问题都思考清楚,找到相应的解决方法,那么享元模式也就应用起来了,多是标准的应用,也多是变形的应用,但万变不离其宗。
建议在以下状况中,选用享元模式:
若是一个应用程序使用了大量的细粒度对象,可使用享元模式来减小对象数量;
若是因为使用大量的对象,形成很大的存储开销,可使用享元模式来减小对象数量,并节约内存;
若是对象的大多数状态均可以转变为外部状态,好比经过计算获得,或是从外部传入等,可使用享元模式来实现内部状态和外部状态的分离;
若是不考虑对象的外部状态,能够用相对较少的共享对象取代不少组合对象,可使用享元模式来共享对象,而后组合对象来使用这些共享对象;
##3.6 相关模式##
这两个模式能够组合使用。
一般状况下,享元模式中的享元工厂能够实现成为单例
。另外,享元工厂里面缓存的享元对象,都是单实例的,能够当作是单例模式的一种变形控制
,在享元工厂里面来单例享元对象。
这两个模式能够组合使用。
在享元模式里面,存在不须要共享的享元实现,这些不须要共享的享元一般是对共享的享元对象的组合对象,也就是说,享元模式一般会和组合模式组合使用,来实现更复杂的对象层次结构
。
这两个模式能够组合使用。
可使用享元模式来共享状态模式中的状态对象,一般在状态模式中,会存在数量很大的、细粒度的状态对象,并且它们基本上都是能够重复使用的,都是用来处理某一个固定的状态的
,它们须要的数据一般都是由上下文传入,也就是变化部分都分离出去了,因此能够用享元模式来实现这些状态对象。
这两个模式能够组合使用。
可使用享元模式来实现策略模式中的策略对象,跟状态模式同样,在策略模式中也存在大量细粒度的策略对象
,它们须要的数据一样是从上下文传入的,因此可使用享元模式来实现这些策略对象。