一、介绍
享元模式,英文(Flyweight),这个翻译还是比较OK的。网上解释比较多,也比较抽象,用我的话来说这个模式就是一个公共,共享的区域,里面放了一些大家可以共用的对象。因为我们知道,创建对象是需要花费时间,占用内存的,但是有些对象常常不需要那么多,仅仅需要一个,或者多个就足够了,也就是不需要到哪儿使用就开始创建。上面解释有点像单例模式,其实单例也是享元 特殊的一种,都是为了复用对象,减少开销儿存在,至于区别,后面说。
二、实例分析:
2.1 这里用设计模式里面 字母的例子:我们知道英文里面有26的字母,而我们每一个字母都假设是一个对象,那么我们需要写一篇文章,肯定会使用很多字母,不可能new 很多对象吧,内存消耗不起。这时你会怎么做呢?
2.2 再来一个案例:我们现在到餐馆吃饭,,会给你一个菜单,然后通过一个 点菜器,要吃什么菜,选择了之后就传输到后台了,过一会到时候菜就送上来了。当我们后台看到A,B,C三个人,假设都点了鱼香肉丝这个菜的时候,那么我们就可以一次抄起来,3个人都用的一个对象(实际情况有点区别),这里怎么实现的呢?
先简单看看代码设计吧:
a. 首先我们要定义一个行为,点菜的行为,我称为享元行为,也就是说大家都要用的一种行为。
/** * * 设计一个抽象类,规定我们的行为 * */ public abstract class Flyweight { public abstract void dianCai(); }
b. 然后对行为具体化,也就是后台如何炒菜。我们要通过菜名才产生对象,因此要一个属性name,并强制
要求点菜,必须传入name.(不然谁知道你点的什么菜!凭啥产生对象)
/** * * 实现主要的行为,这里我们会得到具体的菜 * */ public class FlyweightImpl extends Flyweight{ // 通过菜名,获取不同的菜(对象) private String name; public FlyweightImpl(String name){ this.name = name; } @Override public void dianCai() { // 这里的行为,假设仅仅是打印 System.out.println("点的菜名是:"+name); } }
c.上面可以根据name产生对象了,但是我们需要知道A B C 三个人,到底点的什么菜呢,是否3个人都点了一样的菜呢?因此我们需要一个集合对象,来保存顾客点的什么菜,从而觉得 是抄一份呢,还是抄多份,相当于判断 都点的不同的菜,产生不同的对象,还是点的相同的菜,只需要产生一个对象。这里肯定根据菜名菜判断啦。
import java.util.HashMap; import java.util.Map; /** * * 炒菜工厂,相当于厨房 * */ public class FlyweightFactory { // 一个简单的单例 private FlyweightFactory(){} public static FlyweightFactory factory = new FlyweightFactory(); // 这个用来统一管理客户点的菜,可以从集合中判断哪些菜,被点过了 private Map<String,FlyweightImpl> map = new HashMap<String, FlyweightImpl>(); // 获得菜的对象,传入用户名字 public FlyweightImpl getCai(String name){ FlyweightImpl cai = null; // 判断是否有人点过了 if(map.containsKey(name)){ // 如果已经有人点了,那么只要炒成一份就行了 cai = map.get(name); }else{ // 如果没有,就再抄一份,并记录下来 map.put(name, cai); } return cai; } // 计算一共抄了好多份菜,产生了好多个对象 public int getSize(){ return map.size(); } }
d.下面进行测试
/** * * 客人点菜 * */ public class Client { public static void main(String[] args) { List<Flyweight> list = new ArrayList<Flyweight>(); // 这假设是用户 a b c d e f在 某个是时间点 ,点的菜 FlyweightFactory chufang = FlyweightFactory.factory; Flyweight a = chufang.getCai("鱼香肉丝"); Flyweight b = chufang.getCai("鱼香肉丝"); Flyweight c = chufang.getCai("鱼香肉丝"); Flyweight d = chufang.getCai("小白菜"); Flyweight e = chufang.getCai("小白菜"); Flyweight f = chufang.getCai("红烧肉"); list.add(a);list.add(b);list.add(c); list.add(d);list.add(e);list.add(f); // 比较对象 System.out.println(a.equals(b)); System.out.println(a.equals(c)); System.out.println(a.equals(e)); System.out.println(d.equals(e)); System.out.println(d.equals(f)); System.out.println("一共产生的对象:"+chufang.getSize()); // for(Flyweight fly : list){ fly.dianCai(); } } }
输出信息:
true true false true false 一共产生的对象:3 上的菜是:鱼香肉丝 上的菜是:鱼香肉丝 上的菜是:鱼香肉丝 上的菜是:小白菜 上的菜是:小白菜 上的菜是:红烧肉
从上面看到,虽然点了 7到菜,但是除开一样的,其实只产生了3个对象。
像字母那个实例,也许有的人会说,可以建立26个 对象,都用单例,那么也是可行的。但是如果像下面这个点菜的模式,不同的菜品,不可能创建不同的对象吧,那就太多了。
三、代码分析
享元模式,主要是为了产生对象,并管理对象,让重复对象提升利用率,减少类存开销。这里有一个很好的例子:“活字印刷术”,相信这个大家想象一下就明白。
享元模式的使用场景,相信也有一定了解了,从代码上来讲:
1、享元模式 要分为内部外部状态。
内部状态:也就是对象是根据 内部的参数而创建或者区分。比如几个引用的对象是否是同一个呢?我们可以通过对象的主键ID 进行比较,也可以通过里面集合属性(name+id) 进行比较,判断 这个对象是否一样,参考对象equals()方法。比如上面例子就是通过菜名字(name) 进行区分,创建对象的时候也是通过name 进行创建。这样的对象创建出来,name 就无法更改了,无法更改的东西,我们就是内部状态,判断该对象是否共享(已经存在),都要经过这个内部状态来判断。
外部状态:就是外部可以改变的,简单来说,假设对象已经创建了,A B C 都拿到这个对象,那么就可以对立面的属性就行改变,因为这个对象是共享的,因此一般都是发生在客户端,拿到对象之后。
上面例子,我们虽然点 鱼香肉丝的 有3个人,但是我想知道是哪些人点了,怎么办呢?我们尝试加一个可以改变的外部属性。
/** * * 设计一个抽象类,规定我们的行为 * */ public abstract class Flyweight { public abstract void dianCai(); // 添加一个 添加名字的方法 public abstract void addPerson(String name); // 获得外部属性的方法 public abstract List getPersons(); }
/** * * 实现主要的行为,这里我们会得到具体的菜 * */ public class FlyweightImpl extends Flyweight{ // 通过菜名,获取不同的菜(对象) private String name; public FlyweightImpl(String name){ this.name = name; } @Override public void dianCai() { // 这里的行为,假设仅仅是打印 System.out.println("上的菜是:"+name); } // 外部属性,存放那些人点了这些菜 private List<String> persons = new ArrayList<String>(); @Override public void addPerson(String name) { persons.add(name); } }
Test FlyweightFactory chufang = FlyweightFactory.factory; Flyweight a = chufang.getCai("鱼香肉丝"); a.addPerson("A"); Flyweight b = chufang.getCai("鱼香肉丝"); b.addPerson("B"); Flyweight c = chufang.getCai("鱼香肉丝"); c.addPerson("C"); System.out.println(Arrays.toString(a.getPersons().toArray()));
从上面可以看出,内部状态负责创建对象,如果需要其他的外部状态,是可以从新定义的。而我们熟悉的连接池 就是使用了享元模式。它根据url,driver,username,password内部属性, 创建一个可以共享的对象,然后创建的对象本身可以 断开连接等其他额外的操作,这些都是获得对象之后,在外部操作的。后面我们可以写一个简单的数据库连接池。
小结:
享元模式 为了减少重复对象,提高对象利用率,减少内存开销存在。
1.首先用抽象类或者接口 指定产生这个对象的行为。如:dianCai();
2.写一个具体的实现类,实现上面的行为,当让也可以存放一些外部状态属性
3.需要一个工厂,用来获得对象,并对产生的对象进行管理
4.客户端利用工厂,和传入的参数,而获得对象,如果有外部状态,并可以改变它。
其他:这个模式一般和 单例 工厂模式一起用。这里没加线程控制,仅仅写了享元模式的过程。单例 模式和享元模式的区别在于,单例仅仅是产生不重复的对象,而享元模式会根据内部状态来产生不重复的对象,更加灵活。