讲完了SINGLETON和Proxy,按计划应该讲讲Flyweight了。
Flyweight,字面上理解,就是Weight都Fly了,重量都飘起来了,这个模式在《设计模式》这本书里面翻译为“享元”。这个翻译有点怪怪的,一开始我还以为是亨元,咸亨的亨,大亨的亨呢。仔细一看,原来是说“共享的元素”。这么一分解,含义倒是挺贴切的,只是这种缩略语实在是很蹩脚。我们先来看看《设计模式》这本书是怎么写Flyweight的结构的:

这里面可以看到有一个基本的思想就是,客户代码通过给出一个key来标明需要什么东西,然后通过给出extrinsicState来给出相关的上下文参数——这些参数都是非共享的、独立的信息。打个比方说,我们的文字处理程序可能所有的东西都需要完全用对象来表示。这样的情况下,每一个字符都是一个对象,该对象的作用是把这个字符画到屏幕上适当的位置。可以想象一篇一百万字的英文文章可能就需要一百万个对象,这是一个不可接受的消耗。但是如果我们把所有出现过的英文字符都缓冲到一个尺子当中,并且下一次使用的时候就只是把引用指向这个缓冲池当中的适当对象,那么我们也许就只需要创建几百个对象。这个时候key就是a的Ascii值或者Unicode值,extrinsicState就是这个字符在屏幕上面的位置,intrinsicState就是这个字符的Ascii值或者Unicode值,也可能干脆就是这个字符的图像。需要注意的是ConcreateFlyweight应该不允许直接构造,而应该通过FlyweightFactory构造,否则就可能无法缓冲到缓冲池当中了。
大家仔细看看就会发现,实际上这个模式的不共享对象也被纳入到一个统一的体系里面来了,这样客户代码可以完全不需要关心这个对象是否是共享的。此外我们还可以看到,这里面的FlyweightFactory还完全可以设计成工厂方法的模式。举个例子,还是文字处理软件的场景:假设我们定义一篇文档由文档、段、行、字符组成,他们都是有一个抽象图元的基类派生而来,并且互为上下级关系。于是文档、段、行、字符都分别实现积累所定义的GetChildAt(int Position)函数——也就是上图当中的GetFlyweight函数,其中字符永远返回空,行则返回ConcreateFlyweight,文档和段则返回UnsharedConcreateFlyweight。
但是实际上的应用是不是一定要这样子呢?是不是一定要整个对象要么都是共享的要么都是非共享的呢?实际上我们很可能不会遇到纯粹的ConcreateFlyweight,因此需要进行一些分解。就比如字符对象,依附于某个特定字符对象的信息可以分为好几类,其中字符编码是一个集合,字符的字体(包括大小、粗细、字体名称等)也是一个集合,字符的位置也是一个集合。《设计模式》这本书里面也有这样的例子,就是讲解关于字体的处理问题。其实这些集合完全可以分别抽取出来进行缓冲,比如字符这个ConcreateFlyweight里面关于字体的信息,就可以通过调用this.GetChildAt(this.Position)来获得一个字体的ConcreateFlyweight。为了减少内存消耗量,我们甚至需要像上面那样把内部信息抽取出来分门别类,把拥有相同的key类型的信息放在一个Flyweight缓冲池当中。
说一个我的实际应用吧。很多时候我们需要对一些信息用树的方式表达出来,比如说公司结构图。节点的类型简单分为两种——部门和员工。但是员工有很多种,比如说经理、助手、职员等等,不同类型的员工会有某些值是不一样的,但是相同类型的员工这些值则是一样的,比如说头像(ImageIndex)、职位(PostName)。甚至我们仔细研究,还可以更进一步的优化,把头像该成为图标、职位变成节点类型,这样连“部门”这个节点也被我们统一进来了。在这个基础上面我们还可以进一步增加“可包含子节点类型”这样一个信息,这样我们就得到“图标”、“节点类型”、“可包含子节点类型(集合)”这几个与节点类型相关的Flyweight了。好,我们看看代码:
internal class NodeFlyweight
{
public int ImageIndex;
public string NodeTypeName;
public ArrayList AcceptNodes = new ArrayList();
private NodeFlyweight()
{
}
static public NodeFlyweight Pooling(int TypeNum)
{
if (ht.Contains(TypeNum))
{
return (NodeFlyweight) ht[NodeType];
}
else
{
NodeFlyweight nf = new NodeFlyweight();
ht.Add(TypeNum, nf);
return nf;
}
}
static private Hashtable ht = new Hashtable();
}
public class BaseNode
{
private int typeNum;
public string Name;
public BaseNode Root;
public int ImageIndex
{
get
{
return NodeFlyweight.Pooling(typeNum).ImageIndex;
}
set
{
NodeFlyweight.Pooling(typeNum).ImageIndex = value;
}
}
public int NodeTypeName
{
get {...}
set {...}
}
public ArrayList AcceptNodes{...}
}
public class StaffNode
{
public float Salary;
}
public class DepartmentNode
{
public string PhoneNumber;
}
上面这个代码没有写全,甚至很多地方都是随意写写的(比如很多东西没有写函数,直接暴露字段),看看大意就好了。我们看到,这个代码里面把很多的东西都变成共享了,通过typeNum进行共享。上面没有交代typeNum是怎么工作的,这个typeNum是通过一个注册步骤产生的,这册函数这里面没有写,因为一写起来就没完了——要把整个ProtoType的设计模式给写下来啊。这样在外界看来StaffNode/DepartmentNode好像没有什么特别的地方,甚至不一定就能够察觉这个对象有什么东西被共享了。此外,大多数的逻辑都可以写到基类BaseNode当中,派生类只需要专注那些只有他们才有的东西。甚至我们还可以找出一些其他的共享内容,比如说如果我们实现一些Observer或者MVC之类的模式,需要将这个数据和一些视图通过某个Manager绑定起来,那么我们很容易看出来,一棵树肯定只会被一个Manager管理。也就是说也许还有一些内容是整棵树都一样的,但是为了方便访问,我们需要每一个节点都有直接访问这些信息的成员。比如说用户对某个节点的信息进行修改了,这时候程序希望通知所有视图:某棵树的某个节点有所变动。我们当然希望代码写成
node.Manager.XXX_Changed(node);
这个样子,看起来比较清晰。为了避免每一个node对象都占用一个引用空间(用来指向一个Manager),我们可以利用Flyweight——添加一个TreeFlyweight类,只是这个时候key就变成了node.root。用Flyweight还能避免每一个node指向的Manager可能出现不一致的意外情况,不再需要考虑如何保证一致性的问题。
其实我也不知道这样的设计是否仍然属于Flyweight,因为和《设计模式》这本书上面所描述的结构还是有点区别的。实际上想想,也就是把Flyweight里面的元素映射到上一级对象当中,而自己却不暴露出来而已,似乎没有什么大的区别。
其实上面的代码确实是我一个工程当中的简化情况,里面还有很多的东西没有提到,比如说,为什么需要那个AcceptNodes呢,如何使用呢?这个要留到下一节ProtoType里面讲了。此外还有一些失败的地方没有提,我想可能会在另外的帖子里面讲吧,比如说上面的NodeFlyweight把FlyweightFactory和FlyWeight合二为一了,这就出现了一定的问题了。
文章来源:http://dotnet.blogger.cn/sumtec/articles/844.aspx
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器