开闭原则如何通过依赖注入与策略模式实现?
解读
国内大厂(阿里、腾讯、字节)及二线电商、SaaS 公司面试时,常把“SOLID”作为必考题。其中“O”——开闭原则(Open-Closed Principle)是区分“只会写业务代码”与“能写可扩展框架”的关键分水岭。
面试官真正想听的是:
- 你能否用 PHP 语言机制(而不是 Java/C# 概念)把“对修改关闭、对扩展开放”落地;
- 能否把依赖注入(DI)与策略模式结合,写出可单测、可配置、可热插拔的代码;
- 是否熟悉 Composer + PSR 的自动加载,能一句话说明“新策略类加进来为何不用改一行旧代码”。
回答时切忌背定义,必须给出“可运行的 PHP 代码骨架”+“配置扩展点”,并点出 Laravel 容器/ Symfony DI 组件的底层原理,才能拿到高分。
知识点
- 开闭原则:软件实体应对扩展开放、对修改关闭。
- 依赖注入(DI):把依赖的创建权从“类内部”转移到“外部容器”,通过构造函数或 setter 传入,满足 DIP(依赖倒置)。
- 策略模式:定义一族算法,把每个算法封装起来,并使它们可以互换;策略类实现同一接口,客户端面向接口编程。
- 结合方式:DI 负责“运行时把正确策略实例注入”,策略模式负责“新增策略时只增加新类+配置,不改动旧类”。
- PHP 落地细节:
- 使用 PSR-4 自动加载,新策略文件放在指定 namespace,Composer dump-autoload 即可;
- 构造函数声明接口类型,由 DI 容器自动解析实现类;
- 配置层(数组、yaml、env)决定映射关系,实现“零代码改动”切换策略;
- 单元测试可 Mock 接口,验证开闭效果。
答案
以下示例用原生 PHP 8 语法写出一个“订单折扣”场景,演示“新增节日折扣策略”时,业务核心类 OrderService 一行不改,完全符合开闭原则。
- 定义策略接口
namespace App\Contract;
interface DiscountStrategy
{
public function calculate(float $amount): float;
}
- 实现两种策略
namespace App\Strategy;
class VipDiscount implements \App\Contract\DiscountStrategy
{
public function calculate(float $amount): float
{
return $amount * 0.9;
}
}
class FestivalDiscount implements \App\Contract\DiscountStrategy
{
public function calculate(float $amount): float
{
return $amount * 0.8;
}
}
- 业务核心类(对扩展关闭)
namespace App\Service;
use App\Contract\DiscountStrategy;
class OrderService
{
private DiscountStrategy $discount;
// 依赖注入:由外部容器传入具体策略
public function __construct(DiscountStrategy $discount)
{
$this->discount = $discount;
}
public function settle(float $amount): float
{
return $this->discount->calculate($amount);
}
}
- 容器与配置(模拟 Laravel 容器思想,原生可运行)
$container = new class {
private array $bindings = [];
public function bind(string $abstract, string $concrete): void
{
$this->bindings[$abstract] = $concrete;
}
public function make(string $abstract)
{
$concrete = $this->bindings[$abstract];
return new $concrete;
}
};
// 配置层:一行配置即可切换策略,无需改 OrderService
$container->bind(\App\Contract\DiscountStrategy::class, $_ENV['DISCOUNT_CLASS']);
- 使用端
$order = new \App\Service\OrderService($container->make(\App\Contract\DiscountStrategy::class));
echo $order->settle(100); // 输出 90 或 80,取决于 env 配置
- 扩展演示
618 大促需要“满减折扣”,只需:
a) 新建class FullReductionDiscount implements DiscountStrategy;
b) 把.env里的DISCOUNT_CLASS换成新类名;
c)composer dump-autoload;
d) 部署上线,OrderService 0 改动。
面试官如追问“Laravel 如何做到自动注入”,可答:
“Laravel 容器通过反射读取构造函数参数类型,当发现参数是接口且存在对应 bind/上下文绑定(contextual binding)时,自动 new 实现类并注入,与上述手动 $container->make() 本质一致,但框架帮你省掉了工厂代码。”
拓展思考
-
策略过多时的治理:
- 使用“策略工厂+配置键”模式,把策略类名与业务语义(如 ‘vip’、’festival’)映射,避免在 .env 里写长类名;
- 结合 Laravel 的 Tag 功能,给策略打标签,实现批量缓存预热或监控。
-
运行时动态组合策略:
- 引入“装饰器模式”包装策略,支持链式折扣(先 VIP 9 折,再 Festival 8 折);
- 用 DI 的“链式绑定”或 Symfony 的 Decorator 标签,自动按优先级排序注入。
-
与发布/订阅搭配:
- 把策略切换事件抛到队列,监听者自动刷新策略缓存,实现秒级热更新;
- 保证高并发电商场景下,策略变更零停机。
-
测试策略:
- 在 PHPUnit 中通过
@dataProvider一次性测试所有策略实现,确保新增策略不会破坏旧断言; - 使用 PHP 8 的
named argument特性,让测试用例可读性更高。
- 在 PHPUnit 中通过
掌握以上思路,可在国内 PHP 高级/架构师面试中把“开闭原则”讲深、讲透,直接拿到“代码设计”维度的高分评价。