Skip to content

享元模式(Flyweight)

前言

享元模式通过共享对象来减少内存使用,适用于大量相似对象的场景。它将对象状态分为内部状态(可共享)和外部状态(不可共享),在游戏开发、文本编辑等领域应用广泛。本文将详细讲解享元模式的核心原理及实际应用。


一、核心概念

1.1 定义

运用共享技术有效地支持大量细粒度的对象。

1.2 核心概念

概念说明示例
内部状态可共享,存储在享元对象中棋子颜色(黑/白)
外部状态不可共享,由客户端传入棋子位置(x, y)
享元工厂管理享元对象的创建和共享棋子工厂

1.3 核心角色

角色说明
享元接口(Flyweight)定义享元对象接口
具体享元(ConcreteFlyweight)实现接口,存储内部状态
享元工厂(FlyweightFactory)创建和管理享元对象
客户端(Client)维护外部状态,使用享元

二、代码实现

2.1 基础实现

php
<?php
// 享元接口
interface Flyweight
{
    public function operation(string $extrinsicState): void;
}

// 具体享元:存储内部状态
class ConcreteFlyweight implements Flyweight
{
    private string $intrinsicState; // 内部状态(可共享)

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

    public function operation(string $extrinsicState): void
    {
        echo "Flyweight: intrinsic={$this->intrinsicState}, extrinsic={$extrinsicState}" . PHP_EOL;
    }
}

// 享元工厂:管理共享对象
class FlyweightFactory
{
    /** @var array<string, Flyweight> */
    private array $flyweights = [];

    public function getFlyweight(string $key): Flyweight
    {
        // 已存在则复用
        if (isset($this->flyweights[$key])) {
            echo "Reusing existing flyweight: {$key}" . PHP_EOL;
            return $this->flyweights[$key];
        }

        // 不存在则创建并缓存
        echo "Creating new flyweight: {$key}" . PHP_EOL;
        $flyweight = new ConcreteFlyweight($key);
        $this->flyweights[$key] = $flyweight;
        return $flyweight;
    }

    public function getCount(): int
    {
        return count($this->flyweights);
    }
}

// 使用
$factory = new FlyweightFactory();

$fw1 = $factory->getFlyweight("A");
$fw1->operation("State 1");

$fw2 = $factory->getFlyweight("A"); // 复用已有实例
$fw2->operation("State 2");

$fw3 = $factory->getFlyweight("B");
$fw3->operation("State 3");

echo "Total flyweights: {$factory->getCount()}" . PHP_EOL; // 2

2.2 实际案例:围棋游戏

php
<?php
// 棋子享元接口
interface ChessPiece
{
    public function display(int $x, int $y): void;
}

// 具体棋子享元:只存储颜色(内部状态)
class ConcreteChessPiece implements ChessPiece
{
    public function __construct(private string $color) {}

    public function display(int $x, int $y): void
    {
        echo "{$this->color} piece at ({$x}, {$y})" . PHP_EOL;
    }
}

// 棋子工厂:管理共享的棋子
class ChessPieceFactory
{
    /** @var array<string, ChessPiece> */
    private array $pieces = [];

    public function getChessPiece(string $color): ChessPiece
    {
        if (!isset($this->pieces[$color])) {
            $this->pieces[$color] = new ConcreteChessPiece($color);
        }
        return $this->pieces[$color];
    }

    public function getTotalPieces(): int
    {
        return count($this->pieces);
    }
}

// 棋盘:维护外部状态(位置)
class ChessBoard
{
    private ChessPieceFactory $factory;
    /** @var array<string, array{piece: ChessPiece, x: int, y: int}> */
    private array $positions = [];

    public function __construct()
    {
        $this->factory = new ChessPieceFactory();
    }

    public function place(string $color, int $x, int $y): void
    {
        $piece = $this->factory->getChessPiece($color);
        $key = "{$x},{$y}";
        $this->positions[$key] = ['piece' => $piece, 'x' => $x, 'y' => $y];
    }

    public function display(): void
    {
        foreach ($this->positions as $item) {
            $item['piece']->display($item['x'], $item['y']);
        }
    }

    public function getTotalFlyweights(): int
    {
        return $this->factory->getTotalPieces();
    }
}

// 使用:1000 个棋子只创建 2 个享元对象
$board = new ChessBoard();

for ($i = 0; $i < 500; $i++) {
    $board->place("Black", $i, $i);
    $board->place("White", $i, $i + 1);
}

// $board->display(); // 只显示部分
echo "Flyweight objects: {$board->getTotalFlyweights()}" . PHP_EOL; // 2

2.3 实际案例:文本编辑器字符

php
<?php
// 字符享元
interface CharacterFlyweight
{
    public function display(int $position): void;
}

class Character implements CharacterFlyweight
{
    public function __construct(
        private string $char,  // 内部状态:字符
        private string $font,  // 内部状态:字体
        private int $size      // 内部状态:字号
    ) {}

    public function display(int $position): void
    {
        echo "'{$this->char}' [{$this->font}, {$this->size}px] at pos {$position}" . PHP_EOL;
    }
}

// 字符工厂
class CharacterFactory
{
    /** @var array<string, Character> */
    private array $cache = [];

    public function getCharacter(string $char, string $font, int $size): Character
    {
        $key = "{$char}_{$font}_{$size}";
        if (!isset($this->cache[$key])) {
            $this->cache[$key] = new Character($char, $font, $size);
        }
        return $this->cache[$key];
    }

    public function getCacheSize(): int
    {
        return count($this->cache);
    }
}

// 使用
$charFactory = new CharacterFactory();
$text = "Hello World";

// 相同字符+字体+字号复用
foreach (str_split($text) as $i => $char) {
    $c = $charFactory->getCharacter($char, "Arial", 12);
    $c->display($i);
}

echo "Cache size: {$charFactory->getCacheSize()}" . PHP_EOL;
// 'l' 出现 3 次只创建 1 个享元

三、适用场景

场景说明
大量相似对象棋子、字符、图标
对象创建成本高数据库连接、图片资源
内存优化减少对象数量
缓存池连接池、线程池
游戏开发粒子系统、树/草渲染

四、优缺点分析

优点缺点
大幅减少内存使用需要区分内外部状态
提高性能代码复杂度增加
集中管理共享对象线程安全需考虑
减少对象创建外部状态管理复杂

五、常见踩坑与问题排查

5.1 线程安全问题

php
<?php
// 问题:多进程/多线程同时创建享元对象
class UnsafeFactory
{
    /** @var array<string, Flyweight> */
    private array $cache = [];

    public function getFlyweight(string $key): Flyweight
    {
        if (!isset($this->cache[$key])) {
            // 多进程可能同时到达这里,创建多个实例
            $this->cache[$key] = new ConcreteFlyweight($key);
        }
        return $this->cache[$key];
    }
}

// 解决:使用文件锁或共享内存扩展(如 APCu / Swoole Lock)
class SafeFactory
{
    /** @var array<string, Flyweight> */
    private array $cache = [];
    private \Mutex $lock;

    public function getFlyweight(string $key): Flyweight
    {
        if (!isset($this->cache[$key])) {
            // 使用锁保证原子性(伪代码示意,PHP 中可使用 Swoole\Lock)
            $this->lock->lock();
            try {
                if (!isset($this->cache[$key])) {
                    $this->cache[$key] = new ConcreteFlyweight($key);
                }
            } finally {
                $this->lock->unlock();
            }
        }
        return $this->cache[$key];
    }
}

5.2 对象池与享元的区别

php
<?php
// 对象池:对象可变,用完归还,可被不同客户端使用
// 享元:对象不可变(内部状态固定),可被多个客户端同时共享

// 享元对象应该是不可变的
class ImmutableFlyweight implements Flyweight
{
    public function __construct(private readonly string $state) {}

    public function operation(string $extrinsic): void
    {
        // 不修改 $this->state
    }
}

六、优化方案与进阶

6.1 组合享元

php
<?php
// 复合享元:组合多个简单享元
class CompositeFlyweight implements Flyweight
{
    /** @var array<string, Flyweight> */
    private array $flyweights = [];

    public function add(string $key, Flyweight $flyweight): void
    {
        $this->flyweights[$key] = $flyweight;
    }

    public function operation(string $extrinsicState): void
    {
        foreach ($this->flyweights as $fw) {
            $fw->operation($extrinsicState);
        }
    }
}

6.2 结合 LRU 缓存

php
<?php
class LRUFlyweightFactory
{
    /** @var array<string, Flyweight> */
    private array $cache = [];
    private int $maxSize;

    public function __construct(int $maxSize = 100)
    {
        $this->maxSize = $maxSize;
    }

    public function getFlyweight(string $key): Flyweight
    {
        if (isset($this->cache[$key])) {
            // 移到最新位置
            $value = $this->cache[$key];
            unset($this->cache[$key]);
            $this->cache[$key] = $value;
            return $value;
        }

        // 超出容量,淘汰最老的
        if (count($this->cache) >= $this->maxSize) {
            $oldestKey = array_key_first($this->cache);
            unset($this->cache[$oldestKey]);
        }

        $flyweight = new ConcreteFlyweight($key);
        $this->cache[$key] = $flyweight;
        return $flyweight;
    }
}

七、全文总结

享元模式的核心是 通过共享对象减少内存使用,分离内部状态和外部状态

核心要点

  1. 内部状态可共享,外部状态由客户端传入
  2. 享元工厂管理共享对象的创建和复用
  3. 适用于大量相似对象的场景
  4. 享元对象应该是不可变的
  5. 可与 LRU 缓存、组合模式结合使用