2.10 设计模式:调度室的标准作业手册 本节摘要:设计模式是前人反复验证过的结构方案合集,价值一半在方案本身,另一半在"行话"——说"这里用策略模式",同行立刻懂结构意图。本节挑服务端日常出场率最高的四件:单例管全局唯一、工厂管对象 births、策略管同族算法切换、观察者管事件广播,每件给出月台场景的实现与适用边界,再专门讲"什么时候不该用模式"。本节是 2.1 面向对象编制思想的收官。 模式是行话不是咒语 先立心态。模式不是"用了就高级"的装饰,每件都对应一个具体的结构难题:某处必须全局只有一份(数据库配置),某处对象创建过程太啰嗦(多种支付通道),某处一组行为要能热插拔(多种排序规则),某处一件事发生要通知多方(下单成功要发短信、记积分、推报表)。先有难题,再对号入座;
本节摘要:设计模式是前人反复验证过的结构方案合集,价值一半在方案本身,另一半在"行话"——说"这里用策略模式",同行立刻懂结构意图。本节挑服务端日常出场率最高的四件:单例管全局唯一、工厂管对象 births、策略管同族算法切换、观察者管事件广播,每件给出月台场景的实现与适用边界,再专门讲"什么时候不该用模式"。本节是 2.1 面向对象编制思想的收官。
先立心态。模式不是"用了就高级"的装饰,每件都对应一个具体的结构难题:某处必须全局只有一份(数据库配置),某处对象创建过程太啰嗦(多种支付通道),某处一组行为要能热插拔(多种排序规则),某处一件事发生要通知多方(下单成功要发短信、记积分、推报表)。先有难题,再对号入座;反过来拿着模式找地方套,代码只会多绕一层。带着这个前提,逐件看。
单例保证一个类全请求只有一个实例。月台场景是"站点配置":处处要用、初始化有成本、改两份就出事。PHP 的经典写法:
<?php class Config { private static ?Config $instance = null; private array $items; private function __construct() { // 构造私有:外部 new 不出来 $this->items = json_decode( file_get_contents(__DIR__ . '/station.json'), true ) ?? []; } public static function get(): static { return static::$instance ??= new static(); // 首次取时才创建 } public function item(string $key, mixed $default = null): mixed { return $this->items[$key] ?? $default; } } echo Config::get()->item('open_time', '05:30'), "\n";
三处关节:构造私有堵死外部创建、静态属性持有唯一实例、首次访问时才初始化(顺带是惰性加载)。要泼的冷水也得泼:单例本质是"包装过的全局变量",状态难隔离、测试难替换(2.8 节的替身插不进去),现代框架的依赖注入容器在多数场景下是更好的"唯一性管理方"。我的分寸是:无状态配置类可以单例;带业务状态的类谨慎,能用注入就不单例。
工厂把"创建哪一种、怎么创建"收敛到一个出口。月台场景是支付通道:微信、支付宝、余额三类通道创建过程不同,调用方不该关心细节:
<?php interface PayChannel { public function pay(float $amount): string; } class WxPay implements PayChannel { public function pay(float $amount): string { return "微信支付 {$amount} 元"; } } class AliPay implements PayChannel { public function pay(float $amount): string { return "支付宝支付 {$amount} 元"; } } class PayFactory { public static function make(string $type): PayChannel { return match ($type) { 'wx' => new WxPay(), 'ali' => new AliPay(), default => throw new InvalidArgumentException("未知通道 {$type}"), }; } } echo PayFactory::make('wx')->pay(240.0), "\n"; // 微信支付 240 元
新增"余额支付"只需加一个类、工厂添一行分支,调用方代码不动。这就是 2.1 节"对扩展开放、对修改关闭"的具体落地。

策略模式解决"同族算法可切换"。月台场景是票价折扣:会员折、新客折、深夜班次折,规则只增不减。按策略组织,主干代码稳定:
<?php interface DiscountStrategy { public function apply(float $fare): float; } class MemberDiscount implements DiscountStrategy { public function apply(float $fare): float { return $fare * 0.8; } } class NewbieDiscount implements DiscountStrategy { public function apply(float $fare): float { return $fare - 10; } } class FareCalculator { public function __construct(private DiscountStrategy $strategy) {} // 策略注入 public function final(float $fare): float { return max(0, $this->strategy->apply($fare)); // 兜底不为负 } } echo (new FareCalculator(new MemberDiscount))->final(240.0), "\n"; // 192
对照反例感受差异:主干里写 if 会员 ... elseif 新客 ... elseif 深夜 ...,每加一种折扣就得重开主干——策略模式把"变化"关进了各自的类。与 2.1 的校验岗对照着看,结构是同一套思想的两次应用。
观察者模式解决"一件事、多方跟进"。下单成功后要发短信、加积分、推报表,把这三件事直接写进下单函数,每加一件事就改一次核心逻辑——危险信号。改成事件广播:
<?php class EventHub { private array $listeners = []; public function on(string $event, callable $fn): void { $this->listeners[$event][] = $fn; } public function fire(string $event, mixed $payload): void { foreach ($this->listeners[$event] ?? [] as $fn) { $fn($payload); } } } $hub = new EventHub; $hub->on('order.paid', fn($o) => echoLine("短信台:通知 {$o['user']} 支付成功")); $hub->on('order.paid', fn($o) => echoLine("积分台:为 {$o['user']} 记积分")); function echoLine(string $s): void { echo $s, "\n"; } $hub->fire('order.paid', ['user' => '张三', 'amount' => 240]); // 短信台与积分台各自接单,下单函数本身一行不改
下单函数只负责"广播一件事",后续动作各自登记、各自演化、各自测试。框架章会看到 Laravel 的事件系统就是这套结构的工程化成品。
反模式清单与正例同样重要。其一,只有一种实现就别抽接口建工厂:为想象中的扩展性先付复杂度,多数时候那份扩展永远不来。其二,三层调用只为了转发:模式包装层层加码后,读代码的人要在五六层之间跳转才能找到真逻辑——判断标准是"新模式层是否带来了新信息",没有就是纯噪声。其三,用模式掩盖设计混乱:类职责都没分清就上模式,等于给危房刷漆。每个模式的引入都该能写出一句话理由:"它解决了这里的什么难题"——写不出,就先不做。
⚠️ 模式名词当评审标准:评审该问"这个结构是否清楚、好测、好改",而不是"为什么不用某某模式"。
⚠️ 单例里藏业务状态:并发请求共享状态制造时有时无的怪病,状态进数据库或缓存,不进单例。
本章十节到此收拢。下一章把这套内功交给框架——看看"大站调度中心"如何把这些手工活自动化。