Skip to content

享元模式

引言

在图形编辑器、游戏开发等场景中,大量相似对象(如粒子效果、字符渲染)会导致内存爆炸性能瓶颈。享元模式通过共享对象而非重复创建,实现“万变不离其宗”的资源复用,成为解决海量细粒度对象问题的经典方案。

诞生背景

GoF在《设计模式》中提出享元模式,应对三大挑战:

  1. 内存过载:十万级粒子对象导致OOM(如游戏场景的树木渲染)
  2. 对象冗余:重复创建本质相同的对象(如文档中相同字体的字符)
  3. 性能瓶颈:高频对象创建/销毁引发GC停顿

演进过程

  • GoF基础(1994):确立内部状态(共享)与外部状态(独享)分离原则
  • 多线程演进:引入线程安全享元工厂(如ConcurrentHashMap管理连接池)
  • 现代应用:前端虚拟DOM复用、数据库连接池、字符编码器

核心概念

  • 享元(Flyweight)
    • 封装共享部分的接口(如字符对象的绘制方法)
  • 具体享元(ConcreteFlyweight)
    • 实现共享对象的内部状态(如字符的Unicode编码)
  • 享元工厂(FlyweightFactory)
    • 创建并缓存享元对象,实现复用
  • 外部状态(External State)
    • 由客户端维护的非共享数据(如字符的位置、颜色)

通用实现

Java 实现

java
// 享元接口
interface TextCharacter {
    void draw(String position);
}

// 具体享元:字符对象
class Character implements TextCharacter {
    private final char symbol; // 内部状态(共享)

    public Character(char symbol) {
        this.symbol = symbol;
    }

    @Override
    public void draw(String position) {
        System.out.println("Draw '" + symbol + "' at " + position); // 外部状态
    }
}

// 享元工厂
class CharacterFactory {
    private final Map<Character, TextCharacter> pool = new HashMap<>();

    public TextCharacter getCharacter(char c) {
        return pool.computeIfAbsent(c, Character::new); // 复用已存在对象
    }
}

// 客户端
class Client {
    public static void main(String[] args) {
        CharacterFactory factory = new CharacterFactory();
        
        TextCharacter a1 = factory.getCharacter('A'); 
        a1.draw("(10,20)"); // Draw 'A' at (10,20)
        
        TextCharacter a2 = factory.getCharacter('A'); // 复用对象
        a2.draw("(30,40)"); // Draw 'A' at (30,40)
        
        System.out.println(a1 == a2); // true (同一对象)
    }
}

PHP 实现

php
// 享元接口
interface Graphic {
    public function render(int $x, int $y): void;
}

// 具体享元:树对象
class Tree implements Graphic {
    private string $type; // 内部状态(共享)

    public function __construct(string $type) {
        $this->type = $type;
    }

    public function render(int $x, int $y): void {
        echo "Render {$this->type} tree at ($x, $y)\n";
    }
}

// 享元工厂
class TreeFactory {
    private array $pool = [];

    public function getTree(string $type): Graphic {
        if (!isset($this->pool[$type])) {
            $this->pool[$type] = new Tree($type);
        }
        return $this->pool[$type];
    }
}

// 客户端
$factory = new TreeFactory();
$tree1 = $factory->getTree('Oak');
$tree1->render(100, 200); // Render Oak tree at (100, 200)

$tree2 = $factory->getTree('Oak'); // 复用对象
$tree2->render(300, 400); // Render Oak tree at (300, 400)

var_dump($tree1 === $tree2); // true (同一对象)

应用场景

  1. 资源密集型对象:游戏中的地形/植被渲染
  2. 高频重复元素:文档编辑器的字符/图标
  3. 内存敏感场景:嵌入式系统的UI组件
  4. 池化技术基础:数据库连接池、线程池

案例:游戏地图渲染

Java 实现

java
// 享元:地形区块
class TerrainTile {
    private final String texture; // 内部状态(共享)
    public TerrainTile(String texture) { 
        this.texture = texture; 
    }
    public void render(int x, int y) {
        System.out.printf("Rendering %s at (%d,%d)\n", texture, x, y);
    }
}

// 工厂
class TileFactory {
    private static final Map<String, TerrainTile> cache = new HashMap<>();
    public static TerrainTile getTile(String texture) {
        return cache.computeIfAbsent(texture, TerrainTile::new);
    }
}

// 客户端
public class GameMap {
    public static void main(String[] args) {
        TerrainTile grass = TileFactory.getTile("grass.png");
        TerrainTile water = TileFactory.getTile("water.png");
        
        grass.render(0, 0);
        grass.render(0, 1); // 复用grass对象
        water.render(1, 0);
    }
}

PHP 实现

php
// 享元:武器模型
class WeaponModel {
    private string $modelPath; // 内部状态(共享)
    public function __construct(string $path) { 
        $this->modelPath = $path; 
    }
    public function display(float $scale): void {
        echo "Display {$this->modelPath} scaled to {$scale}\n";
    }
}

// 工厂
class WeaponFactory {
    private array $pool = [];
    public function getWeapon(string $path): WeaponModel {
        return $this->pool[$path] ??= new WeaponModel($path);
    }
}

// 客户端
$factory = new WeaponFactory();
$sword = $factory->getWeapon('/models/sword.obj');
$sword->display(1.0); // Display /models/sword.obj scaled to 1.0

$sameSword = $factory->getWeapon('/models/sword.obj'); // 复用对象
$sameSword->display(0.8); // Display /models/sword.obj scaled to 0.8

优点

  • 内存优化:减少对象数量(如10万字符→百个共享对象)
  • 性能提升:避免重复创建/GC开销
  • 集中管理:享元工厂统一控制共享逻辑
  • 扩展灵活:新增享元类型不影响已有结构

缺点

  • 逻辑复杂化:需严格分离内部/外部状态
  • 线程安全风险:共享对象需同步机制(如ConcurrentHashMap)
  • 过度共享问题:可能误共享应独立的对象

扩展

  • 复合享元:组合多个享元(如单词=字符享元组合)
  • 懒加载优化:工厂延迟初始化享元对象
  • 分布式共享:Redis缓存享元对象(跨进程复用)

模式协作

  • 与工厂模式:享元工厂是核心实现载体
  • 与组合模式:复合享元实现树形结构(如UI组件树)
  • 与状态模式:外部状态可封装为独立状态对象

延伸思考

  • 缓存与享元边界:享元强调对象粒度复用(如字符),缓存侧重数据结果复用(如数据库查询)
  • GC语言适配:在Go/Rust中需显式管理享元生命周期
  • 不可变设计:享元内部状态必须不可变(避免并发修改)

总结

享元模式是资源优化的空间魔术师,通过共享不变的本质(内部状态)、分离可变的外壳(外部状态),实现“以一当百”的内存效率。其核心价值在于:用共享对抗冗余的存储哲学与以时间换空间的性能权衡。在游戏开发、UI框架等场景中,享元模式能化资源危机为性能优势,成为高密度对象处理的终极武器。