重温设计模式(一)——享元模式(Flyweight)

一. 世间万物皆为对象

从大学校园中拦住一个软件工程专业的学生,问他,什么是面向对象。他会告诉你,世间万物皆是对象。

世界之大,何止万物。上至宇宙星辰,下至细菌病毒。皆为对象。

女孩,吐气如兰,仍留淡淡余香。

男孩,闭眼陶醉,不亦乐乎。

此乃共享之妙也!

二. 对象爆炸

呼吸之间,分子无数。

每个分子皆为一对象,恐万台服务器之矩阵亦无可容。

奈何乎?

GOF 曰: 享元模式!

三. 何为享元模式

Flyweight : 次最轻量级的拳击选手。即粒度最小。

因此,享元模式的目的是采用共享技术解决大量细粒度对象的爆炸问题。

图:

www.exciton.cs.rice.edu__image0

四. 享元模式应用之QQ聊天

我们不妨假设QQ是在服务器端将每次的对话都抽象出来形成了一个类。于是代码如下:

class People
{
    private string name;
    private int age;
    public string Name
    {
        get
        {
            return name;
        }
    }
    public int Age
    {
        get
        {
            return age;
        }
        set
        {
            age = value;
        }
    }
    public People(string name, int age)
    {
        this.name = name;
        this.age = age;
    }
}

class Chat
{
    private People boy;
    private People girl;
    private string chatContent;

    public Chat(People p1, People p2)
    {
        this.boy = p1;
        this.girl = p2;
    }
    public string ChatContent
    {
        get
        {
            return chatContent;
        }
        set
        {
            chatContent = value;
        }
    }
    public People Boy
    {
        get
        {
            return boy;
        }
    }
    public People Girl
    {
        get
        {
            return girl;
        }
    }
}

若每次二者聊天时均将Chat实例化为一个对象,如下:

class Program
{
    static void Main(string[] args)
    {
        People boy=new People("PrettyBoy",20);
        People girl=new People("BeautifulGirl",18);
        Chat chat = new Chat(boy, girl);
        chat.ChatContent = "I love you";
        ChatServer.Send(chat);
    }
}

若如此,服务器就需要每次都去初始化一个对象,而当chatServer将此次聊天的记录发送给客户机之后,这个对象便成了垃圾对象。这样,每小时几百万的聊天次数,便有了几百万的对象垃圾。垃圾回收器GC便需要不停地去工作,回收对象。

这就对效率产生了极大的影响。于是,我们想办法,使用享元模式来解决这个问题。

两者聊天,他们的聊天方是不变的,因此,我们可以在服务器端去维护一个这样的Chat对象集合,如果该聊天对象已经存在,那么我们便重复去利用这个聊天对象。这样既减少了内存垃圾,又节省了创建对象的时间。

代码如下:

class FlyweightFactory
{
    private IDictionary<string, Chat> cache = new Dictionary<string, Chat>();

    private void Add(Chat c)
    {
        cache.Add(c.Boy.Name + "_" + c.Girl.Name, c);
    }

    public Chat GetChat(People boy , People girl)
    {
        if (!cache.ContainsKey(boy.Name + "_" + girl.Name))
        {
            cache.Add(boy.Name + "_" + girl.Name, new Chat(boy, girl));
        }
        return cache[boy.Name + "_" + girl.Name];
    }
}

于是,从客户端访问该FlyweightFactory即可。

这样,便有效控制了对象的数量。

五. 享元模式的.NET Framework典型应用——String

(在这里麻烦请教一下各位,我想在Reflector中,看一下String赋值的具体代码,怎么找到呢?比如说string s=”111”;这一步的代码)

好,步入正题,让我们来看看享元模式在.NET Framework中的应用。

String 无论在.NET 还是 Java中,都是一个特殊的引用对象。

我们可以试想,出现了这样一段代码:

String s=”Hello world”;

String s1=”Hello world”;

那么是不是每次都要重新的去申请一块内存,然后去保存这个字符串呢?那么这样的话是不是会效率很低呢?因为我们知道,字符串在实际使用中往往都是非常短暂的。他们通常是被读出来之后,便直接展示给了客户。然后这个字符串的生命结束,变成垃圾。是不是很像我们刚才那个QQ聊天对象呢?

于是在.NET 和 Java中,String都被以不变模式来进行设计。

我们来简单的分析一下String的驻留机制:在CLR被加载之后,就会在SystemDomain的托管堆中去建立一个HashTable来维护String。

于是模拟代码如下:(伪代码)

Hashtable table;
if (!table.Contains("Hello world"))
{
    table.Add("Hello world", &(new String("Hello world")));
}
return *(table["Hello world"]);

代码写的有些乱,我来解释一下。

也就是说,我是在模拟一个string s=”Hello world”的过程。过程是,首先,他先去找Hashtable中目前是否存有Key为”Hello world”的项。如果不存在,那么就分配一块堆内存,存储这字符串,然后将地址作为Value,存储在Hashtable中。如果存在的话,那么便直接找到该字符串所对应的地址,然后取出地址中的值。

用一个Hashtable来控制String对象的数量。这次您明白了么?

六 . 享元模式的扩展——对象池的应用

我们之前说,无论是字符串还是Object对象,使用享元模式都是去检查该对象是否存在,只要存在,那么便去重复使用。

那么是否有这样一种情况呢?

在峰期时,大量的客户端去访问同一个服务器,这个时候,如果只有一个对象的话,会引起一定的并发问题。我的语言表述有些不大清楚。简单的说,就是每当一个对象被访问的时候,他必须将自身锁定,并且防止其他客户去引用至该对象。

如果这个时候,我们依然去只维护一个对象的话,便会让大量的客户端处于等待队列中。因此,我们需要靠维护一个对象池,允许在对象池中,维护一个类的多个对象。从而来实现一个服务器空间与客户端等待时间的均衡问题。

因此,曾经,我们是在Dictionary中去维护一个Value为Object的缓存。而如今,我们便需要在Dictionary中去维护一个Value为List<Object>的缓存。而这个List应当是限定数量的,能保存同一类型Object的数组。

代码如下:(参考蜡笔小王的<设计模式——基于C#的工程化实现及扩展>)

class ObjectCache
{
    private static IDictionary<Type, Object> cache;

    static ObjectCache()
    {
        cache = new Dictionary<Type, Object>();
    }

    public bool TryToGetObejct<T>(out T item, out bool increasable) where T : class,IPoolable, new()
    {
        TryToAddObject<T>();
        return (cache[typeof(T)] as SizeRestrictedList<T>).Acquire(out item, out increasable);
    }
    private void TryToAddObject<T>() where T:class,IPoolable,new()
    {
        if (!cache.ContainsKey(typeof(T)))
        {
            cache.Add(typeof(T), new SizeRestrictedList<T>());
        }
    }
}
public bool Acquire(out T item, out bool increasable)
{
    increasable = cache.Count >= configuration.Max ? false : true;
    item = null;
    if (cache.Count <= 0)
    {
        return false;
    }
    foreach (T cacheItem in cache)
    {
        if (cacheItem != null && cacheItem.Unoccupied)
        {
            item = cacheItem;
            return true;
        }
    }
    return false;
}

七.  从微观到宏观——究竟多小才算Flyweight

我们上文说过,Flyweight是来解决细粒度对象的重用问题。那么我们去想想,究竟多小才算细粒度呢?

在上文中,我们一直在解决的都是对象的重用问题。那么我们向宏观方向去想一想。

爱因斯坦的相对论:世间万物都是相对的。没有什么是绝对大的,只有相对的小。那么我们来这样想。

我们是否可以重用一个模块,或者一个子系统呢?

八. 举一而反三—— 从享元到单例

其实,在一定意义上,我个人认为单例模式和享元模式的初衷是一样的。他们都是一个基于空间和性能的模式。他们都是要控制对象的数量,而且实现方式本质上有着一些类似,就是首先查询这个对象是否存在,然后返回这个对象。

那么从享元模式上的引申,我们就一样可以用到单例模式上了:

1. 我们可以不局限于单例,而是可以控制为多例。比如说:类似我前面对象池的目的

2. 单例只是对象么?我们一样可以把子系统和模块单例!

看看他们的不同:

应该说享元模式是单例模式的一个延伸。享元模式通过享元工厂来控制多个对象的单例化。而单例化解决的只是本身的单例问题!

九. 不要为模式而模式——何时才用享元

我一直觉得,模式不要乱用,乱用模式是学习的阶段,但是一旦在工作中,我们去乱用模式,那么可能会造成很惨的后果。

那么究竟何时应该用享元模式呢?

1. 系统中要有大量的对象,这才值得用享元模式。否则你去维护一张对象表,就不值得了。

2. 对象的创建是会消耗大量时间的过程,并且对象占用较大内存。如果不是,那就让系统去创建吧。

3. 在B/S的系统中,个人感觉享元的应用相对较少,Web的无状态,加之我们完全在客户端进行一系列的复杂逻辑,然后将之统一传递给Web服务器端,而不需要享元。享元主要应用还是在C/S及Winform的本地程序上较多。

其余的,比如,关于外蕴状态和内蕴状态究竟何种应该使用享元的问题,如果不满足情况,您也根本没有办法去使用享元。因此,我就不在这说那些蹩嘴的定义了。

十 . 享元总结

享元模式(Flyweight):运用共享技术有效地支持大量细粒度的对象。

不早了,该睡觉了,等会还要起床加班………

谢谢大家的关注,也希望大家多多指教!

posted @ 2009-03-29 06:19  飞林沙  阅读(4771)  评论(43编辑  收藏