Flyweight 模式
为什么使用 Flyweight 模式
Flyweight 模式是一种结构型设计模式,它通过共享尽可能多的相同对象来减少内存使用,从而提高性能。在需要生成大量细粒度对象的场景中,Flyweight 模式非常有用。通过共享对象,Flyweight 模式可以显著减少内存消耗,并提高应用程序的效率。
实例代码
// bigChar.ts
export class BigChar {
charname: string;
fontdata: string;
static fontdatas: { [key: string]: string } = {
"0":
".....######.....\n" +
"....##....##....\n" +
"...##......##...\n" +
"..##........##..\n" +
"..##........##..\n" +
"..##........##..\n" +
"..##........##..\n" +
"..##........##..\n" +
"...##......##...\n" +
"....##....##....\n" +
".....######.....\n",
"1":
".......##.......\n" +
".....####.......\n" +
".......##.......\n" +
".......##.......\n" +
".......##.......\n" +
".......##.......\n" +
".......##.......\n" +
".......##.......\n" +
".......##.......\n" +
".....######.....\n" +
"................\n",
"2":
".....######.....\n" +
"...##......##...\n" +
"..##........##..\n" +
"..........##....\n" +
".........##.....\n" +
".......##.......\n" +
"......##........\n" +
".....##.........\n" +
"...##...........\n" +
"....##########..\n" +
"................\n",
"3":
".....######.....\n" +
"...##......##...\n" +
"..##........##..\n" +
"..........##....\n" +
".........##.....\n" +
"......####......\n" +
".........##.....\n" +
"..........##....\n" +
"..##........##..\n" +
"...##......##...\n" +
".....######.....\n",
"4":
"........###.....\n" +
".......####.....\n" +
"......##.##.....\n" +
".....##..##.....\n" +
"....##...##.....\n" +
"...##....##.....\n" +
"..##########....\n." +
"........##......\n" +
"........##......\n" +
".......######...\n",
"5":
"....########....\n" +
"....########....\n" +
"....##..........\n" +
"....##..........\n" +
"....##..........\n" +
"....##..........\n" +
"....##..........\n" +
"....##..........\n" +
"....######......\n" +
"....######......\n" +
"..........##....\n" +
"..........##....\n" +
"...........##...\n" +
"...........##...\n" +
"...........##...\n" +
"...........##...\n" +
"..##.......##...\n" +
"..##.......##...\n" +
"...##......##...\n" +
"...##......##...\n" +
".....######.....\n",
"6":
".......####.....\n" +
"......##........\n" +
".....##.........\n" +
"....##..........\n" +
"...##...........\n" +
"..######........\n" +
"..##...##.......\n" +
"..##....##......\n" +
"..##.....##.....\n" +
"...##....##.....\n" +
"....####........\n",
"7":
"..##########....\n" +
"..##......##....\n" +
"........##......\n" +
".......##.......\n" +
"......##........\n" +
"......##........\n" +
".....##.........\n" +
".....##.........\n" +
"....##..........\n" +
"....##..........\n" +
"....##..........\n",
"8":
".....######.....\n" +
"...##......##...\n" +
"..##........##..\n" +
"..##........##..\n" +
"...##......##...\n" +
"....######......\n" +
"...##......##...\n" +
"..##........##..\n" +
"..##........##..\n" +
"...##......##...\n" +
".....######.....\n",
"9":
".....######.....\n" +
"...##......##...\n" +
"..##........##..\n" +
"..##........##..\n" +
"...##......##...\n" +
"....#######.....\n" +
"..........##....\n" +
".........##.....\n" +
"........##......\n" +
"......###.......\n" +
".....##.........\n",
};
constructor(charname: string) {
this.charname = charname;
this.fontdata = BigChar.fontdatas[charname];
this.fontdata = this.fontdata.replace(/#/g, charname);
}
print(): void {
console.log(this.fontdata);
}
}
// bigCharFactory.ts
import { BigChar } from './bigChar';
export class BigCharFactory {
private pool: Map<string, BigChar> = new Map();
private static singleton: BigCharFactory = new BigCharFactory();
private constructor() { }
public static getInstance(): BigCharFactory {
return BigCharFactory.singleton;
}
public getBigChar(charname: string): BigChar {
let bc: BigChar = this.pool.get(charname);
// ts 是单线程的,所以不需要考虑多线程问题, 若为多线程则需要加锁
if (bc == null) {
bc = new BigChar(charname);
this.pool.set(charname, bc);
}
return bc;
}
}
// bigString.ts
import { Factory } from '../abstractFactory/factory';
import { BigChar } from './bigChar';
import { BigCharFactory } from './bigCharFactory';
export class BigString {
bigchars: BigChar[] = [];
constructor(public str: string) {
let factory = BigCharFactory.getInstance();
str.split('').forEach((char) => {
this.bigchars.push(factory.getBigChar(char));
});
}
print() {
this.bigchars.forEach((bigchar) => {
bigchar.print();
});
}
}
// main.ts
import { BigString } from './bigString';
let bs: BigString = new BigString('123');
bs.print();
运行结果
PS design_patern> ts-node "d:\code\design_patern\src\flyweight\main.ts"
.......11.......
.....1111.......
.......11.......
.......11.......
.......11.......
.......11.......
.......11.......
.......11.......
.......11.......
.....111111.....
................
.....222222.....
...22......22...
..22........22..
..........22....
.........22.....
.......22.......
......22........
.....22.........
...22...........
....2222222222..
................
.....333333.....
...33......33...
..33........33..
..........33....
.........33.....
......3333......
.........33.....
..........33....
..33........33..
...33......33...
.....333333.....
拓展思路的要点
对多个地方产生影响
Flyweight 模式的主题是“共享”。那么,在共享实例时应当注意什么呢?首先要想到的是“如果要改变被共享的对象,就会对多个地方产生影响”。也就是说,一个实例的改变会同时反映到所有使用该实例的地方。例如,假设我们改变了示例程序中 BigChar 类的 '3' 所对应的字体数据,那么 BigString 类中使用的所有 '3' 字符的字体(形状)都会发生改变。在编程时,像这样改一个地方会对多个地方产生影响并非总是不好。有些情况下这是好事,有些情况下这是坏事。不管怎样,“修改一个地方会对多个地方产生影响”,这就是共享的特点。
因此,在决定 Flyweight 角色中的字段时,需要精挑细选。只将那些真正应该在多个地方共享的字段定义在 Flyweight 角色中即可。关于这一点,让我们简单地举个例子。假设我们要在示例程序中增加一个功能,实现显示“带颜色的大型文字”。那么此时,颜色信息应当放在哪个类中呢?首先,假设我们将颜色信息放在 BigChar 类中。由于 BigChar 类的实例是被共享的,因此颜色信息也被共享了。也就是说,BigString 类中用到的所有 BigChar 类的实例都带有相同的颜色。如果我们不把颜色信息放在 BigChar 类中,而是将它放在 BigString 类中。那么 BigString 类会负责管理“第三个字符的颜色是红色的”这样的颜色信息。这样一来,我们就可以实现以不同的颜色显示同一个 BigChar 类的实例。
那么两种解决方案到底哪个是正确的呢?关于这个问题,其实并没有绝对的答案。哪些信息应当共享,哪些信息不应当共享,这取决于类的使用目的。设计者在使用 Flyweight 模式共享信息时必须仔细思考应当共享哪些信息。
Intrinsic 与 Extrinsic
前面讲到的“应当共享的信息和不应当共享的信息”是有专有名词的。应当共享的信息被称作 Intrinsic 信息。Intrinsic 的意思是“本质的”“固有的”。换言之,它指的是不论实例在哪里、不论在什么情况下都不会改变的信息,或是不依赖于实例状态的信息。
在示例程序中,BigChar 的字体数据不论在 BigString 中的哪个地方都不会改变。因此,BigChar 的字体数据属于 Intrinsic 信息。另一方面,不应当共享的信息被称作 Extrinsic 信息。Extrinsic 的意思是“外在的”“非本质的”。也就是说,它是当实例的位置、状况发生改变时会变化的信息,或是依赖于实例状态的信息。在示例程序中,BigChar 的实例在 BigString 中是第几个字符这种信息会根据 BigChar 在 BigString 中的位置变化而发生变化,因此,不应当在 BigChar 中保存这个信息,它属于 Extrinsic 信息。因此,前面提到的是否共享“颜色”信息这个问题,我们也可以换种说法,即应当将“颜色”看作是 Intrinsic 信息还是 Extrinsic 信息。
不要让被共享的实例被垃圾回收器回收了
在有垃圾回收的语言中要注意共享内存未被使用时被 GC 的问题。在使用 Flyweight 模式时,我们通常会将共享对象存储在某种集合(如 HashMap 或 List)中,以便重复使用这些对象。然而,如果这些集合中的对象没有被其他任何地方引用,垃圾回收器可能会认为这些对象是垃圾,从而将其回收掉。这会导致共享对象在需要时无法被找到,从而引发程序错误。
为了避免这种情况,我们需要确保共享对象在整个程序生命周期内都被引用。以下是一些常见的解决方案:
强引用:将共享对象存储在一个全局的集合中,并确保这个集合在程序的整个生命周期内都存在。这种方法简单直接,但可能会导致内存泄漏,因为共享对象永远不会被回收。
弱引用:使用弱引用(WeakReference)来存储共享对象。弱引用允许垃圾回收器在没有其他强引用时回收对象,从而避免内存泄漏。Java 提供了
WeakHashMap
类,可以用来存储弱引用的键值对。引用计数:通过引用计数来管理共享对象的生命周期。每当一个对象被引用时,增加计数;每当一个引用被释放时,减少计数。当计数为零时,表示该对象不再被使用,可以安全地回收。
定期清理:定期检查集合中的对象,移除那些不再被使用的对象。这种方法需要额外的逻辑来判断对象是否仍然被使用,但可以有效地管理内存。
内存之外的其它资源
在示例程序中,我们了解到共享实例可以减少内存使用量。一般来说,共享实例可以减少所需资源的使用量。这里的资源指的是计算机中的资源,而内存是资源中的一种。时间也是一种资源。使用 new 关键字生成实例会花费时间。通过 Flyweight 模式共享实例可以减少使用 new 关键字生成实例的次数。这样,就可以提高程序运行速度。文件句柄(文件描述符)和窗口句柄等也都是一种资源。在操作系统中,可以同时使用的文件句柄和窗口句柄是有限制的。因此,如果不共享实例,应用程序在运行时很容易就会达到资源极限而导致崩溃。
相关的设计模式
- Proxy 模式
- Composite 模式
- Singleton 模式