享元模式
意图
运用共享技术有效地支持大量细粒度的对象。
使用场景
Flyweight模式的有效性很大程度上取决于如何使用它以及在何处使用它。当以下情况都成立时使用Flyweight模式:
- 一个应用程序使用了大量的对象。
- 完全由于使用大量的对象,造成很大的存储开销。
- 对象的大多数状态都可变为外部状态。
- 如果删除对象的外部状态,那么可以用相对较少的共享对象取代很多组对象。
- 应用程序不依赖于对象标识。由于Flyweight对象可以被共享,对于概念上明显有别的对
象,标识测试将返回真值。
结构
享元模式结构如下
Flyweight
– 描述一个接口,通过这个接口flyweight可以接受并作用于外部状态。ConcreteFlyweight
– 实现Flyweight接口,并且为内部状态(如果有)提供储存。一个ConcreteFlyweight对象必须是可以共享的。它所存储的任何状态都应该是内部的,也就是说,它必须独立于ConcreteFlyweight对象的上下文(Context)。UnsharedConcreteFlyweight
– 并非所有的Flyweight子类都需要被共享。Flyweight接口提供了共享的能力,但是它并不是强制共享。在Flyweight的某些层次,UnsharedConcreteFlyweight对象通常会将ConcreteFlyweight对象当做子节点。FlyweightFactory
– 创建并且管理Flyweight对象
– 确保flyweights对象能被合理地共享。当一个client请求一个flyweight时,FlyweightFactory对象提供一个已经存在的实例或者新建一个(如果一个都不存在的话)。Client
– 维护一个对flyweight的引用
– 计算或者储存flyweight对象的外部状态
协作
- flyweight执行时所需的状态必定是内部的或外部的。内部状态存储于ConcreteFlyweight对象之中;而外部状态则由Client对象存储或计算。当用户调用flyweight对象的操作时,将该外部状态传递给它。
- Client不应直接对ConcreteFlyweight类进行实例化,而只能从FlyweightFactory对象得到ConcreteFlyweight对象,这可以保证对它们适当地进行共享。
优点
使用Flyweight模式时,传输、查找和/或计算外部状态都会产生运行时的开销,尤其当flyweight原先被存储为内部状态时。然而,空间上的节省抵消了这些开销。共享的flyweight越多,空间节省也就越大。
存储节约由以下几个因素决定:
- 因为共享,实例总数减少的数目
- 对象内部状态的平均数目
- 外部状态是计算的还是存储的
共享的Flyweight越多,存储节约也就越多。节约量随着共享状态的增多而增大。当对象使用大量的内部及外部状态,并且外部状态是计算出来的而非存储的时候,节约量将达到最大。所以,可以用两种方法来节约存储:用共享减少内部状态的消耗,用计算时间换取对外部状态的存储。
享元模式实现(Implement)
案例
DOTA2是风靡全球的电子竞技游戏。在游戏中,地图一共分为上中下三路,每条路会出现大量的小兵,玩家可以通过击杀小兵获取金钱和经验。那么在这么一整张地图之中,如果对每一个小兵都产生一个新的对象,那么系统会消耗大量的资源,享元模式就可以在这里派上用处,通过共享来节约对象的产生。
代码实现
近战小兵抽象类,也就是Flyweight接口,内部状态是公共而且固定的,包括小兵视野,攻击距离以及经验,外部状态是移动的位置
天辉方小兵,ConcreteFlyweight类之一
夜魇方小兵,ConcreteFlyweight类之一
小兵工厂类,可以设置成单例的
测试类
外部状态小兵位置通过client来控制,内部状态保存在对象之中共享,这样整张地图只有两个小兵对象,节约了大量对象创建的开销,通过时间来换取空间。
总结
享元模式有优点也有缺点。享元模式可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能;享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。但同时,享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化;为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。