系统架构技能之设计模式—享元模式
一、上篇回顾
通过上篇的讲述,我们知道装饰模式,特别适合对某个类型的对象,动态的增加新的职责,应用程序就像使用原来的对象一样使用对象新增的装饰后的功能,装 饰模式就好像是穿了一层层的外壳,这样的方式避免了通过继承来为类型添加新的职责的形式可取,通过继承的方式容易造成子类的膨胀,但是当装饰类太多的时 候,也是个难以维护的问题,至少是在装饰对象的时候,我们可能需要多步操作来完成对象的装饰,这时候我们可以同上面提出的改进的方案,来完成自动配置装饰 模式,记录操作模式的状态,可以进行有效的回滚操作,以完成撤销操作。
我们先来回顾下装饰模式的使用场景:
1、当我们需要为某个现有的对象,动态的增加一个新的功能或职责时,可以考虑使用装饰模式。
2、适应于某个对象的职责经常发生变化或者经常需要动态的增加职责,避免因为这种为了适应这样的变化,而增加继承子类扩展的方式,因为这种方式为 造成,子类膨胀的速度过快,难以控制。
二、摘要
本篇我们将会讲述结构性模式中的另外一个非常有用的模式-享元模式,享元模式的特点是,复用我们内存中已存在的对象,降低系统创建对象实例的性能消耗。在.NET下的值类型和引用类型的内存分配机制,我这里就不做详细的讲解了,包括引用类型与值类型之间的装箱和拆箱的操作,这个具体的可以参考园子里面的关于这方面的文章的讨论。
我们来给出个简单的享元模式的应用前后的对比图,大概我们就知道享元模式的重要作用了。
我们这里以绘制一个有样式的字体来说明吧,有的时候我们想绘制一个纯色的文字,比如红色,那么我们可能需要创建很多的实例,通常来说,这些实例的差别不大,这个时候,我们可以考虑复用其中创建的某个实例,而不用去new这么多相同的对象,来完成这样的工作。我们下面以这个例子来说明,使用享元模式的前后对比的情况。
通过上图我们可以大概的看出享元模式的目的是什么,本篇将会从以下几点出发,讲述享元模式的应用。
1、享元模式的特点和场景。
2、享元模式的经典实现。
3、享元模式的其他方案。
4、享元模式小结。
下面我们来看下享元模式的类图吧:
三、本文大纲
a、上篇回顾。
b、摘要。
c、本文大纲。
d、享元模式的特点及使用场景。
e、享元模式的经典实现。
f、享元模式的其他方案。
g、享元模式使用总结。
四、享元模式的特点及使用场景
4.1、享元模式的特点
享元模式的意图是通过共享有效支持大量细粒度的对象,来提供应用程序的性能,节省系统中重复创建对象实例的性能消耗,这个怎么理解呢?其实就是以下几点的含义:
1、当我们系统中某个对象类型的实例较多的情况。
2、并且要求这些实例进行分类后,发现真正有区别的分类很少的情况。
例如我们的生活中很多的场景,我们在使用拼音输入的法的时候,如果说我们每个字都是new一个对象实例的操作的话,那么内存中的实例就太可怕,这个时候,我们是不是可以考虑将这些重复的字体在内存中只是创建一次,而是通过复用对象的形式,来组织一些可能有多个字符重复的内容呢?也许这是一个不错的主意,我们来看看这个示例的过程吧。
4.2、享元模式的使用场景
1、当我们发现某个类型的对象有大量的实例时,我们是否可以对这些实例进行分类,经过分类后,我们发现只有很少的类别的情况下。
2、我们发现通过使用享元模式后能够提高系统的性能和不会带来更多的复杂度时。
享元模式一般是给出本地内存资源节省的一个方案,并不适合互联网上的分布式应用的情况,不过享元模式对于排他性的要求资源的控制,是个不错的选择的。
五、享元模式的经典实现
我们下面来根据上面的我们对输入法中的字体来进行分析,给出相关的示例代码:
字体类型的基类:
public class FontBase
{
private List<string> font = new List<string>();
private string fontName;
public FontBase(string name)
{
this.fontName = name;
}
public FontBase AddFont(string font)
{
this.font.Add(font);
return this;
}
public virtual string FontName
{
get
{
return this.fontName;
}
}
}
具体的文字类型类:
public class ChineseFont : FontBase
{
public ChineseFont()
: base("ChineseFont")
{
base.AddFont("ChineseFont");
}
}
public class EnglishFont : FontBase
{
public EnglishFont()
: base("EnglishFont")
{
base.AddFont("EnglishFont");
}
}
具体的创建工厂类:
public class FontFactory
{
private Dictionary<string, FontBase> fonts = new Dictionary<string, FontBase>();
public FontBase Create(string name)
{
FontBase fontBase = fonts[name];
if (fontBase != null)
return fontBase;
fontBase = (FontBase)Activator.CreateInstance(Type.GetType(name));
return fontBase;
}
}
通过上面实例的讲解我们知道,我们通过缓存对象类型的形式来控制对象实例的创建过程,经典的模式中没有体现共享的状态,比如说我们在外部可能对于享元对象来说是不共享的,内部是共享的。下面我们来看看享元模式的变种吧。
六、享元模式的其他方案
对于上面的经典方案带来的问题,可能我们需要更好的考虑,我们如何应对多种对象类型,我们如何管理并共享这些对象实例,这些都是我们需要考虑的问题,经过上面的思考,我们这里可以参考我们平时开发的ORM中的连接池的思路,我们这里对享元模式提供-对象池的技术。
我们在配置文件中控制对象池中的某个类型对象实例的数量,对象的生命周期的时间,默认初始化多少个对象实例,以提前准备,为后续的使用提供服务。
我们这里可以设计出这个专门的对象池,我们可以提供如下的功能:
1、根据对象类型动态的创建对象实例。
2、根据对象池中的配置,在对象池中找到空闲的实体提供给程序调用,减少创建对象的次数。
3、我们需要设计每个类型的缓冲池,通过把对象进行缓存,提供性能。如果对象池中的对象长期不会调用,那么我们会提供一个销毁对象的机制。
我们来看看对象池的设计结构吧:
通过上面的几个组件,来协调完成对象池的工作。
这里给出相关的示例代码:
我们先给出配置文件配置缓冲池的配置信息:
<?xml version="1.0" encoding="utf-8" ?>
<Cache>
<ObjectSection name="" type=""/>
<ObjectCache>
<object name="" type="" max="" timeout="" />
<object name="" type="" max="" timeout="" />
<object name="" type="" max="" timeout="" />
<object name="" type="" max="" timeout="" />
<object name="" type="" max="" timeout="" />
<object name="" type="" max="" timeout="" />
<object name="" type="" max="" timeout="" />
<object name="" type="" max="" timeout="" />
<object name="" type="" max="" timeout="" />
</ObjectCache>
</Cache>
我们来看看具体的工厂类:
public class ObjectFactory
{
public static T Build<T>() where T:class,new()
{
return new T();
}
}
七、享元模式使用总结
享元模式的主旨意在通过共享对象节省总体资源,不过共享对象将导致资源访问上的一些问题,包括对象实例的销毁,还包括线程间的共享和线程内的共享是不同的,我们知道,一般线程退出的时候,会自动释放线程内部的申请的资源,.NET会自动通过GC来回收,但是对于线程间共享的对象也许不会自动回收,这些内容需要宿主进程来进行回收,当然可能这些我们也可以通过对象池来完成这样的回收机制。 或者说也可以参考操作系统中的队列的情况,通过回调函数来通知进行对象的回收等。我们在对于项目中需要大量实例对象的创建工作的时候,我们就考虑一下是不是需要享元模式的应用了,希望大家能在项目中找到合适的切入点,使用合适的模式来提高程序的适应性和健壮性。由于本人水平有限,不足或者错误之处,还请大家批评指出。