PHP8.5怎么实现策略模式切换不同算法

发布时间:2026/10/3 3:15:04

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就是最优解。
延伸阅读

更多相关文章

2026/10/3 3:15:04

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

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

2026/10/3 3:15:04

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

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

2026/10/3 3:10:04

集群负载均衡实战:从算法选型、健康检查到故障转移全链路解析

我们在生产环境里跑过几十台节点的集群,也折腾过从几百 QPS 到几万 QPS 的流量变化,负载均衡这块我踩过的坑比看过的文档多得多。很多人一开始以为负载均衡就是把请求轮询发到几台机器上,等真正上了集群才发现,连接不均衡、数据倾…

2026/10/3 4:25:08

深圳2020 POI数据清洗与坐标统一实战指南

简介:本资源为深圳市2020年高实用性GIS地理信息数据集,面向城市规划、交通管理、商业选址及地理信息科研领域的初/中级用户,解决多源POI与地形基础数据缺失、格式不统一、空间分析门槛高等实际问题。压缩包共137个文件,54.04MB&am…

2026/10/3 4:25:07

3DGS异常显示祛除实战:Mask2Former与CUDA环境下的浮尘幽灵高斯清理

1. 3DGS异常显示问题的背景与祛除思路1.1 为什么3DGS场景里会冒出“幽灵”和“浮尘”做3D Gaussian Splatting重建的朋友,大概率都遇到过这样的画面:明明只拍了一栋建筑或者一个物体,训练完之后场景里却飘着一团团半透明的“雾”,…

2026/10/3 4:25:07

Air780EP模组AT+MQTT接入OneNET平台实战指南

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

2026/10/3 4:20:07

基于PubMed与智能体的综述生成:从100篇文献到万字初稿

1. 先说痛点:100篇文献到底有多难"啃"完1.1 写综述最耗时的不是写作,是文献处理做科研的人应该都有这种体会:真正动手写综述之前的文献筛选阶段,才是最折磨人的。我见过太多人刚下载完100篇PDF,兴冲冲打开En…

2026/10/2 8:16:46

东莞市品牌网站建设报价常见报错与解决

东莞品牌网站建设报价单背后:一份保姆级建站教程避坑实录 网站做好了没人访问,这大概是很多老板最头疼的事。花了大几万做的品牌站,上线后流量惨淡,比路边摊还冷清。别急着骂外包公司,很多“东莞品牌网站建设报价”里藏着不少猫腻,比如用模板站冒充定制…

2026/10/2 18:20:53

如何划分训练/验证集: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 10:48:55

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

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

2026/10/3 0:04:31

国内大学生必备的AI写作辅助软件是哪款?

国内高校学生在论文写作过程中,越来越依赖AI辅助工具提升效率,主流方案以本土化全流程工具为核心,结合通用大模型与专业插件,覆盖选题构思、框架搭建、初稿撰写、查重降重、格式调整等关键环节,本文将深入解析当前主流…

2026/10/3 0:04:31

Codex接入Jev模型完整指南:配置方法、本地部署与踩坑排查

最近不少人在讨论 Codex 搭配 Jev 这套玩法,我一开始没太当回事,直到自己把 Jev 接进 Codex跑了几轮编码任务之后,才明白那些说“直接起飞”的人是怎么想的。Codex 作为工具本身已经够能打了,但模型固定、上下文策略固定&#xff…

2026/10/3 0:04:31

GitHub 热门: NVIDIA/Model-Optimizer

👋 Hi,我擅长 AI 大模型应用落地、意识解码与 AI 开发工具链 。 💡 创业路上,用技术换时间,一起把 AI 变成生产力 🚀 >GitHub 热门: NVIDIA/Model-Optimizer 凌晨两点,你刚把跑通了的 Qwen3.…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑