设计模式这玩意在PHP圈子里一直是个两极分化的话题。一边是“面试造火箭工作拧螺丝”的调侃觉得学了用不上另一边是面试必问、源码里遍地都是看不懂就只能干瞪眼。我搞了十来年PHP从最早写原生SQL拼HTML到后来用ThinkPHP、Laravel再到现在自己维护一套支撑十几个业务的底层框架说实话真正让我把代码写出质感的转折点就是耐下性子把23种GoF设计模式逐一啃透。这篇东西不是教科书式地列定义也不是从网上抄来的UML图堆砌。我会直接站在PHP开发者的角度讲清楚每种模式解决什么痛点、在PHP语法特性下怎么写最顺手、实际项目中到底在哪用以及哪些模式在Web开发里其实很少碰。内容会比较长但我尽量做到每一段都有能直接拿回去用的价值。适合刚学完PHP基础、想进阶的初中级开发者也适合被框架源码绕晕、想系统梳理一遍的在职程序员。1. 设计模式到底在解决什么问题先聊点实在的。很多初学者学设计模式最大的障碍是想搞清楚“它到底有什么用”。我的理解很朴素设计模式是前人总结出来的、应对特定业务场景的类与对象组织方案。它不是凭空造出来的而是从大量优秀代码里提炼出的共性套路。1.1 模式映射到PHP的真实痛点PHP这门语言的特性决定了它在使用设计模式时和Java、C有不少差异。比如PHP是弱类型语言这让很多类型相关的模式实现起来更灵活但也更容易被滥用PHP有闭包和魔术方法这让某些模式有了非常轻量级的替代方案PHP的进程模型是“请求结束即销毁”这让一些长生命周期的模式比如单例在常驻内存场景有了不一样的味道。我见过太多人纠结“单例到底该不该用”或者“工厂模式和简单工厂到底啥区别”本质上是因为没有站在具体场景里去理解。设计模式是解药但不是万能的。它解决的是“变化”和“复用”之间的矛盾。你的代码里哪些地方会变哪些地方要稳定把这些想清楚了模式自然就跳出来了。1.2 模式的分类逻辑从目的和范围切入GoF把23种模式分成三大类按目的划分创建型处理对象的创建过程结构型处理类与对象的组合行为型处理对象间的职责分配与通信。另外还有一个维度是按范围分有些模式作用于类通过继承实现有些作用于对象通过组合实现。理解这个分类逻辑比死记23个名词重要得多。创建型5种单例、工厂方法、抽象工厂、建造者、原型。结构型7种适配器、桥接、组合、装饰器、外观、享元、代理。行为型11种职责链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者。在Web开发中使用频率完全不一样。以我的经验模板方法、策略、观察者、工厂方法、单例、适配器是绝对的“高频模式”几乎天天碰。而解释器、访问者、备忘录、享元这几类在常规业务系统里极少主动使用但理解它们的思路可以帮助你阅读某些底层库的源码。2. 创建型模式对象怎么“生”出来创建型模式的核心就是把new这个动作封装起来让客户端代码不直接依赖具体类。这个道理在PHP里特别实用因为PHP的代码热加载模式每个请求都重新执行一遍意味着new是很廉价的操作所以很多在Java里需要容器管理的复杂度在PHP里反而可以简化。2.1 单例模式一个类只能有一个实例单例大概是争议最大的一个模式。在PHP传统Web开发里单例主要用于数据库连接、配置对象这类资源型组件。PHP每次请求结束进程就销毁所以单例的作用不是跨请求共享而是在单次请求内避免重复实例化。final class Database { private static ?self $instance null; private PDO $pdo; private function __construct(array $config) { // 私有构造函数防止外部new $this-pdo new PDO(...); } private function __clone() {} public function __wakeup() { throw new RuntimeException(Cannot unserialize singleton); } public static function getInstance(array $config []): self { if (self::$instance null) { self::$instance new self($config); } return self::$instance; } }注意几个要点。构造函数私有还不够__clone和__wakeup也要私有或抛异常否则通过clone或反序列化可以绕过单例限制。static属性存储实例类型用?self是PHP 7.4的写法。我在老代码里见过很多人写不规范的版本比如构造函数不私有、没有禁用clone这些在严格意义上都不算合格的单例。但要泼一盆冷水现代PHP开发中单例的使用场景正在大幅收缩。原因在于依赖注入容器普及后容器本身负责管理对象生命周期你不需要自己写单例。Laravel的服务容器绑定一个singleton框架层面帮你管好了。所以我的建议是单体模式下资源类可以用单例但业务类尽量别用否则测试和替换实现都会很痛苦。2.2 简单工厂与工厂方法把new搬个家很多教材把简单工厂算作23种之外但在PHP里我必须先说它因为这是理解工厂方法的基础。简单工厂就是写一个静态方法根据参数返回不同类的实例。class LoggerFactory { public static function create(string $type): LoggerInterface { return match ($type) { file new FileLogger(), database new DatabaseLogger(), default throw new InvalidArgumentException(Unsupported logger type: $type), }; } }上面这种写法简单直接坏处是每次新增Logger类型都要改Factory类的代码违背了开闭原则。于是工厂方法模式出来了把创建动作延迟到子类让每个子类工厂负责创建一种具体产品。interface LoggerFactoryInterface { public function create(): LoggerInterface; } class FileLoggerFactory implements LoggerFactoryInterface { public function create(): LoggerInterface { return new FileLogger(...); } }看明白了吗简单工厂是一个类里switch工厂方法是把switch拆散用继承去扩展。哪种好如果类型变化频繁工厂方法更优雅如果只是两三个类型长期稳定简单工厂完全够用。这就是我常说的模式不能脱离场景谈优劣。2.3 抽象工厂生产一族相关的产品抽象工厂是工厂方法的升级版解决的是“创建一系列相关或相互依赖的对象”的问题。在PHP里最典型的场景比如跨数据库驱动支持一套代码要同时支持MySQL和PostgreSQL每个数据库都有连接器、查询构建器、事务处理器这些属于“一族”的对象。代码结构上抽象工厂定义一组创建方法每个具体工厂返回一族产品。这套模式在PHP老牌框架Yii里就有体现你换一个db组件底层连接的、查询的、Schema的都会跟着切换。实际业务里其实很少会自己写抽象工厂因为现代框架的容器和配置系统已经在做类似的事情。但当你需要理解一个兼容多平台、多存储的SDK时抽象工厂的知识就是敲门砖。2.4 建造者模式复杂对象的组装过程分离创建型里我个人觉得建造者是最被低估的。它在PHP里的一个天然应用就是查询构建器。Laravel的DB::table(users)-where(...)-orderBy(...)-get()其实就是建造者思路。class QueryBuilder { private array $selects []; private array $conditions []; private array $orderBys []; public function select(string ...$columns): self { $this-selects array_merge($this-selects, $columns); return $this; } public function where(string $column, string $operator, mixed $value): self { $this-conditions[] [$column, $operator, $value]; return $this; } public function orderBy(string $column, string $direction asc): self { $this-orderBys[] [$column, $direction]; return $this; } public function getQuery(): string { // 组装SQL省略具体实现 $sql SELECT . implode(, , $this-selects) . FROM users; return $sql; } }建造者的核心点是对象结构复杂创建过程应该独立于组装过程。每一层方法返回self链式调用就形成了流畅的API。这里面有一个很重要的设计技巧叫“流式接口”它和建造者经常一起出现。PHP的很多原生类也这么干比如DateTimeImmutable的modify()、setTime()都返回新的实例只是PHP原生没有强制要求在一个类里完成。2.5 原型模式用克隆代替new原型模式在PHP里的存在感很低因为PHP的数组天然是值拷贝对象默认是引用而且有clone关键字来实现浅拷贝。这个模式的核心价值是当创建对象成本高时复制一份现成的对象比重新创建更高效。$original new ComplexObject(); $original-loadHeavyData(); // 很多地方的配置都相同只需改变少量字段 $copy1 clone $original; $copy1-setUserId(1);注意clone是浅拷贝对象里的属性如果是对象复制后的两个对象会共享同一个内部对象。要深度复制需要自己实现__clone魔术方法。这个模式我在实际PHP业务中几乎没主动用过理解它有助于搞懂PHP对象的赋值和引用机制但确实不需要刻意寻找使用场景。3. 结构型模式如何把类组织成更大的结构创建型管对象的出生结构型管类和对象如何组合成更大的结构。PHP是单继承语言这一点让组合优于继承成为更重要的信条也让结构型模式在日常开发中无处不在。3.1 适配器模式接口不兼容的“转接头”几乎每个PHP开发者在职业生涯中都会写至少一次适配器。最经典的就是邮件发送项目里可能使用过phpmailer、swiftmailer或symfony/mailer如果业务代码直接依赖具体某个库将来替换时就要改很多地方。适配器模式把三方库包装成统一接口。interface MailerInterface { public function send(string $to, string $subject, string $body): bool; } class SymfonyMailerAdapter implements MailerInterface { public function __construct(private readonly Symfony\Component\Mailer\Mailer $mailer) {} public function send(string $to, string $subject, string $body): bool { $email (new Email()) -to($to) -subject($subject) -html($body); $this-mailer-send($email); return true; } }适配器解决的问题不是“代码不够美”而是“替换成本太高”。如果你的项目可能更换第三方SDK或者同时支持多个类似服务比如短信服务商A和B在进门的地方加一层适配将来就省大劲。适配器还可以用于让老代码兼容新接口我遇到过的典型情况是旧系统有一个OrderService的类方法叫createOrder新接口期望叫placeOrder用一个适配器包装旧类两边都不得罪。3.2 桥接模式多维变化维度解耦桥接是结构型里理解成本比较高的一个。它的典型场景是一个类有多个独立的变化维度时如果只用继承会产生类爆炸。比如消息既分“紧急程度”普通、加急又分“发送方式”短信、邮件、App推送如果继承组合2x3会产生6个子类而且加一个新维度会指数增长。桥接的做法是把“抽象部分”和“实现部分”分离两边各自独立变化。abstract class Message { public function __construct(protected MessageSender $sender) {} abstract public function send(string $content): void; } class NormalMessage extends Message { public function send(string $content): void { $this-sender-send($content); } } class UrgentMessage extends Message { public function send(string $content): void { $this-sender-send([紧急] $content); } } interface MessageSender { public function send(string $content): void; } class EmailSender implements MessageSender { ... } class SmsSender implements MessageSender { ... }现在消息类型变化时不用动SenderSender变化时不用动消息类型。桥接的核心是识别出“抽象”和“实现”两个维度在抽象中持有实现的引用而不是用继承去绑死。PHP项目里遇到多维度组合的业务对象时桥接是很好的思考方向。3.3 组合模式树形结构与部分-整体组合模式让客户端统一对待单个对象和组合对象。最典型的就是文件系统一个文件夹里既有文件也有子文件夹对用户来说都属于“可以展示大小”的东西。在PHP里菜单树、权限树、组织架构树都是这个模式的应用场景。interface Node { public function getName(): string; } class FileNode implements Node { public function __construct(private readonly string $name) {} public function getName(): string { return $this-name; } } class DirectoryNode implements Node { private array $children []; public function __construct(private readonly string $name) {} public function add(Node $node): void { $this-children[] $node; } public function getName(): string { $names [$this-name]; foreach ($this-children as $child) { $names[] $child-getName(); } return implode(/, $names); } }组合模式的关键设计点叶子节点和组合节点实现同一个接口组合节点内部维护子节点集合。它让递归遍历变得很自然PHP里做树形菜单的时候这个思路配合递归函数会非常舒服。3.4 装饰器模式动态增强而不改源码装饰器是我个人在PHP业务里用得最频繁的模式之一。它解决的核心问题是在不修改原有类代码的前提下给对象动态地添加职责。和继承相比装饰器更灵活可以组合多种行为和“直接改类”相比装饰器不违背开闭原则。经典的例子是HTTP中间件但其实更贴近的是缓存、日志、权限校验这类横切关注点。interface NotifierInterface { public function notify(string $message): void; } class BasicNotifier implements NotifierInterface { public function notify(string $message): void { // 默认发送逻辑比如邮件 } } abstract class NotifierDecorator implements NotifierInterface { public function __construct(protected NotifierInterface $wrapped) {} } class LogNotifier extends NotifierDecorator { public function notify(string $message): void { // 记录日志 $this-wrapped-notify($message); } } class WechatNotifier extends NotifierDecorator { public function notify(string $message): void { $this-wrapped-notify($message); // 额外发送微信通知 } } // 使用时灵活组装 $notifier new BasicNotifier(); $notifier new LogNotifier($notifier); $notifier new WechatNotifier($notifier);接口一个实现N个装饰器类每个装饰器持有且仅持有同一个接口类型的对象然后任意叠加。装饰器模式有一个要求装饰器和被装饰对象实现同一个接口这样用户无感知。在Laravel里类似的机制无处不在。中间件Pipeline本质上就是装饰器链路的变体。你定义一个中间件请求经过一层层装饰最后到达控制器每层都可以在核心操作前或后添加行为。3.5 外观模式给复杂子系统一个门面外观模式是结构型里最“朴素”也最好用的。它做的事就一句话给复杂的子系统提供一个统一的简单入口。我去很多公司review代码时都会发现有些类对外暴露了几十个public方法调用方要按顺序调ABCDE一旦漏一步就出错。外观模式可以把这些子系统的交互封装成一个高度聚合的门面方法。真实案例一个下单流程涉及库存校验、订单创建、优惠计算、库存扣减、消息通知。业务控制器如果直接去调这五个子系统代码会长且脆弱。做一个OrderFacade暴露一个checkout(array $cart): OrderResult内部编排这五步控制器只依赖这这一个门面。class OrderFacade { public function __construct( private readonly InventoryService $inventory, private readonly OrderService $orders, private readonly PromotionService $promotion, private readonly NotificationService $notification, ) {} public function checkout(array $cartItems): OrderResult { $this-inventory-validate($cartItems); $order $this-orders-create($cartItems); $discount $this-promotion-calculate($order); $order-applyDiscount($discount); $this-inventory-decrease($cartItems); $this-notification-sendOrderConfirmation($order); return new OrderResult($order); } }门面自己并不包含业务逻辑它只是一个组织的壳。真正的好处是调用方只要认识一个门面不需要知道子系统怎么协作。耦合就从“控制器与五个服务”变成了“控制器与门面、门面与五个服务”链条清晰也更符合最少知识原则。3.6 代理模式控制访问和延迟加载代理模式和装饰器结构上很像一个持有一个真实对象对外暴露相同接口。区别在目的装饰器是“增强”代理是“控制”。控制什么访问权限、延迟加载懒加载、日志记录、远程代理。在PHP里ORM是代理模式的最典型的隐藏案例。你用Doctrine的时候$article-getComments()第一次访问时才会真正去查数据库。它返回的看似是一个集合实际上是一个Proxy对象或LazyCollection真正的数据在你真正遍历时才加载过来。这就是一个延迟加载代理避免了一次大查询把用不到的关联数据全部查出来。如果自己写一个简单的延迟加载代理class HeavyServiceProxy implements HeavyServiceInterface { private ?HeavyService $realService null; public function __construct( private readonly HeavyServiceLoader $loader, private readonly string $serviceId ) {} public function process(): string { if ($this-realService null) { $this-realService $this-loader-load($this-serviceId); } return $this-realService-process(); } }代理在PHP框架里还有一个知名应用是路由的延迟分发。一个拥有100个路由定义的框架启动时不会把每个路由对应的控制器都实例化而是等请求命中具体某个路由时才加载对应控制器。这就是代理的思路减少了无效的开销。3.7 享元模式大量细粒度对象的共享享元是为了支持大量细粒度对象而采用共享的技术。PHP数组非常强大这降低了享元模式的存在感但在处理海量数据格式化、游戏开发等场景还是有价值。核心是区分“内部状态”可共享存在享元对象中和“外部状态”依赖场景由客户端维护。比如你要给10万行数据做格式化成Word文档样式每行都有字体、对齐、颜色等信息。如果每行都新建一个样式对象内存爆炸。把这些样式提取为有限的几种Style每行只是引用其中一个。class StyleFactory { private array $styles []; public function getStyle(string $font, int $size, string $color): TextStyle { $key md5($font . $size . $color); if (!isset($this-styles[$key])) { $this-styles[$key] new TextStyle($font, $size, $color); } return $this-styles[$key]; } }在PHP里享元的实际意义更多是提醒不要为了写代码方便把所有动态数据都塞进一个对象里。数据量大了之后同一个共享体要尽量复用一个实例。4. 行为型模式对象之间的协作与职责分配行为型模式是23种模式里占比最大、也最能体现设计功底的部分。很多模式的差别很细微容易被搞混。这里我会结合PHP特性尽量用通俗的语言讲透。4.1 职责链模式一层层传递直到有人处理职责链模式让多个处理器形成一条链请求沿着链传递每个处理器决定自己处理还是抛给下一个。真实世界的类比就是客服工单一级客服处理不了的升级到二级逐级往上。PHP中最著名的应用是Laravel的中间件管道。abstract class Handler { private ?Handler $next null; public function setNext(Handler $handler): Handler { $this-next $handler; return $handler; } public function handle(Report $report): ?Report { if ($this-canHandle($report)) { return $this-process($report); } return $this-next?-handle($report); } abstract protected function canHandle(Report $report): bool; abstract protected function process(Report $report): ?Report; }这种链路模式在审批流、内容审核关键词过滤、用户注册校验等场景非常合适。职责链模式最需要注意的设计点是链中的每个处理器应该担当单一职责处理不了的快速放行避免在链中写越来越长的if-else。另外要特别注意?-空安全运算符如果链尾没有人处理需要返回默认结果或者抛明确的异常。4.2 命令模式把请求封装成对象命令模式把请求的发起者与接收者解耦让请求本身变成可存储、可排队、可撤销的对象。比如编辑器的“撤销”每一次操作都被封装成一个命令对象撤销栈里存的是这些对象。PHP里命令模式的一个常用变体是消息队列任务。比如你把一个耗时的邮件发送任务封装成Command推到队列里Worker从队列取出Command并执行。这个环境下命令对象要能被序列化/反序列化所以它不宜持有复杂的闭包或非序列化资源。Laravel的Job就是命令模式的产物。我自己在处理“批量数据导出”时也用过命令模式每种导出类型是一个Command类统一的接口execute(array $options): FileResult。任务录入后即使用户断线了后台命令依然可以继续执行。如果你是写控制台场景较多的命令模式用起来很顺手——它把“怎么执行”和“何时执行”解开也让任务可以排队、可以重试。4.3 解释器模式自己定义一种小语言解释器模式用于定义一门语言的语法并解释它的句子。在业务代码里它的应用非常受限。最典型的案例是正则表达式引擎、SQL解析器等。PHP中比较通俗的例子是表达式计算器支持price 100 AND status active这种条件组合解析。但说句实话如果你有这种需求通常会引入现成的解析库如symfony/expression-language或topthink/think-orm的where数组语法而不会自己造轮子。解释器模式的学习价值在于它让你理解表达式树AST是怎么组织的。每一个语法规则对应一个类叶子是终结符表达式非叶子是非终结符表达式。这种模式不需要硬往业务上套。知道它可以用来做自定义规则引擎、把复杂字符串转成可执行对象模型就足够了。4.4 迭代器模式不同的聚合统一的遍历迭代器模式提供一种方法顺序访问一个聚合对象内的元素而又不暴露其内部表示。在PHP里这个模式被内置到了语言层面。Iterator、IteratorAggregate接口配合foreach使用让任何类都可以被遍历。class UserCollection implements IteratorAggregate { public function __construct(private readonly array $users) {} public function getIterator(): Traversable { yield from $this-users; } }有了生成器之后实现迭代器非常优雅。而且PHP的SPL扩展里已有很多现成迭代器比如DirectoryIterator遍历目录、RecursiveIteratorIterator遍历树形结构、LimitIterator截取集合的一部分。框架里的Collection类Laravel的Collection本质就是一个功能加强版的迭代器容器。日常开发中你可能不太会觉得自己在“用迭代器模式”但只要你在foreach一个对象而不是数组你其实已经在和迭代器打交道。4.5 中介者模式把网状依赖改成星型依赖中介者模式用起来也比较少但它的思想对设计高质量模块非常重要。在没有中介者时多个模块相互直接通信像蜘蛛网一样改一个模块影响所有模块。引入中介者后模块之间不直接交互而是通过中介者转发。经典场景是聊天室用户不发消息给另一个用户而是发给聊天室服务器由服务器转给其他人。在PHP的系统架构中可以当作中介者的有事件调度器。拿用户注册举例。注册成功之后需要发欢迎邮件、送优惠券、通知风控、更新统计。如果想不用中介者在注册服务里一处一处写调用注册服务会越来越胖。引入事件调度器作为中介者后注册服务只负责用户注册然后发布一个UserRegistered事件。关心这个事件的监听器各自完成自己的事。这就是中介者模式观察者模式在框架里的典型融合。4.6 备忘录模式状态的快照与恢复备忘录模式用来捕获并外部化一个对象的内部状态以便将来可以恢复。对应到用户系统就是草稿保存、快照回滚、命令撤销。PHP本身提供了序列化机制这让备忘录的实现非常直白把需要备份的对象serialize()之后存起来恢复时unserialize()回来。但要注意盲目的深拷贝可能导致数据量非常大。在业务设计上除了“存状态”备忘录模式还提醒你要有“版本”概念。我在自己设计的配置系统里用过这个思路每次修改配置都保存一个版本快照到历史表出问题时可以回滚到任意版本。这里的困难点在于快照的粒度把控全量快照简单但占空间差异快照省空间但恢复逻辑复杂。对于中小企业系统我建议能用全量就别优化内存可靠性优先。4.7 观察者模式一个对象变化多个对象跟着动观察者模式在PHP里算是使用最广泛的行为型模式之一。它的定义是在对象之间定义一对多的依赖当一个对象主体状态改变时所有依赖它的对象观察者都会收到通知并自动更新。我举一个实际的库存预警例子。库存服务在扣减库存后如果发现某个SKU低于阈值传统的做法是在库存服务里写判阈值、发短信、发邮件、更新大屏。每增加一个新的“关心库存变化的东西”库存服务就要改动一次。观察者模式让库存服务只发布“库存变化”事件其他事情让观察者去做。interface Observer { public function update(string $event, array $data): void; } class InventoryService { private array $observers []; public function attach(Observer $observer): void { $this-observers[] $observer; } public function decrease(int $skuId, int $quantity): void { // 扣库存逻辑... $this-notify(inventory.decreased, [sku_id $skuId, quantity $quantity]); } private function notify(string $event, array $data): void { foreach ($this-observers as $observer) { $observer-update($event, $data); } } }SplSubject和SplObserver是SPL提供的原生观察者接口可以帮助快速实现。现代PHP框架里事件监听就是观察者模式的成熟实现。比起自己写一个简易事件系统直接用框架的Event或Laravel的Event更稳妥因为框架帮你解决了事件监听器排序、异常处理、异步队列等许多细节。这里有一个实践上的建议观察者里的逻辑要尽量是“低频、低耦合”的避免监听器之间又互相依赖。一个事件同时触发多个监听器时如果一个监听器抛异常默认会影响后面的监听器执行。在框架级事件系统里通常有处理这种异常的机制但如果你是自己实现一定要注意。4.8 状态模式把分支逻辑拆到状态类里状态模式和策略模式结构几乎一样但意图完全不同。状态模式针对的是“一个对象因为状态不同会有不同行为”而且状态之间可以流转。比如订单状态待支付、已支付、已发货、已完成、已取消。每个状态下“执行某个动作”的行为是不同的。用传统的写法就是一大坨switchswitch ($order-status) { case pending: // ... break; case paid: // ... break; }这种代码一旦逻辑变重case分支越来越长可维护性直线下降。状态模式的做法是把每个状态封装成类interface OrderState { public function pay(Order $order): void; public function ship(Order $order): void; public function complete(Order $order): void; } class PendingState implements OrderState { public function pay(Order $order): void { // 处理支付逻辑 $order-setState(new PaidState()); } public function ship(Order $order): void { throw new RuntimeException(待支付订单不能发货); } // ...其他方法 }状态模式的价值在于把原先散落在业务流程里的“if 状态 xx”集中到状态类里每个状态只关心自己合法的行为和能流转到哪些状态非法操作自然就被拦下了。我个人经验是状态模式不要一上来就用。当订单状态流转在2-3个状态、每个状态分支逻辑都不长的时候用switch或match足够。等到状态数超过5个或者某些状态的处理逻辑超过了10行再去做状态模式的重构。理解状态模式时把“行为随状态变化而变化”这个本质抓住就不会和策略模式搞混。4.9 策略模式算法的封装与切换策略模式的实际使用频率非常高它就是“面向接口编程”的集中体现。策略模式定义一族算法让它们可以互相替换算法的变化不影响使用算法的客户端。最典型的场景是订单计算优惠价或者快递运费计算不同地区不同计费规则。简单说就是“我有一件事要做但是怎么做有很多种版本我要把它们封装起来运行的时候随意切换”。interface ShippingStrategy { public function calculate(Order $order): float; } class FlatRateShipping implements ShippingStrategy { public function calculate(Order $order): float { return 9.9; } } class WeightShipping implements ShippingStrategy { public function calculate(Order $order): float { return $order-getWeight() * 1.5; } } class ExpressShipping implements ShippingStrategy { public function calculate(Order $order): float { return $order-getWeight() * 3 20; } } class ShippingCostCalculator { public function __construct(private readonly ShippingStrategy $strategy) {} public function calculate(Order $order): float { return $this-strategy-calculate($order); } }PHP 8.0之后第一公民的闭包、箭头函数让策略实现得非常轻量甚至不必总是建类。小型临时算法切换闭包传进去就够了。策略模式和回调模式Callback在思想上是相通的。框架里的依赖注入容器天然支持策略模式你把不同策略注册到容器通过接口解析实际使用哪一个。4.10 模板方法模式父类定骨架子类填细节模板方法在Web框架里几乎无处不在。它定义一个操作中的算法骨架将某些步骤延迟到子类中实现。核心就是“父类控流程子类改写步骤”。PHP本身是单继承所以模板方法实现非常直接但继承层级太深容易僵化使用时注意控制层数。一个现实中常见的例子是数据导入导出。不管是用户导入、商品导入还是订单导入流程都是打开文件、逐行读取、数据校验、逐行入库、生成报告。不同的只是校验规则和入库逻辑。abstract class DataImporter { final public function import(string $filePath): ImportReport { $handle fopen($filePath, r); $headers fgetcsv($handle); $processed 0; $failed 0; while (($row fgetcsv($handle)) ! false) { $data array_combine($headers, $row); try { $this-validate($data); $this-persist($data); $processed; } catch (\Throwable $e) { $this-recordError($row, $e-getMessage()); $failed; } } fclose($handle); return new ImportReport($processed, $failed); } abstract protected function validate(array $data): void; abstract protected function persist(array $data): void; abstract protected function recordError(array $row, string $message): void; }父类的模板方法import用final修饰防止子类破坏流程。子类只需要实现几个抽象方法。这其实就是“控制反转”的一种体现——父类在控制流程子类在填充内容。Laravel的中间件、路由分发、Exception Handler很多都有模板方法的身影。如果要写框架模板方法是必会的基础模式。4.11 访问者模式在不改变类的前提下增加操作访问者模式是23种中最难理解也最容易被认为“没用”的模式。它解决的核心问题是一个稳定的对象结构中想增加新的操作但不想改这些对象类本身。它的巧妙之处是把“数据结构”和“基于数据的操作”分离。举个例子。一个报表系统里有员工和部门节点需要导出成HTML、JSON、XML三种格式。如果每种格式都在节点类里写一个exportHtml方法类会像茅坑一样越填越臭。访问者模式把格式导出逻辑放到Visitor类里每个Visitor是一种格式的完整导出逻辑被导出对象的类只需要提供一个accept方法。访问者的代价是理解成本高对象结构增加新类时每个访问者都要加新方法而且PHP是动态语言方法重载不严格实现访问者模式时需要使用instanceof或方法名拼接来模拟双分派。在普通业务开发中不推荐使用。但如果你在做编译器、AST遍历、报表引擎这类偏底层或结构固定的应用访问者模式依然是一个值得掌握的工具。5. 真实项目中如何选择和落地设计模式模式学完了最大的问题来了我到底什么时候要用很多人都觉得设计模式“学了用不上”其实是因为没有一套判断的脚手架。下面分享一些我实际做技术决策时使用的判断维度。5.1 识别变化点让模式来适应变化这是最高层的方法论。不要问“这里能用什么模式”而是先问“这里未来什么可能会变”。变化的维度决定了模式的选择方向。比如“创建对象时类型不确定未来还可能增加类型”——选工厂方法或抽象工厂。“同一个请求可能有多种处理方式处理逻辑需要动态叠加”——选装饰器。“一组算法可替换不同的业务对象有不同的实现”——选策略。“不同状态下行为变化明显分支逻辑复杂”——选状态模式。“一个事件发生后很多模块需要响应而且未来响应者会不断增加”——选观察者。这种“先分析变化点再套用模式”的方式能避免为了模式而模式的过度设计。我刚带团队的时候经常给下面的开发说如果代码里出现了大片的if-elseif-else且每个分支都大于10行那就是模式要介入的信号——先别急着用策略或状态仔细看这些分支是沿着什么维度的变化再来决定。5.2 过度设计的代价模式不是装饰品必须承认滥用设计模式比不用更难受。我也见过一个个“满汉全席”一个Utils类不做任何拆分却有几千行代码号称用了门面、单例、工厂模式其实只是把逻辑堆在一起再加了个壳。更常见的是很多人为了“未来可能需要扩展”写了大量的抽象类和接口结果业务半年也没变化代码却要多维护两层。我的建议是“有一个变化源才开始抽象”。如果一个模式需要你额外写4-5个类来解决一个当前仅有1个固定实现的问题而这个变化在未来半年内都不可预见那这个模式就是摆设。真正的高手是“重构出来的”先写出简单清晰的代码在需求的变化真正到来时再借助模式让重构安全快速地完成。PHP中的设计模式落地很多时候是“半自动”的。你只要把接口定义好、实现类按规范放好框架的容器会替你解决组装问题。模式的核心思想是解耦是管理变化不是为了美观。5.3 从框架和源码中反推模式事半功倍如果你觉得学习设计模式很枯燥那我强烈建议一个思路直接读框架源码反推它用了什么模式。以Laravel为例Container的服务绑定是工厂模式/单例模式。Pipeline中的中间件是职责链模式。Event Dispatcher是观察者模式。Collection的map、filter内部有迭代器模式。Manager类比如CacheManager、LogManager是工厂方法模式。Str::replaceArray这些字符串处理看着是静态工具但设计上属于策略模式、外观模式的混合。Symfony 也一样Console 的 Input/Output 是多层装饰。HttpKernel 的请求处理是职责链和模板方法。事件管理器是观察者。没有哪种学习方法比“读你业务中实际会用的框架源码”更扎实的了。我当年花了两周时间只做了一件事下载Laravel框架源码用IDE全局搜索implements和extends逐个接口追实现把容器、事件、中间件三条线硬啃了一遍。啃完之后设计模式的细节全部串起来了。6. 实战经验手写一个简化的多支付渠道系统光说不练假把式。我结合自己的支付系统重构经历来写一个综合案例——假设我们要接入微信支付和支付宝后续还要接银联或虚拟货币。这个场景非常适合同时使用策略模式、工厂模式和适配器模式。可以让你直观感受设计模式组合使用时的威力。6.1 第一步定义统一支付接口策略首先定义一个支付接口把所有支付渠道的公共操作抽象出来interface PaymentGateway { public function createPayment(float $amount, array $orderInfo): PaymentResult; public function refund(string $transactionId, float $amount): RefundResult; public function verifyCallback(array $callbackData): bool; }微信和支付宝的SDK千差万别这个接口屏蔽了它们各自的差异性。后续业务中只需要操作PaymentGateway接口类型不跟具体SDK直接接触。6.2 第二步编写具体渠道适配器适配器模式因为微信、支付宝的第三方SDK不遵循我们统一的接口所以需要写适配器把这些SDK包装进来。class WechatPayGateway implements PaymentGateway { public function __construct(private readonly WechatPaySdk $wechatSdk) {} public function createPayment(float $amount, array $orderInfo): PaymentResult { $response $this-wechatSdk-unifiedOrder([ total_fee (int) round($amount * 100), out_trade_no $orderInfo[order_no], ]); return new PaymentResult($response[code_url], wechat); } // refund / verifyCallback ... } class AlipayGateway implements PaymentGateway { public function __construct(private readonly AlipayClient $alipayClient) {} // 类似实现 }这里有一个要注意的点微信的金额单位是“分”支付宝的金额单位是“元”。如果没有适配层极容易在某个地方漏掉单位换算导致金额错误这种事故在接入第三方支付时非常常见。适配层把“差异”关在里面外部调用方永远用统一的“元”。6.3 第三步工厂创建支付渠道有了不同渠道的适配器后业务侧需要一个工厂来根据字符串标识创建对应的渠道对象。class PaymentGatewayFactory { public function __construct(private readonly Container $container) {} public function make(string $channel): PaymentGateway { return match ($channel) { wechat $this-container-make(WechatPayGateway::class), alipay $this-container-make(AlipayGateway::class), default throw new InvalidArgumentException(Unsupported payment channel: $channel), }; } }如果走依赖注入容器这里的工厂可以非常轻量仅做字符串到类名的映射。以后要增加新的支付渠道不需要修改现有业务代码只需要新增适配器类和一个新的映射关系。6.4 第四步业务侧的使用方式在真正的下单服务里流程变简单了。只依赖工厂和接口不关心渠道的底层实现。class PaymentService { public function __construct( private readonly PaymentGatewayFactory $factory, ) {} public function pay(string $channel, float $amount, array $orderInfo): PaymentResult { $gateway $this-factory-make($channel); return $gateway-createPayment($amount, $orderInfo); } }以后如果加新的渠道核心支付逻辑的调用方式几乎不用变只替换成新的策略与适配器即可。这就是策略适配器工厂模式组合使用的典型结构。我在做这套支付系统重构后深刻的感受是改动从一个巨大的Service类里解放了出来渠道越多优势越明显。如果只有一两个支付渠道且永远不变这个设计确实会显得“重”但一旦有三四个渠道、每个渠道参数又都有细微差异你会发现没有抽象层的日子就是灾难。6.5 常见问题快查把上面整个过程实际落地时容易踩的问题有这些回调验签不一致。微信、支付宝的回调参数名和验签方式各不相同务必在适配器里就完成验签转换不要让控制器直接处理回调的原始数据。如果控制器被三方回调数据结构反向耦合将来每加一种方式都要改控制器。日志不按渠道隔离。排查线上问题时分渠道日志会帮你快速定位是哪个渠道出的问题。建议适配器内部用LoggerInterface写独立的日志通道。单元测试难写。如果没有接口抽象测试支付时需要mock具体SDK非常痛苦。有了PaymentGateway接口测试时可以无缝替换成Fake测试替身。金额精度问题。PHP中float有精度损失涉及货币强烈建议以分为单位存整数或在交互边界用字符串。7. 设计模式学习的“快与慢”最后聊一些规划和心法层面的东西。学习设计模式不应该是一条直线走完就结束而是要结合不同阶段的认知水平反复咀嚼。最初你可能需要一个个模式过一遍基础概念和php代码实现但到后面更重要的是掌握它们的共性和组合关系。7.1 几种似像非像的模式的辨别方法很多人学完之后最容易混淆的几个组合策略模式 vs 状态模式结构相同意图不同。策略的客户端主动选择使用哪种算法状态的“算法”由对象当前的状态决定且状态可以自动流转。问自己一句行为的选择权在“客户端”还是“状态本身”前者是策略后者是状态。装饰器 vs 代理结构相似意图不同。装饰器的目的是增强原有对象客户端知道它还存在代理的目的是控制真实对象的访问客户端可以不感知代理的存在。简单说装饰器“服务”被包装对象代理“管理”被包装对象。工厂方法 vs 抽象工厂工厂方法是一个方法生产一种产品通过继承扩展抽象工厂是一组生产方法通过组合生产一整个产品族。适配器 vs 外观适配器为了“接口兼容”而改变接口外观为了“简化操作”而提供统一新接口。适配器的对象是让它能正常工作外观的对象是让一堆东西好操作。7.2 建议的学习路线如果你想系统化地啃下这块知识我建议按这样的顺序第一阶段入门约1-2周把创建型5种先用简单的PHP代码实现一遍。写完之后看看框架源码里的Container如何管理对象生命周期。第二阶段进阶约2-3周把结构型7种过一遍重点学习装饰器、适配器、组合之间的区别再开一个框架源码明确每个设计模式藏在哪个组件中。第三阶段深化约3-4周啃行为型建议把重点放在观察者、策略、模板方法、状态、职责链上。手写几个模拟场景订单状态机、用户事件监听、消息推送链处理。第四阶段综合持续进行找一个你熟悉的老项目按照“识别变化点”的方式逐步重构其中的一两个核心模块体会模式在真实需求驱动下的应用。7.3 推荐资源《设计模式可复用面向对象软件的基础》GoF。经典中的经典虽然以C描述但思想是通用的。《Head First 设计模式》。入门友好图示丰富适合第一次接触者。Laravel/Symfony 框架源码。英文源码是第一手资料依赖容器的解耦思路值得精读。PHP官方文档SPL中关于Observer、Iterator等接口的用法。7.4 心态上有一个大坑学模式最忌讳的就是“会了榔头看什么都像钉子”。以前我看到一些初级开发者写一个Hello World都要套上工厂单例策略这不是设计模式这是芭比娃娃换装纯粹自我感觉良好。模式是在代码复杂度足够高的时候才显示威力的简单场景用简单写法。反过来也一样。一些经验丰富的开发者会走向另一个极端说“设计模式是Java圈子的糟粕PHP用数组就够了”。这种心态同样有害。PHP再怎么独特项目复杂度到了一定级别设计模式依然是有效的思考工具。区别只是实现方式更轻。比如模板方法可以用trait加回调替代观察者可以直接用闭包数组组合模式用嵌套数组可能比对象树更方便。模式是思想语言只是载体。设计模式学到一定水平之后你会发现它就像写字时的间架结构。小孩子学写字先学笔画、再照字帖临摹大人写字时不会再去想每一笔要怎么摆但写出来的字会自然体现出结构感。写代码也一样先按照模式框架刻意练习慢慢地它们就融进你的设计直觉里了。等到看你代码的人说“这代码结构真清晰改起来真舒服”那才算是真正通关了。