本文主要是看了《设计模式》做的笔记和思考,在此分享仅代表个人观点,如有不对的地方欢迎批评和指正。
应用场景
如果你打算做一个文字编辑器,类似Word那种,你可能想着给每个字都做成一个对象,但这样一来,一篇长文章中的对象数量增长会非常的快,对内存的消耗也是大问题,不过,这些字总有些相同的,如果是英文,256个(参考ASCII)能凑合着用了。那么,当同样的字符使用同一个对象,并且在字符对象外部对其进行修饰,就能够在满足使用需求的情况下大幅节省内存。
基础
Flyweight模式中文名叫享元模式,该模式的目的是在于分享最小规格的对象,来达到节省内存的目的。至于各个对象在不同位置的不同性质,由它的外部决定,UML如下。就继续拿文字编辑器的例子来说,被分享的是每个字符,但你在屏幕上看到的,是那些字符打印出来的效果,即相同的字符由同一字符对象打印。
Flyweight的外部
由于屏幕上的每个文字都可能有样式的不同,如果个每个字符添加样式控制的对象,那么使用Flyweight模式的意义就不大了。正确的做法是使用一个外部类,这个类能够通过使用特定的数据结构来存储这些信息,使得相同的信息只有一份储存。
class GlyphContext{
public:
GlyphContext();
virtual ~GlyphContext();
virtual void Next(int step = 1);
virtual void Insert(int quantity = 1);
virtual Font* GetFont();
virtual void SetFont(Font*, int span = 1);
private:
int _index;
BTree* _fonts; // 使用二叉树保存样式信息
}
这就是一个外部类,它的引用以参数的形式进入到字符对象中,当字符添加、修改样式时它将实时更新内部信息。重点它保存字符样式的方法,是使用一个二叉树,二叉树可以抽象成下图(图来源于书),此处每个数字代表字符数量,叶子节点代表字符样式,顺着这个树向下遍历,便能得到某个字符串的样式。如果此处换做给每个字符都保存一个样式信息,那么该模式的使用意义并不大。
二叉树.png
上图可以这么理解:字符对象储存的是字符信息(它是哪个字符),GlyphContext类则储存了排版信息,当界面开始绘制时,字符对象就询问GlyphContext类:“我该画成什么样子”,GlyphContext就开始查表(当前位置我们可以通过光标传入,而不需要由字符对象保存),告诉字符对象你该怎样怎样。
因此,这里可以总结出来:如果希望通过享元模式节省内存,那么应该尽量整合外部的信息,最好外部信息可以通过计算得到,以时间换空间。
管理享元对象
创建
为了更好地管理并且确保享元对象是真的被共享,你需要通过工厂模式来创建享元对象。重点:如果标识相同,就返回相同的对象,不存在就新建一个。
class FlyweightFactory{
...
public MyChar getCharacter(char c){
if(!map.containsKey(c)){
newOne = new MyChar(c);
map.put(c,newOne);
}
return map.get(c);
}
private HashMap<char c,MyChar mc> map;
}
那么,如何防止用户绕过工厂去创建Flyweight对象呢?在Java中,你可以将工厂和Flyweight放在同一个包,并且将Flyweight的权限设置为default,工厂的设置为public。
回收
这个我在很多博客中都没有看到,也请读者不要忽略这个问题。设想一下,如果享元过多,是否本身就是一种对内存的消耗(尽管成为享元前更多),比如这里要存的不是英文,而是中文,那享元的数量就很可观了。然而哪怕存的是中文,也不是每个中文字符都会被用上,因此当某个生僻字终于被用户删除时,我们应该立马释放它的内存(毕竟在内存中,它与常用字占用着同样的资源)。
class MyChar{
private char character;
private int useQuantity;
public MyChar(char c){
character = c;
useQuantity = 1;
}
}
class FlyweightFactory{
...
public boolean removeChar(char c){
if(map.containsKey(c)){
if(map.get(c).useQuantity == 0){
map.remove(c);
return true;
}
}
return false;
}
}
这里我设计了一个useQuantity的计数器,在外部可以检测其数值,一旦发现其使用数为0,可以调用工厂内的removeChar方法来释放内存。
当然,如果你的享元数量较少,而且本身占用内存很少时,你可以永久保留。
网友评论