引言
在软件开发过程中,如果我们要重复使用某个对象的时候,重复使用new创建这个对象的话,这样我们在内存就要多次地去申请内存空间,这样可能会出现内存使用越来越多的情况,这种问题是非常严重的,使用享元模式可以结局这个问题。
详细介绍
上面说了,享元模式可以解决重复使用对象导致内存使用越来越多的问题,先分析一下如何去解决。上面的问题就是重复创建统一个对象,如果我们去解决这个问题肯定会这样想:既然是同一个对象,能不能只创建一个对象,然后下次需要创建这个对象的时候,让它直接用已经创建好了的对象。也就是说,让一个对形象共享。
定义:
运用共享技术有效地支持大量细粒度的对象。享元模式可以避免大量相似类的开销,在软件开发过程中如果需要生成大量细粒度的类实例来表示数据,如果这些实例除了几个参数外基本上都是相同的,这时候就可以使用享元模式来大幅度减少需要实例化类的数量。如果能把这些参数(指的是这些类实例不同的参数)移到实例类外面,在方法调用时将他们传递进来,这样就可以通过共享大幅度减少单个实例的数目(这也是享元模式实现的要领),然而我们把类实例外面的参数称为享元对象的外部状态,把在享元对象内部状态与外部状态的定义为:
内部状态:在享元对象的内部并且不会随着环境改变而改变的共享部分
外部状态:随着环境改变而改变,不可以共享的状态。、
实现:
一个文本编辑器中会出现很多字面。使用享元模式去实现这个文本编辑器的话,会把每个字面做成一个享元对象。享元对象的内部状态就是这个字面,而字母在文本中的位置和字体风格等其他信息就是它的外部状态。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace _29FlyweightPatternDemo
{
class Program
{
static void Main(string[] args)
{
//自定义外部状态,例如字母的位置信息等
int externalstate = 5;
//初始化享元工厂
FlyweightFactory factory = new FlyweightFactory();
//判断是否已经创建了字母A,如果已经创建就直接使用创建的对象
Flyweight fa = factory.GetGlyweight("A");
if(fa!=null)
{
//把外部状态作为享元对象的方法调用参数
fa.Operation(--externalstate);
}
//判断是否已经创建了字母B
Flyweight fb = factory.GetGlyweight("B");
if(fb!=null)
{
fb.Operation(--externalstate);
}
//判断是否已经创建了字母C
Flyweight fc = factory.GetGlyweight("C");
if (fc != null)
{
fc.Operation(--externalstate);
}
//判断是否已经创建了字母D
Flyweight fd = factory.GetGlyweight("D");
if (fd != null)
{
fd.Operation(--externalstate);
}
else
{
Console.WriteLine("驻留池不存在字符串D");
//创建一个对象并放入驻留池中
ConcreteFlyweight d = new ConcreteFlyweight("D");
factory.flyweight.Add("D", d);
}
Console.WriteLine("驻留池中存在的字符串:");
foreach(string item in factory.flyweight.Keys)
{
Console.WriteLine(item);
}
}
}
/// <summary>
/// 享元工厂,负责创建和管理享元对象
/// </summary>
public class FlyweightFactory
{
//最好使用泛型Dictionary<string,Flyweight>
//public Dictionary<string,Flyweight> flyweight = new Dictionary<string,Flyweight>();
public Hashtable flyweight = new Hashtable();
public FlyweightFactory()
{
flyweight.Add("A", new ConcreteFlyweight("A"));
flyweight.Add("B", new ConcreteFlyweight("B"));
flyweight.Add("C", new ConcreteFlyweight("C"));
}
public Flyweight GetGlyweight(string key)
{
// 更好的实现如下
//Flyweight flyweight = flyweights[key] as Flyweight;
//if (flyweight == null)
//{
// Console.WriteLine("驻留池中不存在字符串" + key);
// flyweight = new ConcreteFlyweight(key);
//}
//return flyweight;
return flyweight[key] as Flyweight;
}
}
//抽象享元类,提供享元类具体的方法
public abstract class Flyweight
{
public abstract void Operation(int extrinsicstate);
}
//具体的享元对象,这样我们不把每个字母设计成一个单独的类,而是作为把共享的字母作为享元对象的内部状态
public class ConcreteFlyweight : Flyweight
{
//内部状态
private string instrinsicstate;
//构造函数
public ConcreteFlyweight(string innerstate)
{
this.instrinsicstate = innerstate;
}
/// <summary>
/// 享元类的实例方法
/// </summary>
/// <param name="extrinsicstate"></param>
public override void Operation(int extrinsicstate)
{
Console.WriteLine("具体实现类:intrinsicstate{0},extrinsicstate{1}",instrinsicstate,extrinsicstate);
}
}
}
在享元模式的实现中,我们没有像之前一样,把一个细粒度的类实例设计成一个单独的类,而是把他作为共享对象的内部状态放在共享类的内部定义。
类图
上图涉及到的集中角色:
- 抽象享元角色:Flyweight,作为所有的具体的享元类的基类,为这些类规定出需要实现的公共接口。那些需要外部状态的操作可以通过调用方法以参数形式传入
- 具体享元橘色:ConcreteFlyweight,实现抽象享元角色所规定的接口。如果有内部状态的话,可以在类内部定义
- 享元工厂角色:FlyweightFactory,负责创建和管理享元角色。本角色必须保证享元对象可以被系统适当的共享,当一个客户端对象调用一个享元对象的时候,享元工厂角色检查系统中是否已经有一个符合要求的享元对象。如果已经存在,享元工厂角色就提供已经存在的享元对象,如果系统中没有一个符合的享元对象的话,就创建一个合适的享元对象。
- 客户端:Program,需要存储所有享元对象的外部状态
优缺点
优点:
- 降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力
缺点:
- 为了是对象可以共享,需要将一些状态外部化,这使得程序的逻辑更复杂,使系统复杂化
- 享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长
使用场景
在下面所有条件都满足时,可以考虑使用享元模式:
- 一个系统中有大量的对象;
- 这些对象耗费大量的内存;
- 这些对象中的状态大部分都可以被外部化
- 这些对象可以按照内部状态分成很多的组,当把外部对象从对象中剔除时,每一个组都可以仅用一个对象代替
- 软件系统不依赖这些对象的身份,
满足上面的条件的系统可以使用享元模式。但是使用享元模式需要额外维护一个记录子系统记录已有的所有享元的表,而这也需要耗费资源,所以,应当在有足够多的享元实例可共享时才值得使用享元模式。
总结
享元模式主要是用来解决由于大量的细粒度对象所造成的内存开销问题,它在实际开发中并不常用,可以作为底层的提升性能的一种手段。