PHP 8 享元模式(Flyweight)实战:DesignPatternsPHP 源码与测试深度解析
发布时间:2026/10/1 1:56:03来源:尧图网络
示例工程教程【免费下载链接】DesignPatternsPHPSample code for several design patterns in PHP 8.x项目地址https://gitcode.com/gh_mirrors/de/DesignPatternsPHP点击查看免费下载本文以 DesignPatternsPHP 仓库中 Structural/Flyweight 的官方示例为主线完整剖析享元模式Flyweight Pattern在 PHP 8 中的落地实现从目的与适用场景、内部/外部状态划分到Text、Character、Word、TextFactory四个类的协作关系再到测试用例对对象共享语义的验证方式。读完本文你将掌握如何用对象池化共享减少内存占用并能直接运行仓库测试观察共享效果。一、模式目的为什么需要享元按照 Structural/Flyweight/README.rst 的定义享元模式的目的非常明确To minimise memory usage, a Flyweight shares as much as possible memory with similar objects. It is needed when a large amount of objects is used that dont differ much in state.即最小化内存占用——让享元对象尽可能多地与相似对象共享内存。它适用于系统中存在大量、彼此状态差异很小的对象的场景。例如一个文本渲染器要显示成千上万个字符如果每个字符都独立创建对象内存会迅速膨胀而字符本身的字形可以复用只有字体、字号、颜色等显示参数随上下文变化。原文还给出了该模式的一贯实践原则A common practice is to hold state in external data structures and pass them to the flyweight object when needed.即把易变的状态存放在外部数据结构中在需要时将其传给享元对象。这正是下面源码中render(string $extrinsicState)参数设计的由来。在 DesignPatternsPHP 仓库中Flyweight 属于三类模式中的 结构型模式Structural——它通过识别一种简单的方式来建立实体间的关系从而简化整体设计。二、两个核心概念内部状态与外部状态享元模式高效的前提是把对象状态一分为二状态类型含义存放位置仓库示例内部状态Intrinsic State与具体上下文无关、可被多个对象共享的部分存放在享元对象内部一经创建不再变化字符本身如a、b单词本身外部状态Extrinsic State依赖上下文、随调用场景变化的部分存放在客户端或外部数据结构中通过方法参数传入渲染时的字体font对应到本仓库的代码注释Character.phpAny state stored by the concrete flyweight must be independent of its context. For flyweights representing characters, this is usually the corresponding character code. Clients supply the context-dependent information that the flyweight needs to draw itself. For flyweights representing characters, extrinsic state usually contains e.g. the font.也就是说享元对象内部存储的必须是与上下文无关的固有数据对字符而言就是字符编码而绘制所需的环境信息如字体由客户端在调用时提供。三、源码实现四个类如何协作仓库中的享元示例共包含四个 PHP 文件全部位于 Structural/Flyweight 目录项目要求 PHP 8.0见 composer.json代码大量使用了构造函数属性提升constructor property promotion等 PHP 8 语法。3.1 享元接口Text.php所有享元对象必须实现的统一接口定义了接收外部状态并完成渲染的方法?php declare(strict_types1); namespace DesignPatterns\Structural\Flyweight; /** * This is the interface that all flyweights need to implement */ interface Text { public function render(string $extrinsicState): string; }接口只有一个方法render(string $extrinsicState): string$extrinsicState就是传入的外部状态示例中代表字体返回值是渲染结果字符串。接口的存在让Character与Word可以互换也让工厂能够以统一的Text类型管理整个对象池。3.2 具体享元一Character.phpCharacter是字符型享元内部仅持有与上下文无关的字符本身private string $name渲染时结合外部传入的字体?php declare(strict_types1); namespace DesignPatterns\Structural\Flyweight; /** * Implements the flyweight interface and adds storage for intrinsic state, if any. * Instances of concrete flyweights are shared by means of a factory. */ class Character implements Text { /** * Any state stored by the concrete flyweight must be independent of its context. * For flyweights representing characters, this is usually the corresponding character code. */ public function __construct(private string $name) { } public function render(string $extrinsicState): string { // Clients supply the context-dependent information that the flyweight needs to draw itself // For flyweights representing characters, extrinsic state usually contains e.g. the font. return sprintf(Character %s with font %s, $this-name, $extrinsicState); } }可以观察到$name字符编码是内部状态在构造函数中一次注入、终身不变$extrinsicState字体是外部状态每次调用render()时由客户端临时传入。这种内部状态固化 外部状态按需传入的结构正是享元模式的核心形态。3.3 具体享元二Word.phpWord是单词型享元结构上与Character完全对称只是内部状态从单个字符换成了整个单词渲染输出也相应变为Word xxx with font yyy?php namespace DesignPatterns\Structural\Flyweight; class Word implements Text { public function __construct(private string $name) { } public function render(string $extrinsicState): string { return sprintf(Word %s with font %s, $this-name, $extrinsicState); } }注意Word.php与Character.php都实现了同一个Text接口因此它们可以被工厂无差别地放入同一个对象池中管理。3.4 享元工厂TextFactory.php工厂是享元模式的共享调度中枢。客户端不应该直接实例化享元而应交给工厂工厂负责返回已有对象或按需创建新对象。仓库实现如下?php declare(strict_types1); namespace DesignPatterns\Structural\Flyweight; use Countable; /** * A factory manages shared flyweights. Clients should not instantiate them directly, * but let the factory take care of returning existing objects or creating new ones. */ class TextFactory implements Countable { /** * var Text[] */ private array $charPool []; public function get(string $name): Text { if (!isset($this-charPool[$name])) { $this-charPool[$name] $this-create($name); } return $this-charPool[$name]; } private function create(string $name): Text { if (strlen($name) 1) { return new Character($name); } return new Word($name); } public function count(): int { return count($this-charPool); } }该工厂有三个值得注意的设计点get()的先查后建逻辑每次请求先检查$charPool中是否已有同名字典键若已存在直接返回共享实例只有首次出现时才调用create()创建。这是享元复用而非新建的关键机制。create()的策略分派根据strlen($name)判断——长度为 1 创建Character否则创建Word。这把该用哪类享元的决策也收拢到了工厂内部。实现Countable接口通过count()暴露对象池当前大小便于外部尤其是测试验证一共只创建了多少个实例从而证明共享确实发生了。四、测试验证如何证明对象被共享仓库用 Structural/Flyweight/Tests/FlyweightTest.php 以单元测试的形式实证了享元模式的效果这是理解该模式最有说服力的部分。测试逻辑如下?php declare(strict_types1); namespace DesignPatterns\Structural\Flyweight\Tests; use DesignPatterns\Structural\Flyweight\TextFactory; use PHPUnit\Framework\TestCase; class FlyweightTest extends TestCase { private array $characters [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z]; private array $fonts [Arial, Times New Roman, Verdana, Helvetica]; public function testFlyweight() { $factory new TextFactory(); for ($i 0; $i 10; $i) { foreach ($this-characters as $char) { foreach ($this-fonts as $font) { $flyweight $factory-get($char); $rendered $flyweight-render($font); $this-assertSame(sprintf(Character %s with font %s, $char, $font), $rendered); } } } foreach ($this-fonts as $word) { $flyweight $factory-get($word); $rendered $flyweight-render(foobar); $this-assertSame(sprintf(Word %s with font foobar, $word), $rendered); } // Flyweight pattern ensures that instances are shared // instead of having hundreds of thousands of individual objects // there must be one instance for every char that has been reused for displaying in different fonts $this-assertCount(count($this-characters) count($this-fonts), $factory); } }这个测试包含三层验证渲染结果正确性外层循环将 26 个字母 × 4 种字体 × 11 轮迭代共 26 × 4 × 11 1144 次反复调用$factory-get($char)并渲染每次都断言输出严格等于Character {字符} with font {字体}保证共享实例的渲染行为完全正确。单词同样被复用4 个字体名Arial、Times New Roman、Verdana、Helvetica因为长度大于 1 会走Word分支同样从池中获取并断言输出。实例数量验证核心断言assertCount(count($this-characters) count($this-fonts), $factory)断言对象池最终只包含26字母 4字体单词 30个实例。测试注释点明了意图如果没有享元模式上千次调用会产生成千上万个独立对象而有了共享机制无论调用多少次每个字符/单词都只有一个实例被反复用于不同字体的渲染。五、在本地运行测试该仓库已通过 composer.json 声明php 8.0依赖并把Behavioral、Creational、Structural、More四个目录以 classmap 方式注册到自动加载中。运行 Flyweight 示例测试的完整步骤如下# 1. 安装依赖首次运行必须 composer install # 2. 运行全部模式测试phpunit 配置见 phpunit.xml.dist ./vendor/bin/phpunit其中 phpunit.xml.dist 已把Structural/*/Tests等目录纳入测试套件directory suffixTest.phpStructural/*/Tests/directory因此上述命令会直接执行到FlyweightTest。如果想只运行本模式测试可加--filter过滤./vendor/bin/phpunit --filter FlyweightTest测试全部通过后你就从实证层面确认了享元工厂在整个调用周期内只创建了 30 个对象却支撑了超过千次的渲染请求。六、适用场景与使用注意事项基于 Structural/Flyweight/README.rst 的目的阐述与仓库实现可以总结享元模式的适用条件与边界适用场景同时满足时收益最大程序需要创建海量对象例如文本编辑器中的字符、地图上的 POI 点、游戏中的子弹/粒子等这些对象状态差异极小大部分数据可以归纳为少量可共享的内部状态对象的外部状态可以被提取出来由客户端持有并在使用时传入。注意事项外部状态必须可分离从源码结构看享元对象只能安全保存与上下文无关的数据如Character::$name一旦把可变上下文塞进对象内部共享就会互相污染对象池查询有成本TextFactory::get()每次调用都要做一次isset检查属于典型的以查找开销换取内存收益的权衡不适合对象数量本身就很少的场景内部状态设计要克制享元的内部字段这里是$name应尽量精简只保留真正跨上下文通用的部分否则共享空间会被无谓放大。七、小结通过 DesignPatternsPHP 的 Flyweight 示例可以看到享元模式在 PHP 8 中的落地非常简洁Text接口统一了享元行为Character与Word分别承载字符与单词两类可共享的内部状态TextFactory以数组字典实现先查后建的对象池而FlyweightTest用 30 个共享实例扛住 1144 次渲染调用直观证明了内存节约的效果。核心要领即原文那句话——让相似对象尽可能共享内存把易变状态放到外部用时再传入。赞分享示例工程教程【免费下载链接】DesignPatternsPHPSample code for several design patterns in PHP 8.x项目地址https://gitcode.com/gh_mirrors/de/DesignPatternsPHP点击查看免费下载相关推荐PHP 8 对象池模式Object Pool Pattern实战指南DesignPatternsPHP 的 WorkerPool 源码与测试深度解析PHP 8 对象池模式Object Pool Pattern实战指南DesignPatternsPHP 的 WorkerPool 源码与测试深度解析 对象示例工程教程ASP.NET Core Blazor JavaScript互操作完全教程.NET与JS双向调用技巧ASP.NET Core Blazor JavaScript互操作完全教程.NET与JS双向调用技巧 在 ASP.NET Core Blazor 应用中Ja示例工程教程Unity3DTraining 设计模式实战享元模式Flyweight源码级解析——用共享技术支持大量细粒度对象Unity3DTraining 设计模式实战享元模式Flyweight源码级解析——用共享技术支持大量细粒度对象 在游戏与图形密集型应用中对象太多导致示例工程上一篇【亲测免费】 Unity MCP 服务器项目教程下一篇如何用Markmap将Markdown文档转化为交互式思维导图创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网