设计模式学习笔记--享元(Flyweight)模式


写在模式学习之前


       什么是设计模式:在我们进行程序设计时,逐渐形成了一些典型问题和问题的解决方案,这就是软件模式;每一个模式描述了一个在我们程序设计中经常发生的问题,以及该问题的解决方案;当我们碰到模式所描述的问题,就可以直接用相应的解决方法去解决这个问题,这就是设计模式。

       设计模式就是抽象出来的东西,它不是学出来的,是用出来的;或许你根本不知道任何模式,不考虑任何模式,却写着最优秀的代码,即使以“模式专家”的角度来看,都是最佳的设计,不得不说是“最佳的模式实践”,这是因为你积累了很多的实践经验,知道“在什么场合代码应该怎么写”,这本身就是设计模式。

       有人说:“水平没到,学也白学,水平到了,无师自通”。诚然,模式背熟,依然可能写不出好代码,更别说设计出好框架;OOP理解及实践经验到达一定水平,同时也意味着总结了很多好的设计经验,但"无师自通",却也未必尽然,或者可以说,恰恰是在水平和经验的基础上,到了该系统的学习一下“模式”的时候了,学习一下专家总结的结果,印证一下自己的不足,对于提高水平还是很有帮助的。

       本系列的设计模式学习笔记,实际是对于《Java与模式》这本书的学习记录。


享元模式的定义


享元模式以共享的方式高效地支持大量的细粒度对象。

享元对象能做到共享的关键是区分内部状态(Internal State)和外部状态(External State)。

一个内部状态是存储在享元对象内部的,并且是不会随环境改变而有所不同的。因此,一个享元可以具有内部状态并可以共享。

一个外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态必须由客户端保存,并在享元对象创建之后,在需要使用的时候再传入到享元对象内部。

外部状态不可以影响享元对象的内部状态。它们是相互独立的。

分类:

根据所涉及的享元对象的内部表象,享元模式可以分为单纯享元模式和复合享元模式。

应用场景:

享元模式在编辑器系统中大量使用。比如,用来将每一个字母做成一个享元对象,提供多种字体。

在Java语言中,String也使用了享元模式。在JVM内部,String对象是共享的,如果两个String对象所包含的字符串相同,JVM实际只创建了一个String对象提供给两个引用,从而实现了String对象的共享。在堆内存中,这两个String对象的地址;以及栈内存中,这两个String对象引用的地址,都是完全一致的。String的intern()方法给出这个字符串在共享池中的唯一实例。

如下可证:

String a="123";String b = "123";则a==b和a.equals(b)都是成立的;当然如果我们强制new新的堆内存,则不一致,比如String c = new String("123");则a==c是不成立的,但是a.equals(c)是成立的,说明a和c所的代表的值相等,但是a和c指向不同的栈地址空间。


单纯享元模式


结构图




所涉及的角色


(1)抽象享元(Flyweight)角色:此角色是所有的具体享元角色的超类,为这些类规定出需要实现的公共接口。那些需要外部状态的操作可以通过调研商业方法以参数形式传入。

(2)具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内部状态的话,必须负责为内部状态提供存储空间。享元对象的内部状态必须与对象所处的周围环境无关,从而使得享元对象 可以在系统内共享。

(3)享元工厂(FlyweightFactory)角色:负责创建和管理享元角色。享元工厂必须保证享元对象可以被系统适当地共享。当一个客户端对象调用一个享元对象时,享元工厂角色会检查系统中是否已经有一个符合要求的享元对象。如果有,就提供这个已有的享元对象;如果没有,就创建一个享元对象。

(4)客户端(Client)角色: 客户单角色需要维护一个对所有享元对象的引用,需要自行存储所有享元对象的外部状态。


代码实现

import java.util.*;
abstract class Flyweight
{
	//一个示意性的方法,参数state是外部状态
	public abstract void operation(String state);
}
class ConcreteFlyweight extends Flyweight
{
	private Character intrinsicState = null;
	//构造函数,内部状态作为参数传入
	public ConcreteFlyweight(Character state)
	{
		this.intrinsicState = state;
	}
	//外部状态作为参数传入,改变方法的行为,但不改变对象的内部状态
	public void operation(String state)
	{
		System.out.println("Intrinsic State = " + intrinsicState + ",Extrinsic State = " + state);
	}
}
class FlyweightFactory
{
	private Map<Character,Flyweight> files = new HashMap<Character,Flyweight>();
	private Flyweight lnkFlyweight;
	public FlyweightFactory() {}
	public Flyweight factory(Character state)
	{
		if(files.containsKey(state))
		{
			return files.get(state);
		} else{
			Flyweight fly = new ConcreteFlyweight(state);
			files.put(state,fly);
			return fly;
		} 
	}
	//辅助方法
	public void checkFlyweight()
	{
		Flyweight fly;
		int i = 0;
		System.out.println("\n======checkFlyweight()======begin");
		for(Iterator it = files.entrySet().iterator();it.hasNext();)
		{
			Map.Entry e = (Map.Entry)it.next();
			System.out.println("Item " + (++i) + ":" + e.getKey());
		}
		System.out.println("======checkFlyweight()======end");
	}
}
class Client
{
	public static void main(String[] args)
	{
		//创建一个享元工厂对象
		FlyweightFactory factory = new FlyweightFactory();
		//向享元工厂对象请求一个内部状态为'a'的享元对象
		Flyweight fly = factory.factory(new Character('a'));
		//以参数方式传入一个外部状态
		fly.operation("First Call");
		//向享元工厂对象请求一个内部状态为'b'的享元对象
		fly = factory.factory(new Character('b'));
		//以参数方式传入一个外部状态
		fly.operation("Second Call");
		//向享元工厂对象请求一个内部状态为'a'的享元对象
		fly = factory.factory(new Character('a'));
		//以参数方式传入一个外部状态
		fly.operation("Third Call");

		//以上代码,虽然申请了3个享元对象,但是实际上只创建了两个,这就是共享的含义。
		//我们检查一下一个有几个对象
		factory.checkFlyweight();
	}
}

系统往往只需要一个享元工厂的实例,所以享元工厂可以设计成单例模式。


复合享元模式


结构图




所涉及的角色


(1)抽象享元(Flyweight)角色:此角色是所有具体享元类的超类,为这些类规定需要实现的公共接口。那些需要外部状态的操作可以通过方法的参数传入。抽象享元的接口使得享元共享成为可能,但是并不强制子类实行共享,因此并非所有的享元对象都是可以共享的。

(2)具体享元(ConcreteFlyweight)角色:实现抽象享元角色所规定的接口。如果有内部状态的话,必须负责为内部状态提供存储空间。享元对象的内部状态必须与对象所处的周围环境无关,从而使得享元对象可以在系统内共享。有时候具体享元角色又叫做单纯具体享元角色,因为复合享元角色是由单纯具体享元角色通过复合而成的。

(3)复合享元(UnsharableFlyweight)角色:复合享元角色所代表的对象是不可以共享的,但是一个复合享元对象可以分解成为多个本身是单纯享元对象的组合。复合享元角色又称作不可共享的享元对象。

(4)享元工厂(FlyweightFactory)角色:享元工厂负责创建和管理享元对象。本角色必须保证享元对象可以被系统适当地共享。当一个客户端对象请求一个享元对象时,享元工厂角色检查是否已有一个符合要求的享元对象,如果有,则提供,如果没有,则创建。

(5)客户端(Client)角色: 客户单角色需要维护一个对所有享元对象的引用,需要自行存储所有享元对象的外部状态。


代理实例


import java.util.*;
abstract class Flyweight
{
	//一个示意性的方法,参数state是外部状态
	public abstract void operation(String state);
}
class ConcreteFlyweight extends Flyweight
{
	private Character intrinsicState = null;
	//构造函数,内部状态作为参数传入
	public ConcreteFlyweight(Character state)
	{
		this.intrinsicState = state;
	}
	//外部状态作为参数传入,改变方法的行为,但不改变对象的内部状态
	public void operation(String state)
	{
		System.out.println("Intrinsic State = " + intrinsicState + ",Extrinsic State = " + state);
	}
}
class ConcreteCompositeFlyweight extends Flyweight
{
	private Map<Character,Flyweight> files = new HashMap<Character,Flyweight>(10);
	private Flyweight flyweight;
	public ConcreteCompositeFlyweight() {}
	public void add(Character key,Flyweight fly)
	{
		files.put(key,fly);
	}
	public void operation(String extrinsicState)
	{
		Flyweight fly = null;
		for(Iterator it = files.entrySet().iterator();it.hasNext();)
		{
			Map.Entry e = (Map.Entry)it.next();
			fly = (Flyweight)e.getValue();
			fly.operation(extrinsicState);
		}
	}
}
class FlyweightFactory
{
	private Map<Character,Flyweight> files = new HashMap<Character,Flyweight>();
	private Flyweight lnkFlyweight;
	public FlyweightFactory() {}
	public Flyweight factory(Character state)
	{
		if(files.containsKey(state))
		{
			return files.get(state);
		} else{
			Flyweight fly = new ConcreteFlyweight(state);
			files.put(state,fly);
			return fly;
		} 
	}
	//符合享元工厂方法,一般传入一个聚集对象,这里换入String,因为可以分解成Character
	public Flyweight factory(String compositeState)
	{
		ConcreteCompositeFlyweight compositeFly = new ConcreteCompositeFlyweight();
		int length = compositeState.length();
		Character state = null;
		for(int i=0;i<length;i++)
		{
			state = new Character(compositeState.charAt(i));
			System.out.println("factory(" + state + ")");
			compositeFly.add(state,this.factory(state));
		}
		return compositeFly;
	}
	//辅助方法
	public void checkFlyweight()
	{
		Flyweight fly;
		int i = 0;
		System.out.println("\n======checkFlyweight()======begin");
		for(Iterator it = files.entrySet().iterator();it.hasNext();)
		{
			Map.Entry e = (Map.Entry)it.next();
			System.out.println("Item " + (++i) + ":" + e.getKey());
		}
		System.out.println("======checkFlyweight()======end");
	}
}
class Client
{
	public static void main(String[] args)
	{
		//创建一个享元工厂对象
		FlyweightFactory factory = new FlyweightFactory();
		//通过享元工厂对象创建一个具有某个组合状态的符合享元
		Flyweight fly = factory.factory("bcb");
		//以参数方式传入一个外部状态
		fly.operation("Composite Call");
	
		//以上代码,虽然申请了3个享元对象,但是实际上只创建了两个,这就是共享的含义。
		//我们检查一下一个有几个对象
		factory.checkFlyweight();
	}
}

通过以上代码可以看到,客户对象向享元工厂申请了一个内部状态为"bcb",外部状态为"Composite Call"的 享元对象。调用享元工厂的 辅助方法factory.checkFlyweight(),可以看到系统一共创建了两个单纯享元对象,分别对应"b"和"c"两种内部状态。 


使用场景


当以下所有的条件都满足时,可以考虑使用享元模式:

(1)一个系统有大量的对象。

(2)这些对象耗费大量的内存。

(3)这些对象的状态中的大部分都可以外部化。

(4)这些对象可以按照内部状态分成很多的组,当把外部状态从对象中剔除时,每一个组都可以仅用一个对象代替。

(5)软件系统不依赖于这些对象的身份,换言之,这些对象可以是不可分辨的。

满足以上条件的系统可以使用享元模式。但是同时要注意,享元模式需要维护一个记录了系统已有的所有享元的表,而这需要耗费资源,因此,只有在有足够多的享元实例可供共享时才值得使用享元模式。


享元模式的优点和缺点


享元模式不是一个常见的模式,享元模式的优点在于它大幅度地降低内存中对象的数量。但是,做到这一点代价也是很高的:

(1)享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。

(2)享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。

在实际的工作过程中,个人还想不起来哪个项目真正使用过享元模式。


结构模式(Structural Pattern)小结


结构模式(Structural Pattern)一共有七种,分别是:适配器模式、装饰模式、合成模式、代理模式、享元模式、门面模式、桥梁模式。

大致总结如下:

 最大特点典型应用
适配器模式利用对象的功能,并转换其接口日常工作,入目尽是适配器,DAO适配,Cache功能适配,等等
装饰模式对象层面的功能增强,接口不变Java I/O类库的设计
合成模式树枝、叶子同样对待分类树、权限树等
代理模式代表人WebService 的本地代理,权限访问代理,引用计数代理等
享元模式共享对象,减小内存占用编译器系统,Java String
门面模式统一对外接口,方便调用基于SOA框架编程中,不同系统之间的接口
桥梁模式解耦大多数的驱动器,包括JDBC Driver

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值