- Published on
享元模式详解
- Authors
- Name
- 青雲
享元模式(Flyweight Pattern)是一种结构型设计模式,关注于通过共享机制来有效地支持大量细粒度对象的复用,以减少应用程序的内存使用。这种模式尤其适用于那些对象数量众多,且对象状态大部分可以外部化的场景。
为什么需要享元模式?
一所图书馆拥有成千上万本书,如果每次读者想阅读一本书时,图书馆就购买一本新书供其阅读,那么图书馆将需要无比巨大的空间来存放这些书,并且这种做法在经济上也是不可行的。为了解决这个问题,图书馆采用了一种“共享”策略。也就是说,当一位读者借阅某本书之后,这本书并不是他/她个人的所有物,读者阅读完毕后需要归还,这样其他读者也可以借阅。
在这个例子中,每一本书可以看作是一个“享元”,它被多个读者共享阅读。图书馆只需维护一定数量的书籍——任何时候都足够满足读者的阅读需求,而不是为每个读者购买新书,从而显著降低了成本。
将这个概念应用到软件开发中,享元模式允许系统重用对象,减少系统创建对象的数量,节省资源。在前端开发中,享元模式可以用于优化DOM元素的使用,或是在处理大量具有相似属性和方法的对象时提升性能。例如,当你在一个页面上展示成千上万条评论时,通过享元模式重用DOM节点或者对象的属性,可以极大减少浏览器的负担,提升页面的性能。
基本概念
享元模式将对象的状态分为内部状态和外部状态:
- 内部状态:相同的对象之间共享的一部分状态,不会随上下文的变化而变化。
- 外部状态:依赖于上下文并随环境改变而改变的状态。
享元对象不可变,这意味着它们的内部状态在创建后不能改变。外部状态在每次方法调用时通过参数传递给享元对象。 核心组成:
- 享元对象(Flyweight):这是被共享的对象,它能够提供内部(不变的)状态和外部(变化的)状态。内部状态存储在享元对象内部,通常不会随环境的变化而变化;外部状态由客户端对象存储,并在需要的时候传递给享元对象。
- 享元工厂(Flyweight Factory):负责创建和管理享元对象。当客户端请求一个享元对象时,工厂检查是否已经创建过这样的对象,如果已经创建,就返回现有的享元对象;如果没有,则创建一个新的享元对象。
- 客户端(Client):持有对享元对象的引用。它负责计算或存储享元对象的外部状态。
实现示例
通过一个实际示例来展示享元模式的实现过程。假设我们要在一个图形编辑器中绘制不同颜色的圆形,我们可以通过享元模式来优化内存使用。
定义享元接口和具体享元类
// 享元接口,定义共享对象的方法
interface Shape {
draw(x: number, y: number, color: string): void;
}
// 具体享元类,表示可以共享的圆形对象
class Circle implements Shape {
private readonly radius: number;
constructor() {
this.radius = 5; // 共享的内部状态
}
draw(x: number, y: number, color: string): void {
console.log(`Drawing a ${color} circle at (${x}, ${y}) with radius ${this.radius}`);
}
}
定义享元工厂类
// 享元工厂类,用于创建和管理享元对象
class ShapeFactory {
private circleMap: Map<string, Shape> = new Map();
getCircle(color: string): Shape {
let circle = this.circleMap.get(color);
if (!circle) {
circle = new Circle();
this.circleMap.set(color, circle);
console.log(`Created a new circle of color: ${color}`);
}
return circle;
}
}
使用享元模式绘制图形
const shapeFactory = new ShapeFactory();
const colors = ["Red", "Green", "Blue", "Red", "Blue", "Green"];
for (let i = 0; i < colors.length; i++) {
const circle = shapeFactory.getCircle(colors[i]);
circle.draw(i * 10, i * 20, colors[i]);
}
// 输出
Created a new circle of color: Red
Drawing a Red circle at (0, 0) with radius 5
Created a new circle of color: Green
Drawing a Green circle at (10, 20) with radius 5
Created a new circle of color: Blue
Drawing a Blue circle at (20, 40) with radius 5
Drawing a Red circle at (30, 60) with radius 5
Drawing a Blue circle at (40, 80) with radius 5
Drawing a Green circle at (50, 100) with radius 5
总结
Shape
是一个接口,声明了一个draw
方法。这个接口定义了所有形状对象必须实现的功能,即在某个位置以某种颜色绘制自己。Circle
是一个实现了Shape
接口的类,表示圆形。它有一个私有属性radius
,用于存储圆的半径大小。同时,它重写了draw
方法,具体实现了如何绘制一个圆形。Circle
作为一个具体的形状,是享元对象的一种实现。ShapeFactory
是一个工厂类,它的主要职责是管理和提供Shape
对象。它内部维护了一个名为circleMap
的Map
,这个Map
的键是颜色(string类型),值是对应颜色的Circle
对象。getCircle
方法接收一个颜色字符串作为参数,然后它检查circleMap
中是否已经有了这个颜色的圆形。如果有,就直接返回现有的圆形对象;如果没有,就创建一个新的圆形对象,存储到circleMap
中,然后返回这个新创建的对象。
这种方式体现了享元模式的本质——即通过共享来避免大量相似对象的重复创建,从而节省资源。
应用场景
享元模式适用于以下场景:
- 难以承受的大量对象:例如图形应用、游戏开发中的粒子系统等,需要创建大量相同或相似的对象。
- 性能要求较高:需要优化内存使用和提高运行效率的情况下。
- 共享状态:对象中有可以共享的部分状态,可以利用共享来减少内存占用。
DOM元素的重用
在动态生成大量类似的 DOM 元素(如列表项、表格行)时,频繁地创建和销毁这些元素可能会导致性能问题和内存泄漏。通过享元模式,可以重用 DOM 元素而不是频繁地创建和销毁,从而减少页面重绘和回流的次数,提高性能。
案例:虚拟滚动(Virtual Scrolling)
虚拟滚动是享元模式的一个典型应用。当用户在一个长列表中滚动时,只渲染可视区域内的元素,并重用这些元素来显示新的数据。这样无论列表有多长,DOM 中的元素数量保持不变,从而显著提高性能。
class VirtualScroll {
private container: HTMLElement;
private itemHeight: number;
private items: HTMLElement[] = [];
private totalItems: number;
private viewportHeight: number;
constructor(container: HTMLElement, itemHeight: number, totalItems: number) {
this.container = container;
this.itemHeight = itemHeight;
this.totalItems = totalItems;
this.viewportHeight = container.clientHeight;
this.initialize();
}
initialize() {
const itemCount = Math.ceil(this.viewportHeight / this.itemHeight);
for (let i = 0; i < itemCount; i++) {
const item = document.createElement('div');
item.style.height = `${this.itemHeight}px`;
this.items.push(item);
this.container.appendChild(item);
}
this.container.addEventListener('scroll', this.onScroll.bind(this));
this.updateItems(0);
}
onScroll() {
const scrollTop = this.container.scrollTop;
const firstVisibleIndex = Math.floor(scrollTop / this.itemHeight);
this.updateItems(firstVisibleIndex);
}
updateItems(startIndex: number) {
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
const itemIndex = startIndex + i;
if (itemIndex < this.totalItems) {
item.textContent = `Item ${itemIndex + 1}`;
item.style.display = 'block';
} else {
item.style.display = 'none';
}
}
}
}
// 使用 VirtualScroll
const container = document.getElementById('scrollContainer');
const totalItems = 1000; // 假设列表有 1000 项
const itemHeight = 30; // 每项的高度为 30px
new VirtualScroll(container, itemHeight, totalItems);
Canvas 绘图优化
在使用 Canvas 绘制复杂或大量图形(如游戏、数据可视化)时,绘图操作本身可能非常耗时。享元模式允许共享和重用相似图形的属性和状态,减少不必要的绘图操作。
案例:享元对象池
在一个简单的游戏中,可能有成百上千个相似的对象(如子弹、星星等)。通过享元模式,可以将这些对象的共享属性(如颜色、大小)提取出来,只在必要时绘制区别属性(如位置),减少了绘制开销。
const canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
const context = canvas.getContext('2d');
// 享元对象池
const starPool: Star[] = [];
function getStar(color: string, size: number): Star {
let star = starPool.find(s => s.color === color && s.size === size);
if (!star) {
star = new Star(color, size);
starPool.push(star);
console.log(`Created new star: ${color}, ${size}`);
}
return star;
}
// 初始化并绘制星星
function drawStars() {
const colors = ["Red", "Green", "Blue"];
for (let i = 0; i < 1000; i++) {
const color = colors[Math.floor(Math.random() * colors.length)];
const size = Math.floor(Math.random() * 5) + 5;
const x = Math.floor(Math.random() * canvas.width);
const y = Math.floor(Math.random() * canvas.height);
const star = getStar(color, size);
star.draw(context, x, y);
}
}
drawStars();
数据共享与内存管理
在数据密集型的前端应用中,如大型表格或报表,享元模式可以减少数据对象的创建,优化内存使用。
案例:股票市场的仪表盘应用
在一个股票市场的仪表盘应用中,可能需要显示成千上万个股票的实时数据。如果每个股票都创建一个完整的数据对象,会消耗大量内存。通过享元模式,可以共享那些相同的数据字段,比如市场类型、股票代码等,只为每个股票创建必要的独特数据属性,显著减少了内存使用。
- 定义享元接口和具体享元类
// 享元接口,定义共享对象的方法
interface Stock {
display(uniqueData: StockUniqueData): void;
}
// 表示股票独特数据的接口
interface StockUniqueData {
price: number;
volume: number;
}
// 具体享元类,表示共享的股票信息
class StockShared implements Stock {
constructor(private readonly marketType: string, private readonly code: string) {}
display(uniqueData: StockUniqueData): void {
console.log(`Stock [Market: ${this.marketType}, Code: ${this.code}, Price: ${uniqueData.price}, Volume: ${uniqueData.volume}]`);
}
}
- 定义享元工厂类
// 享元工厂类,用于创建和管理享元对象
class StockFactory {
private stockMap: Map<string, Stock> = new Map();
getStock(marketType: string, code: string): Stock {
const key = `${marketType}-${code}`;
if (!this.stockMap.has(key)) {
const stock = new StockShared(marketType, code);
this.stockMap.set(key, stock);
console.log(`Created new shared stock: [Market: ${marketType}, Code: ${code}]`);
}
return this.stockMap.get(key);
}
}
- 使用享元模式管理股票数据
const stockFactory = new StockFactory();
function displayStocks() {
const stocksData = [
{ marketType: "NYSE", code: "AAPL", price: 150, volume: 1000 },
{ marketType: "NYSE", code: "AAPL", price: 151, volume: 1200 },
{ marketType: "NASDAQ", code: "GOOG", price: 2800, volume: 300 },
{ marketType: "NASDAQ", code: "GOOG", price: 2820, volume: 400 },
{ marketType: "NYSE", code: "TSLA", price: 700, volume: 500 },
{ marketType: "NYSE", code: "TSLA", price: 710, volume: 450 }
];
stocksData.forEach(data => {
const stock = stockFactory.getStock(data.marketType, data.code);
const uniqueData: StockUniqueData = { price: data.price, volume: data.volume };
stock.display(uniqueData);
});
}
displayStocks();
享元模式优缺点
优点
- 减少内存消耗:通过共享相同的对象,显著减少内存消耗,提高系统性能。
- 提高效率:减少对象创建的开销,提高系统响应速度。
- 灵活性:享元模式将对象状态分为内部状态和外部状态,提高了对象管理的灵活性。
缺点
- 增加代码复杂性:通过引入享元工厂和享元对象,增加了代码的复杂性和维护难度。
- 不适用于所有场景:享元模式适用于具有大量相同或相似对象的场景,如果对象之间差异较大,享元模式的效果会大打折扣。
总结
享元模式通过共享相同的对象来减少内存消耗和对象创建的开销,提升系统性能。它特别适合于需要大量细粒度对象的场景,如图形应用、游戏开发中的粒子系统、以及前端开发中的虚拟滚动和数据密集型应用等。在实际应用中,合理使用享元模式可以显著提升系统性能,但也需注意其代码复杂性和适用场景。