设计模式之(十二)享元模式(Flyweight)
享元模式思想
就以小时候导锅来说吧(导锅是我家乡的方言,就是用细沙把一个模型锅的形状拓下来,然后把铝水倒进模型中,就导好一个锅了。小时候很喜欢看的,可惜现在看不到了。上个图片回忆下)了解了这个过程后就知道,沙子、模型锅、都是可以一直使用的。而用做化铝水的物品是需要不同铝制品东西化生成。做这个生意的人不可能用一次沙子和模型锅就因为用过不用了。
在代码的世界里,那些不变的类属性就像这些沙子和模型锅可以共享使用,而变化的类属性就像为导锅化铝水的铝制品一样是不同变化的。但实际上很多程序员就没有共享那些不变属性,而是生成一个类,就新建一个这样的可共享属性。就如同导锅用了一次的锅和沙子就弃之不用了,下次再用新的一样,多么可笑的事情啊,而享元模式就是为了阻止我们干这样可笑的蠢事而生的。
来看下共享模式的定义:运用共享技术有效地支持大量细粒度的对象,达到节省内存的目的。咋一看不太明白这句话的意思,其实就是本文开篇描述的那个意思。
享元模式结构
模式参与成员:
Flyweight:享元抽象接口
FlyweightFactory:管理共享享元对象的缓存池。
ConcreteFlyweight:可共享的类,里面存放着都是不会随着场景变化而变化的状态,同时介绍不可共享的状态的传入。
Client:模拟试用享元对象的地方(对享元模式来说就是客户端)
例子
为了更好的来理解享元模式,我们来举个例子。就以网店邮寄快递这个场景为例子展示此模式。
一个快递对象都有哪些属性呢,来列举下。快递物品名称,价格,邮寄人,邮寄人电话,邮寄人地址,接收人姓名,接收人电话,接收人地址。既然要用享元模式了就要根据此模式的特征来分析一下,可共享(不变)状态和不可共享(随着不同包裹变化的)状态。
不变的状态有:邮寄人,邮寄人电话,邮寄人地址。我们把相同的物品共享一个对象的话,还有物品名称、价格。
不可共享的状态:接收人姓名,接收人电话,接收人地址。
分好这变化状态和不变状态以后,接下来就构建代码了。
享元抽象接口
1 //抽象接口,相当于 Flyweight 2 public interface KuaiDi { 3 // jsr 接受不可共享部分 4 public void sendkd(Jiesr jsr); 5 }
实现接口的可共享类
1 public class ConcreteKuaidi implements KuaiDi { 2 3 private String kdName; 4 private String fsrName; 5 private String fsrPhone; 6 private String fsrAddress; 7 8 public ConcreteKuaidi(String name,String fsrName,String fsrdh,String fsradrress){ 9 this.kdName = name; 10 this.fsrName = fsrName; 11 this.fsrPhone = fsrdh; 12 this.fsrAddress = fsradrress; 13 } 14 15 /* getter 和 setter 方法省略了 */ 16 17 @Override 18 public void sendkd(Jiesr jsr) { 19 // TODO Auto-generated method stub 20 String msg = "邮寄"+ kdName +"给"+jsr.getJsrName()+"\n"; 21 msg = msg + "接受人电话:"+jsr.getJsrPhone()+"\n"; 22 msg = msg + "接受人地址:"+jsr.getAddress()+"\n"; 23 msg = msg + "发送地址:"+fsrAddress+"\n"; 24 System.out.println(msg); 25 // 打印类对象地址,更好的看出来类共享 26 System.out.println(this.toString()); 27 System.out.println("------------------------------------"); 28 } 29 30 }
不可共享部分的类, 快递接收人
public class Jiesr { private String jsrName; private String jsrPhone; private String Address; public String getJsrName() { return jsrName; } public String getJsrPhone() { return jsrPhone; } public String getAddress() { return Address; } public Jiesr(String jsrName,String jsrPhone,String Address){ this.jsrName = jsrName; this.jsrPhone = jsrPhone; this.Address = Address; } }
享元工厂,实现成为单例模式,是共享以前生成的对象,还是需要新建一个对象,由这儿决定。
1 public class KudiFactory { 2 private static KudiFactory kudiFactory = new KudiFactory(); 3 4 private KudiFactory(){} 5 6 public static KudiFactory getInstance(){ 7 return kudiFactory; 8 } 9 //用于存储 对象 10 private Map<String,KuaiDi> map = new HashMap<String,KuaiDi>(); 11 12 // 类似工厂,存有对象池,在这儿生成对象 13 public KuaiDi getKuaidi(String name,String fsrName,String fsrdh,String fsradrress){ 14 KuaiDi kd = map.get(name); 15 if(kd == null){ 16 kd = new ConcreteKuaidi(name,fsrName,fsrdh,fsradrress); 17 map.put(name,kd); 18 } 19 return kd; 20 } 21 }
调用
public class Client { public static void main(String[] args) { KudiFactory kk = KudiFactory.getInstance(); KuaiDi kuaidi1 = kk.getKuaidi("耐克运动鞋", "王小二耐克专营店", "1502369877", "汉东省京州市耐克园区10号"); Jiesr jsr1 = new Jiesr("张小四", "1501236542", "**省**市张小四村"); kuaidi1.sendkd(jsr1); KuaiDi kuaidi2 = kk.getKuaidi("耐克运动库", "王小二耐克专营店", "1502369877", "汉东省京州市耐克园区10号"); Jiesr jsr2 = new Jiesr("王小五", "1602365895", "**省**市张小5村"); kuaidi2.sendkd(jsr2); KuaiDi kuaidi3 = kk.getKuaidi("耐克运动鞋", "王小二耐克专营店", "1502369877", "汉东省京州市耐克园区10号"); Jiesr jsr3 = new Jiesr("李小六", "17045683397", "**省**市张小6村"); kuaidi3.sendkd(jsr3); KuaiDi kuaidi4 = kk.getKuaidi("耐克运动库", "王小二耐克专营店", "1502369877", "汉东省京州市耐克园区10号"); Jiesr jsr4 = new Jiesr("钱小七", "18045683397", "**省**市张小7村"); kuaidi4.sendkd(jsr4); KuaiDi kuaidi5 = kk.getKuaidi("耐克运动鞋", "王小二耐克专营店", "1502369877", "汉东省京州市耐克园区10号"); Jiesr jsr5 = new Jiesr("孙小八", "18645683397", "**省**市张小8村"); kuaidi5.sendkd(jsr5); } } /*-------------------------------------执行结果-------------------------------*/ 邮寄耐克运动鞋给张小四 接受人电话:1501236542 接受人地址:**省**市张小四村 发送地址:汉东省京州市耐克园区10号 example.ConcreteKuaidi@5994a1e9 ------------------------------------ 邮寄耐克运动库给王小五 接受人电话:1602365895 接受人地址:**省**市张小5村 发送地址:汉东省京州市耐克园区10号 example.ConcreteKuaidi@2d11f5f1 ------------------------------------ 邮寄耐克运动鞋给李小六 接受人电话:17045683397 接受人地址:**省**市张小6村 发送地址:汉东省京州市耐克园区10号 example.ConcreteKuaidi@5994a1e9 ------------------------------------ 邮寄耐克运动库给钱小七 接受人电话:18045683397 接受人地址:**省**市张小7村 发送地址:汉东省京州市耐克园区10号 example.ConcreteKuaidi@2d11f5f1 ------------------------------------ 邮寄耐克运动鞋给孙小八 接受人电话:18645683397 接受人地址:**省**市张小8村 发送地址:汉东省京州市耐克园区10号 example.ConcreteKuaidi@5994a1e9 ------------------------------------
分析上述例子。首先看结果,通过打印的对象地址我们可以看出来,虽然邮寄了5个快递,但是由于共享的问题,实际上只是创建了两个对象。运行鞋共享的一个对象。而运动裤共享的一个。由于网点的快递量是很大的。如果这个耐克专卖店就买鞋和裤子,那么理论上鞋共享一个、裤子共享一个对象就够了。这样极大的节省了内存空间的开销。
虽然另外建了一个不可共享的外部类,但是这个外部类都是很小的。共享对象节省的内存远远大于这个外部类占用内存。
享元工厂一定要实现要用单例模式来实现。这样更能节省内存,提交效率。
分析享元模式
享元模式其实是一个很简单的设计模式,他的重点在于分清内部状态、外部状态。内部状态:可共享内容,就是不会变化的内容;外部状态:随着应用场景的变化会变化的状态。内部状态是应用享元模式的基础,一个需要大量产生的对象类,可分离出来的内部状态越多,那么用享元模式的价值就越高;或者共享状态不多,但是可分离成内部状态的属性是一个非常多的内容,占用空间很大,这样使用享元模式的效果也很好。
享元模式的缺点:由于把原来的大对象换成了多个小颗粒对象,而且加入了享元工厂这个类,所以系统变复杂了。
线程安全问题,在设置共享对象的时候一定要注意共享的类是否够场景试用,如果并发太高,而共享的对象太少,就会造成线程安全问题。比如都是访问一个类了。就造成了数据错乱了。
复合享元模式
简单来说,就是享元对象在保存后,某种情景下需要的对象是已存在的两种或以上的不同享元对象组合而成的。这样就不要再去保存这个组合成的享元类了。需要的时候用组合生成即可。
享元模式的理解误区
在开始看享元模式的时候,我对内部状态和外部状态的理解不得其所。总觉得应该把不变的设成外部状态,变化的弄成内部。把分离出去的外部状态聚合到咱们的主对象里。这样在使用类的时候就可以复用不变的外部状态了。认真思考一下这样的设计很明显它没有起到享元模式共享类,节省内容的作用。
在反复看了例子以后才明白了享元模式类结构意图。