TypeScript设计模式:轻量级模式

3 阅读6分钟

轻量级模式(Flyweight Pattern)是一种结构型设计模式,旨在通过共享对象来减少内存使用和提高性能,适用于需要创建大量相似对象的场景。它将对象的内在状态(共享、不变的部分)与外在状态(变化、上下文相关的部分)分离,通过复用内在状态减少对象实例数量。

设计模式原理

轻量级模式的核心是通过工厂类管理共享的轻量级对象(Flyweight),将内在状态存储在共享对象中,外在状态由客户端提供,从而减少内存占用。例如,在文本编辑器中,相同的字符(如“A”)可能在文档中出现多次,但只需要一个共享对象存储其字体和样式,位置信息则由上下文动态提供。

结构

  • 轻量级接口(Flyweight):定义操作方法,接收外在状态。
  • 具体轻量级(ConcreteFlyweight):实现轻量级接口,存储内在状态。
  • 轻量级工厂(FlyweightFactory):管理共享的轻量级对象,确保相同内在状态的对象只创建一次。
  • 客户端(Client):维护外在状态,调用轻量级对象执行操作。

优点

  • 内存优化:通过共享内在状态,大幅减少对象数量和内存使用。
  • 性能提升:减少对象创建开销,适合大量相似对象场景。
  • 可扩展:通过工厂模式,易于添加新共享对象。
  • 职责分离:内在和外在状态分离,逻辑清晰。

缺点

  • 复杂性增加:需要维护工厂和状态分离逻辑,代码复杂度可能上升。
  • 外在状态管理:客户端需自行管理外在状态,增加负担。
  • 线程安全:共享对象需考虑并发访问问题。
  • 适用局限:仅适合大量相似对象的场景。

适用场景

  • 大量相似对象:如文本编辑器中的字符、游戏中的精灵。
  • 内存敏感系统:如移动端应用、嵌入式设备。
  • 重复状态:对象有大量可共享的内在状态。
  • 渲染系统:如GUI框架、图形编辑器。

在字符渲染系统中,轻量级模式通过共享字符的字体和样式,显著减少内存占用,同时通过位置信息灵活渲染。

TypeScript 实现示例

我们实现一个经典的文本编辑器字符渲染系统,通过轻量级模式共享字符的字体和样式,管理字符的坐标位置。假设文档中有大量重复的字符(如“A”或“B”),我们只为每种字符和样式组合创建一个对象,位置信息由客户端维护。代码使用TypeScript确保类型安全。

项目结构

text-editor/
├── src/
│   ├── flyweight.ts     // 轻量级接口和具体轻量级
│   ├── factory.ts      // 轻量级工厂
│   ├── editor.ts       // 客户端(文本编辑器)
│   ├── main.ts         // 执行示例
├── tsconfig.json
├── package.json

1. 安装依赖

npm init -y
npm install typescript @types/node
npx tsc --init

配置 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,
    "moduleResolution": "node"
  }
}

2. 定义轻量级接口和具体轻量级 (flyweight.ts)

export interface CharacterFlyweight {
  render(extrinsicState: { x: number; y: number }): void;
}

export class ConcreteCharacterFlyweight implements CharacterFlyweight {
  constructor(private char: string, private font: string, private fontSize: number) {
    console.log(`创建轻量级字符:字符="${char}", 字体="${font}", 字号=${fontSize}`);
  }

  render(extrinsicState: { x: number; y: number }): void {
    console.log(`渲染字符 "${this.char}" 在位置 (${extrinsicState.x}, ${extrinsicState.y}), 字体="${this.font}", 字号=${this.fontSize}`);
  }
}

说明ConcreteCharacterFlyweight存储内在状态(charfontfontSize),render方法接收外在状态(xy坐标)来渲染字符。

3. 实现轻量级工厂 (factory.ts)

import { CharacterFlyweight, ConcreteCharacterFlyweight } from './flyweight';

export class CharacterFlyweightFactory {
  private flyweights: Map<string, CharacterFlyweight> = new Map();

  getFlyweight(char: string, font: string, fontSize: number): CharacterFlyweight {
    const key = `${char}_${font}_${fontSize}`;
    if (!this.flyweights.has(key)) {
      this.flyweights.set(key, new ConcreteCharacterFlyweight(char, font, fontSize));
    }
    return this.flyweights.get(key)!;
  }

  getFlyweightCount(): number {
    return this.flyweights.size;
  }
}

说明:工厂通过Map缓存字符对象,确保相同字符和样式组合(如“A, Arial, 12”)只创建一次。getFlyweight方法返回共享对象。

4. 实现客户端 (editor.ts)

import { CharacterFlyweight, CharacterFlyweightFactory } from './factory';

interface Character {
  char: string;
  font: string;
  fontSize: number;
  x: number;
  y: number;
}

export class TextEditor {
  private characters: Character[] = [];
  private factory: CharacterFlyweightFactory;

  constructor() {
    this.factory = new CharacterFlyweightFactory();
  }

  addCharacter(char: string, font: string, fontSize: number, x: number, y: number): void {
    this.characters.push({ char, font, fontSize, x, y });
    console.log(`添加字符 "${char}" 到文档,位置=(${x}, ${y}), 字体="${font}", 字号=${fontSize}`);
  }

  renderDocument(): void {
    console.log('开始渲染文档...');
    this.characters.forEach(({ char, font, fontSize, x, y }) => {
      const flyweight = this.factory.getFlyweight(char, font, fontSize);
      flyweight.render({ x, y });
    });
    console.log(`文档渲染完成,共享字符对象数:${this.factory.getFlyweightCount()}`);
  }
}

说明TextEditor作为客户端,维护外在状态(字符的xy坐标),通过工厂获取共享的字符对象来渲染文档。

5. 运行示例 (main.ts)

import { TextEditor } from './editor';

// 创建文本编辑器
const editor = new TextEditor();

// 添加字符(模拟输入“Hello”)
editor.addCharacter('H', 'Arial', 12, 10, 20);
editor.addCharacter('e', 'Arial', 12, 15, 20);
editor.addCharacter('l', 'Arial', 12, 20, 20);
editor.addCharacter('l', 'Arial', 12, 25, 20);
editor.addCharacter('o', 'Arial', 12, 30, 20);
editor.addCharacter('H', 'Times', 14, 10, 30); // 不同字体
editor.addCharacter('e', 'Arial', 12, 15, 30); // 重复字符

// 渲染文档
editor.renderDocument();

6. 编译与运行

npx tsc
node dist/main.js

运行后,控制台输出类似:

添加字符 "H" 到文档,位置=(10, 20), 字体="Arial", 字号=12
创建轻量级字符:字符="H", 字体="Arial", 字号=12
添加字符 "e" 到文档,位置=(15, 20), 字体="Arial", 字号=12
创建轻量级字符:字符="e", 字体="Arial", 字号=12
添加字符 "l" 到文档,位置=(20, 20), 字体="Arial", 字号=12
创建轻量级字符:字符="l", 字体="Arial", 字号=12
添加字符 "l" 到文档,位置=(25, 20), 字体="Arial", 字号=12
添加字符 "o" 到文档,位置=(30, 20), 字体="Arial", 字号=12
创建轻量级字符:字符="o", 字体="Arial", 字号=12
添加字符 "H" 到文档,位置=(10, 30), 字体="Times", 字号=14
创建轻量级字符:字符="H", 字体="Times", 字号=14
添加字符 "e" 到文档,位置=(15, 30), 字体="Arial", 字号=12
开始渲染文档...
渲染字符 "H" 在位置 (10, 20), 字体="Arial", 字号=12
渲染字符 "e" 在位置 (15, 20), 字体="Arial", 字号=12
渲染字符 "l" 在位置 (20, 20), 字体="Arial", 字号=12
渲染字符 "l" 在位置 (25, 20), 字体="Arial", 字号=12
渲染字符 "o" 在位置 (30, 20), 字体="Arial", 字号=12
渲染字符 "H" 在位置 (10, 30), 字体="Times", 字号=14
渲染字符 "e" 在位置 (15, 30), 字体="Arial", 字号=12
文档渲染完成,共享字符对象数:5

解释输出

  • 输入7个字符(“H, e, l, l, o, H, e”),但只有5个独特的字符和样式组合(“H, Arial, 12”, “e, Arial, 12”, “l, Arial, 12”, “o, Arial, 12”, “H, Times, 14”)。
  • 工厂确保重复的字符(如两个“l”和两个“e”)共享相同的轻量级对象,减少内存占用。
  • 外在状态(xy坐标)由TextEditor维护,动态传递给共享对象进行渲染。

总结

轻量级模式的优点在于其内存优化、性能提升、可扩展性和职责分离。通过共享内在状态(如字符的字体和样式),大幅减少对象数量和内存使用;减少对象创建开销,适合大规模对象场景;通过工厂模式易于添加新共享对象;内在和外在状态分离,逻辑清晰。该模式特别适用于大量相似对象、内存敏感系统、重复状态和渲染系统场景,如文本编辑器中的字符渲染、游戏中的精灵管理、AI任务指令系统和图形编辑器。