重温设计模式(一)——享元模式(Flyweight)
一. 世间万物皆为对象
从大学校园中拦住一个软件工程专业的学生,问他,什么是面向对象。他会告诉你,世间万物皆是对象。
世界之大,何止万物。上至宇宙星辰,下至细菌病毒。皆为对象。
女孩,吐气如兰,仍留淡淡余香。
男孩,闭眼陶醉,不亦乐乎。
此乃共享之妙也!
二. 对象爆炸
呼吸之间,分子无数。
每个分子皆为一对象,恐万台服务器之矩阵亦无可容。
奈何乎?
GOF 曰: 享元模式!
三. 何为享元模式
Flyweight : 次最轻量级的拳击选手。即粒度最小。
因此,享元模式的目的是采用共享技术解决大量细粒度对象的爆炸问题。
图:
四. 享元模式应用之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):运用共享技术有效地支持大量细粒度的对象。
不早了,该睡觉了,等会还要起床加班………
谢谢大家的关注,也希望大家多多指教!