策略类必须实现统一接口,否则无法动态切换;context类应通过依赖注入而非硬编码创建策略;运行时切换需确保策略无状态且线程安全;动态加载策略须校验标识符合法性并禁用不安全类名拼接。

策略类必须实现统一接口,否则无法动态切换
策略模式的核心是「同一接口、不同实现」。如果每个策略类的函数名或参数不一致,Context 类就无法在运行时安全调用。比如一个算折扣的场景,DiscountStrategy 接口必须强制定义 apply() 方法,所有具体策略(StudentDiscount、VipDiscount、SeasonalDiscount)都得实现它,且返回类型一致(如 float)。
常见错误是漏写 interface 或让某个策略类只写了个 calculate() 而非 apply(),结果运行时报 Fatal error: Call to undefined method。PHP 8+ 的严格类型提示能提前暴露这类问题,但前提是接口已声明返回类型,例如:
interface DiscountStrategy
{
public function apply(float $price): float;
}
Context 类要避免硬编码策略实例化
Context 类负责持有并调用当前策略,但它不该自己用 new StudentDiscount() 这种方式创建策略对象——这会让切换逻辑散落在各处,也违背依赖倒置原则。
推荐做法是把策略实例通过构造函数或 setter 注入,或者由工厂/容器统一提供:
- 用构造注入:
new Context(new VipDiscount()) - 用 setter 切换:
$context->setStrategy(new SeasonalDiscount()) - 配合 DI 容器(如 Laravel 的
app()->make(DiscountStrategy::class)),便于测试和配置驱动
别在 Context 内部写 if ($type === 'vip') { return new VipDiscount(); } ——这种分支一旦多起来,就退化成“策略模式假面下的 if-else”。
运行时策略切换要考虑状态与线程安全
PHP-FPM 模式下,每个请求是独立进程或线程,所以 Context 实例本身无共享状态风险;但如果你把策略对象存在静态属性、全局变量或单例中,就可能引发并发问题。比如:
class Context
{
private static DiscountStrategy $strategy; // ❌ 危险:跨请求污染
}
更隐蔽的问题是策略自身带状态(如缓存上次计算结果、内部计数器)。若策略类不是无状态的(stateless),就不能被多个 Context 实例复用,否则行为不可预测。建议所有策略类保持 immutable 或至少不维护请求外的状态。
用字符串标识符选策略时,务必校验合法性
实际项目中常需要根据配置项(如数据库字段 discount_type 值为 "student")动态加载策略。这时容易直接拼类名:$class = ucfirst($type) . 'Discount'; new $class()。
这种写法危险在于:$type 来自用户输入或配置时,没过滤会导致类名注入、任意类加载甚至 RCE(配合反序列化 gadget)。正确做法是预定义映射表,并严格校验:
$mapping = [
'student' => StudentDiscount::class,
'vip' => VipDiscount::class,
'seasonal'=> SeasonalDiscount::class,
];
if (!isset($mapping[$type])) {
throw new InvalidArgumentException("Unknown strategy: {$type}");
}
$strategy = new $mapping[$type]();
另外注意自动加载是否启用(Composer 的 autoload 配置)、类名大小写是否匹配(Linux 环境区分大小写),这些细节在线上环境容易暴露。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











