PHP8.5怎么实现策略模式切换不同算法
前言需要先纠正一个概念策略模式不是 PHP 8.5 的特性。它是《设计模式》里提出的对象行为型模式与语言版本无关用 PHP 5 也能写用 PHP 8.5 也能写。之所以标题里带着PHP8.5更合理的理解是在 PHP 8.5 环境下怎么写策略模式——本文就按这个角度来写并且在实现里用上 PHP 8.0 到 8.5 这一路的语言特性构造器属性提升、枚举、readonly、#[\Override]、clone with、管道操作符让代码比十年前那版教科书实现短得多、也安全得多。策略模式要解决的问题非常具体同一件事有多种算法且需要在运行时决定用哪一种。运费计算、支付渠道、加密方式、图片压缩、折扣规则都属于这一类。常见的坏味道是把它们写成一长串if / else if / else或switch函数随业务增长越来越长每加一种算法都要改动它、重测它。本文给出一个能直接落地的实现策略接口 枚举标识 工厂分派 上下文持有并配一份可运行的完整示例最后说明什么情况下不该用它。一、先看反面教材switch是怎么烂掉的?php // ❌ 反面教材新增一种渠道就要动这个函数分支只会越来越长 function calcShipping(string $channel, float $weightKg, float $amount): float { switch ($channel) { case standard: return 8.0 $weightKg * 2.0; case express: return 20.0 $weightKg * 6.0; case free: return 0.0; default: throw new InvalidArgumentException(未知渠道: {$channel}); } }它能跑问题在于三点对扩展不封闭每加一种渠道都要改这个函数改 A 可能碰坏 B、测试无法隔离想单独测顺丰特快得跑完整个函数路径、依赖无法注入要调运价接口、读配置时只能靠全局变量没法替换成测试替身。策略模式的做法是把选择算法和实现算法分开算法各自成为一个实现同一接口的类选择交给工厂使用方只持有接口。二、策略模式的三块积木角色职责本文中的实现Strategy策略接口定义所有算法共同的方法签名interface ShippingStrategyConcreteStrategy具体策略各自实现一种算法StandardShipping、ExpressShipping、FreeShippingContext上下文持有策略对调用方暴露统一入口final class ShippingCalculator额外的一块选择器把用户选择的渠道翻译成策略对象enum ShippingChannelShippingStrategyFactory教科书版本只有前三块把选择逻辑留给调用方去new。实践里真正容易出问题的是第四块——选择器。写成new $class或者字符串拼接类名是最常见的事故来源。用枚举 match表达式match是 PHP 8.0 引入的就稳得多新增渠道时match的分支写不全运行时会抛UnhandledMatchError而不是静默地走到某个default里算错钱。三、完整可运行示例下面这份代码需要PHP 8.3 及以上用到了#[\Override]属性它是 PHP 8.3 引入的。把两处#[\Override]注释掉就可以在 PHP 8.1 上运行。?php // shipping-strategy.php —— 需要 PHP 8.3去掉 #[\Override] 后 PHP 8.1 亦可 // 用法php shipping-strategy.php declare(strict_types1); /** 订单策略所需的输入数据由调用方传入策略本身不读全局状态 */ final readonly class Order // readonly 类是 PHP 8.2 引入的 { public function __construct( public string $id, public float $weightKg, public float $amount, ) {} } /** 策略接口所有算法必须提供名称与运费计算两个能力 */ interface ShippingStrategy { public function name(): string; /** 返回运费元 */ public function cost(Order $order): float; } final class StandardShipping implements ShippingStrategy { private const BASE 8.0; private const PER_KG 2.0; #[\Override] // PHP 8.3编译期校验确实实现了接口方法 public function name(): string { return 标准快递; } #[\Override] public function cost(Order $order): float { return self::BASE $order-weightKg * self::PER_KG; } } final class ExpressShipping implements ShippingStrategy { private const BASE 20.0; private const PER_KG 6.0; public function name(): string { return 顺丰特快; } public function cost(Order $order): float { return self::BASE $order-weightKg * self::PER_KG; } } final class FreeShipping implements ShippingStrategy { public function name(): string { return 满额包邮; } public function cost(Order $order): float { return 0.0; } } /** 渠道枚举对外的选项也是策略的唯一入口枚举是 PHP 8.1 引入的 */ enum ShippingChannel: string { case Standard standard; case Express express; case Free free; public function label(): string { return match ($this) { self::Standard 标准快递3-5 天, self::Express 顺丰特快次日达, self::Free 包邮, }; } /** 把用户输入的字符串转成渠道非法值返回 null 由调用方处理 */ public static function parse(?string $raw): ?self { return self::tryFrom((string) $raw); } } /** 工厂负责渠道 - 策略的映射并缓存无状态策略实例 */ final class ShippingStrategyFactory { /** var arraystring, ShippingStrategy */ private static array $cache []; public static function fromChannel(ShippingChannel $channel): ShippingStrategy { // 无状态策略可以安全复用避免每个请求都 new 一遍 return self::$cache[$channel-value] ?? match ($channel) { ShippingChannel::Standard new StandardShipping(), ShippingChannel::Express new ExpressShipping(), ShippingChannel::Free new FreeShipping(), }; } } /** 上下文调用方只跟它打交道不关心用的是哪种算法 */ final class ShippingCalculator { public function __construct( private ShippingStrategy $strategy new StandardShipping(), ) {} /** 运行时切换算法 */ public function useChannel(ShippingChannel $channel): void { $this-strategy ShippingStrategyFactory::fromChannel($channel); } /** 统一出口报价 */ public function quote(Order $order): array { $cost $this-strategy-cost($order); return [ order $order-id, strategy $this-strategy-name(), cost round($cost, 2), ]; } } // —————————— 使用 —————————— $order new Order(SO20260929001, weightKg: 3.5, amount: 299.0); // 命名参数是 PHP 8.0 的 $calculator new ShippingCalculator(); // 默认标准快递 foreach (ShippingChannel::cases() as $channel) { $calculator-useChannel($channel); $result $calculator-quote($order); printf(%-12s %-24s 运费 %.2f 元\n, $result[order], $result[strategy], $result[cost]); } // 用户提交的渠道非法值必须显式处理不能默认成某个算法 $input $_GET[channel] ?? express; $channel ShippingChannel::parse($input); if ($channel null) { fwrite(STDERR, 非法渠道: {$input}\n); exit(1); } $calculator-useChannel($channel); echo json_encode($calculator-quote($order), JSON_UNESCAPED_UNICODE), PHP_EOL;输出SO20260929001 标准快递 运费 15.00 元 SO20260929001 顺丰特快 运费 41.00 元 SO20260929001 满额包邮 运费 0.00 元 {order:SO20260929001,strategy:顺丰特快,cost:41}这段代码有三个值得说的设计决定1. 策略只接收数据参数不读全局状态。cost(Order $order)的入参就是全部输入测试一个策略只需要构造一个Order不需要起 HTTP 环境。2. 有状态的依赖走构造函数注入。若某个策略要调运价接口就写成new ExpressShipping($rateClient)工厂里从容器取依赖策略内部绝不new否则无法替换成测试替身。3. 无状态策略缓存复用枚举是唯一的渠道来源。工厂里的??缓存让同一渠道始终拿到同一实例前端下拉框用ShippingChannel::cases()生成后端解析用tryFrom()两边不可能对不上。想让上下文不可变用 PHP 8.5 的clone with上面的useChannel()是可变写法。若希望上下文不可变如在常驻内存的框架里避免状态串味PHP 8.5 提供了clone with语法可在复制对象的同时替换属性?php // 需要 PHP 8.5 // 概念示例不改动原对象返回一个换了策略的新上下文 final class ShippingCalculator { public function __construct( public readonly ShippingStrategy $strategy new StandardShipping(), ) {} public function withChannel(ShippingChannel $channel): self { return clone $this with { strategy: ShippingStrategyFactory::fromChannel($channel) }; } } $base new ShippingCalculator(); $express $base-withChannel(ShippingChannel::Express); // 原对象不受影响clone with省掉了为每个属性手写withXxx()的样板代码。在 PHP 8.5 之前的版本里等价写法是自己写构造函数复制return new self(ShippingStrategyFactory::fromChannel($channel));。策略也可以就是一个可调用对象如果算法短到不值得为它建一个类可以用 PHP 8.1 引入的一等可调用语法first-class callable syntax形如strtolower(...)把函数包成策略写一个构造函数接收Closure $costFn的包装类cost()里调用($this-costFn)($order)即可。它是策略模式的退化形态只在算法只有一两行时才划算。顺带一提 PHP 8.5 的管道操作符|它能让数据依次经过多个函数读起来更顺$slug $s | strtolower(...) | trim(...);但它不替代策略模式——管道解决的是同一份数据的处理流水线策略解决的是同一件事的不同实现运行时二选一。四、什么时候不该用策略模式策略模式是有成本的多一个接口、多 N 个类、多一层间接。满足下面任意一条时直接用match就够了别过度设计算法只有一种或者未来可预见的只有两种而且都不会有独立依赖。算法短到一行就能写完且不需要单独测试比如max(0, $x)这种。分支之间共享大量状态拆开之后反而要在多个类之间来回传参。下表是三种写法的选择依据写法适用场景扩展方式可测试性if / else链2~3 个简单分支无依赖改原函数差分支耦合在一起match直接分派分支是纯计算、无外部依赖、不需要单独替换加一个分支中可整函数测策略模式算法有独立依赖、需运行时切换、需独立测试与单独发布加一个类 一个枚举 case好每个策略单独测常见坑点1. 上下文里写if判断具体类型等于没解耦// ❌ 上下文知道了所有具体策略新增一种就要改这里 public function quote(Order $o): float { if ($this-strategy instanceof ExpressShipping) { return $this-strategy-cost($o) * 1.1; } return $this-strategy-cost($o); }// ✅ 把差异放进策略内部上下文只调接口方法 public function quote(Order $o): float { return $this-strategy-cost($o); }2. 用字符串拼类名来创建策略// ❌ 拼错了要到运行时才炸而且允许外部输入构造任意类 $class App\\Shipping\\ . ucfirst($_GET[channel]) . Shipping; $strategy new $class();// ✅ 用枚举白名单 match非法输入在解析阶段就被拦掉 $channel ShippingChannel::tryFrom((string) $_GET[channel]) ?? throw new InvalidArgumentException(非法渠道); $strategy ShippingStrategyFactory::fromChannel($channel);3. 策略方法签名不统一导致无法互换// ❌ 一个返回 float一个返回 [cost ...]换进去就报错 class A implements ShippingStrategy { public function cost(Order $o): float {} } class B implements ShippingStrategy { public function cost(Order $o): array {} }// ✅ 接口是唯一契约签名不一致在编译期就会被拒绝返回类型不兼容是致命错误 interface ShippingStrategy { public function cost(Order $o): float; }4. 策略内部直接读超全局变量// ❌ 策略不可测、不可复用换一个入口CLI/队列就废 public function cost(Order $order): float { return $order-weightKg * (float) ($_POST[rate] ?? 1); }// ✅ 依赖通过构造函数注入数据通过方法参数传入 public function __construct(private float $ratePerKg 2.0) {} public function cost(Order $order): float { return $order-weightKg * $this-ratePerKg; }5. 把数据塞进上下文导致策略无法独立复用// ❌ 策略必须依赖上下文才能工作等于把两者焊死了 final class ShippingCalculator { public array $orderData []; public function quote(): float { return $this-strategy-cost($this); } }// ✅ 数据用参数传递策略只依赖自己声明的输入类型 public function quote(Order $order): float { return $this-strategy-cost($order); }6. 上下文暴露了 setter/getter外部随意改动算法// ❌ 任何拿到上下文的地方都能换掉算法出问题极难定位 $calc-setStrategy(new WhateverStrategy());// ✅ 切换只能通过受控入口枚举完成或干脆用不可变的 withChannel() $calc $calc-withChannel(ShippingChannel::Express);7. 枚举渠道新增了 casematch忘了补分支// ❌ 新增 case 后运行时抛 UnhandledMatchError报错点离真正的改动很远 $strategy match ($channel) { ShippingChannel::Standard new StandardShipping(), ShippingChannel::Express new ExpressShipping(), }; // ✅ 要么穷举所有 case要么在 default 里显式抛出带上下文的异常 default throw new LogicException(未实现的渠道: {$channel-value}),总结关注点结论策略模式是否 8.5 新特性不是它是与语言版本无关的设计模式核心结构策略接口 具体策略 上下文 选择器枚举/工厂选择器怎么写枚举做白名单match做分派工厂做缓存依赖与数据怎么进来依赖走构造函数注入数据走方法参数都不读全局状态8.x 语言特性收益枚举保证选项合法、match保证穷举、readonly保证不可变8.5 相关语法clone with做不可变切换\何时不用分支少、纯计算、无独立依赖直接match即可结论策略模式的本质是把选择和实现分开让新增算法变成加一个类而不是改一个越来越长的函数。在 PHP 8.5 上写它代码量比教科书版本小得多——枚举管住合法选项match保证穷举readonly与clone with守住不可变。但它是有成本的只有当算法真的会独立演进、独立测试、独立注入依赖时这层间接才有价值否则一个match就是最优解。

相关新闻

fcitx5皮肤定制全攻略:主题配置、Rime联动与避坑指南

fcitx5皮肤定制全攻略:主题配置、Rime联动与避坑指南

如果你和我一样,日常在Linux桌面上靠fcitx5输入法过日子,那大概率早晚会走到“看默认皮肤不顺眼”这一步。默认的白底黑字候选框说不上哪里不好,但就是和精心调过的桌面壁纸、GTK主题放在一起时,怎么看怎么扎眼。今天这篇就专门聊…

2026/10/3 3:15:11 阅读更多 →
fcitx5皮肤定制全攻略:从原理到Rime搭配实战

fcitx5皮肤定制全攻略:从原理到Rime搭配实战

折腾Linux桌面这些年,输入法换来换去,最后在fcitx5上彻底安家。倒不是因为它打字多快,而是这套框架的皮肤系统实在太好用了:候选框、状态条、托盘图标全都能按自己喜好重画,装完一套顺手主题,整个桌面的完成…

2026/10/3 3:15:11 阅读更多 →
SpringBoot+Vue+MyBatis+MySQL车辆管理系统全栈开发实战

SpringBoot+Vue+MyBatis+MySQL车辆管理系统全栈开发实战

做过车辆管理的朋友应该深有体会:车辆信息散落在Excel表里、驾驶员和车辆绑定关系模糊、用车记录全凭纸质台账、月底统计里程油耗翻到头晕。这种状态下别说做管理决策,连“现在到底有几辆车在外头跑”这种基础问题都答不上来。所以我用SpringBootVueMyBa…

2026/10/3 3:15:10 阅读更多 →

最新新闻

OpenRig:用铝型材和3D打印搭出万能模块化设备框架

OpenRig:用铝型材和3D打印搭出万能模块化设备框架

1. OpenRig是什么:一套能搭万物的开源框架先说结论:OpenRig是我自己折腾出来的一套开源模块化设备框架,核心材料是铝合金型材加3D打印连接件,再辅以T型螺母和标准螺栓。你可以把它理解成“机械设备界的乐高”:同一批型…

2026/10/3 4:45:35 阅读更多 →
Tessent ICL语法详解:SIB、TDR与TAP定义和连接实战

Tessent ICL语法详解:SIB、TDR与TAP定义和连接实战

做DFT的人看到ICL这几个字母,心情通常比较复杂。我在项目里第一次接触Tessent ICL(Instrument Connectivity Language)时,以为这只是工具自动生成的文件,直到要自己在RTL里插SIB、自定义TDR,还得把不同IP的…

2026/10/3 4:45:34 阅读更多 →
基于WebRTC与Canvas的SaaS客服远程演示与实时协作方案

基于WebRTC与Canvas的SaaS客服远程演示与实时协作方案

1. 项目背景:SaaS客服的"看不见摸不着"之痛SaaS产品的客服,大概是整个公司里最难做的岗位之一。产品看不见摸不着,客户遇到问题的时候,你没法像卖实体商品一样,把东西递到他手里让他自己摆弄。于是大量的时间…

2026/10/3 4:45:33 阅读更多 →
Open Shell:在Windows 11上找回高效经典开始菜单的完整指南

Open Shell:在Windows 11上找回高效经典开始菜单的完整指南

如果你用过Windows 7,一定记得那个干净利落的开始菜单:左边是常用程序列表,右边是控制面板、文档、关机按钮,不需要任何学习成本就能快速定位。后来我升级到Windows 11,面对那个居中排列、塞满推荐项目和固定应用的新开…

2026/10/3 4:45:33 阅读更多 →
OpenRig模拟赛车DIY座舱搭建指南:铝型材模块化从选料到调校

OpenRig模拟赛车DIY座舱搭建指南:铝型材模块化从选料到调校

和我这一代玩模拟赛车的人相处久了,都会经历一个绕不开的阶段:从“桌子支架凑合”切到“独立座舱”。OpenRig这个名称,最早是在模拟赛车的开源社区里见到的,它指的并不是某一家厂商的整机商品,而是一整套模块化驾驶舱的…

2026/10/3 4:45:24 阅读更多 →
SABRE 3D/3DxT安全配置必备前置清单:从环境评估到备份与回滚

SABRE 3D/3DxT安全配置必备前置清单:从环境评估到备份与回滚

每次接到SABRE 3D或者SABRE 3DxT的安全配置任务,我都习惯先沉住气,别急着打开组策略编辑器就开干。半导体设备不像普通办公电脑,你随手改一条安全策略,轻则报警满天飞,重则直接影响电镀腔体的工艺联锁,一批…

2026/10/3 4:44:19 阅读更多 →

日新闻

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南

把回忆蒸馏成 AI 的浪漫实验:为什么你需要前任.skill 完整指南 【免费下载链接】ex-skill 前任 skill 项目地址: https://gitcode.com/gh_mirrors/exsk/ex-skill 前任.skill 是一个运行在 Claude Code 上的开源 Skill:导入微信、iMessage、短信、…

2026/10/3 0:00:27 阅读更多 →
45个经典Linux面试题:从命令到网络排障的完整考点解析

45个经典Linux面试题:从命令到网络排障的完整考点解析

刚开始带应届生的时候,我最头疼的就是他们拿着一摞Linux面试题背得滚瓜烂熟,一上机全露馅。后来自己从被面的人变成面别人的人,才慢慢摸清楚:Linux面试题考的根本不是答案本身,而是你面对一个不确定的系统问题时&#…

2026/10/3 0:01:28 阅读更多 →
SAP生产预留实战指南:MB21/MB23/MB25协同与MRP集成

SAP生产预留实战指南:MB21/MB23/MB25协同与MRP集成

简介:本资源是一份面向SAP ABAP开发人员、生产计划专员及ERP实施顾问的实操型操作指南,聚焦SAP生产预留核心业务场景,系统解决物料预留创建、查询、校验与批量处理等高频问题。文档以结构化方式覆盖预留背景原理、OMC2编码规则、工厂级参数配…

2026/10/3 0:01:28 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/10/1 19:40:48 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/10/1 19:41:40 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/10/1 20:05:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 10:36:31 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 5:26:06 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 6:09:11 阅读更多 →