欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

设计模式-享元模式

程序员文章站 2022-05-05 08:23:45
...

一、介绍

       享元模式,英文(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.客户端利用工厂,和传入的参数,而获得对象,如果有外部状态,并可以改变它。

 

其他:这个模式一般和 单例 工厂模式一起用。这里没加线程控制,仅仅写了享元模式的过程。单例 模式和享元模式的区别在于,单例仅仅是产生不重复的对象,而享元模式会根据内部状态来产生不重复的对象,更加灵活。