享元模式(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; // 22.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; // 22.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;
}
}七、全文总结
享元模式的核心是 通过共享对象减少内存使用,分离内部状态和外部状态。
核心要点:
- 内部状态可共享,外部状态由客户端传入
- 享元工厂管理共享对象的创建和复用
- 适用于大量相似对象的场景
- 享元对象应该是不可变的
- 可与 LRU 缓存、组合模式结合使用
