第4章 设计模式实战:单例、工厂、观察者、策略、依赖注入

📌 章节导读:设计模式是高级工程师的“通用语言”,面试中考察的不只是模式定义,而是你在真实业务中如何选择、实现、权衡。本章聚焦5种高频模式,拆解面试回答结构、项目落地模板与常见陷阱。
回答框架:设计模式实战四步法 Step1 定义 → Step2 场景 → Step3 实现 → Step4 权衡 1. 一句话定义 解决什么问题 2. 业务场景 项目中的具体需求 3. 代码实现 类图 + 关键代码 4. 优劣权衡 性能/扩展/复杂度 典型追问点 与什么模式对比? 单测如何写? 框架中用了哪些? 闭包替代方案?

📄 面试问答卡 1:单例模式

面试问题:请手写一个线程安全的单例模式,并说明在什么场景下适合使用单例?

考察点:
✓ 对全局唯一实例的理解(类加载/静态变量)
✓ 多进程/多线程环境下的安全性
✓ 实际业务判断力(避免滥用)

回答框架:
1. 定义:确保类只有一个实例,提供全局访问点。
2. 场景:配置管理器、数据库连接池、日志记录器。
3. 实现:使用 private static + getInstance(),注意双重检查锁(PHP中可用静态属性 + 构造函数私有化)。
4. 权衡:优点是节省资源;缺点是违反单一职责、不易扩展。

示例回答:
“以数据库连接池为例,单例确保全局只有一个池对象,避免重复创建连接。PHP中我使用 private static $instance 配合 private function __construct(),通过 getInstance() 返回实例。如果是多进程场景(如Resque Worker),则需要使用文件锁或共享内存实现。

可能追问:
- 单例如何实现序列化保护?
- Laravel 的 Service Container 中何时会注册单例?
- 单例与全局变量的区别?

⚠ 避坑提醒:高级面试中切忌只说“防止new多次”,需主动提“破坏单例的手段(clone/serialize/reflection)”以及对测试的影响(引入全局状态)。

📄 面试问答卡 2:工厂模式(简单工厂/工厂方法/抽象工厂)

面试问题:请对比简单工厂、工厂方法、抽象工厂三者的区别,并举例说明你用过的工厂模式。

考察点:
✓ 创建型模式的层次理解
✓ 是否能在框架源码中识别工厂模式
✓ 业务场景的匹配能力

回答框架:
1. 简单工厂:一个工厂类根据参数创建对象,扩展需修改工厂类。
2. 工厂方法:父类定义接口,子类决定实例化哪个类,符合开闭原则。
3. 抽象工厂:创建一组相关/依赖对象(产品族)。
4. 举例:支付渠道对接 – 使用工厂方法,每个支付方式对应一个Creator。

示例回答:
“在电商项目中,需要对接微信、支付宝、PayPal三种支付。我使用工厂方法模式:定义 PaymentCreator 接口,微信/支付宝分别实现 createPayment()。新增支付方式只需增加新的Creator类,无需修改已有代码。简单工厂适合产品类型极少且不变动的情况,抽象工厂适合产品族(如不同UI主题下生成按钮+输入框)。

可能追问:
- 工厂模式与策略模式的区别?
- Laravel 的 Facade 是工厂模式吗?
- 如何避免工厂类过于庞大?

⚠ 避坑提醒:不要过度设计,业务只有1-2种变化时用简单工厂即可。面试时常犯“背定义但说不清场景”,一定要结合项目举例。

📄 面试问答卡 3:观察者模式 + 策略模式 + 依赖注入

面试问题:请分别说一个观察者、策略和依赖注入的使用场景,并说明三者各自解决了什么问题。

考察点:
✓ 行为型模式的理解深度
✓ 能否区分松耦合的不同实现方式
✓ 对框架(如Laravel事件/容器)的熟悉度

回答框架:
1. 观察者:一对多通知,如用户注册后发送邮件+短信(Laravel Event & Listener)。
2. 策略:算法族独立切换,如订单折扣计算(普通会员/ VIP 不同折扣)。
3. 依赖注入:将依赖从类外部传入,控制反转(IoC),如构造函数注入 Repository。
4. 三者共同点:降低耦合;不同点:观察者强调“触发-响应”,策略强调“算法互换”,注入强调“依赖管理”。

示例回答:
“观察者:注册事件触发后,监听队列发送通知,新增逻辑只需增加Listener;策略:折扣策略对象可运行时替换,测试时方便Mock;依赖注入:控制器依赖UserService通过构造函数注入,方便单元测试替换真实实现。三者结合使用可以让系统高度解耦。”

可能追问:
- Laravel 的 Service Provider 属于哪种模式?
- 观察者模式中如何避免内存泄漏?
- 依赖注入与工厂模式的关系?

⚠ 避坑提醒:不要混淆“依赖注入”与“控制反转容器”。核心是“类不自己创建依赖,而是由外部传入”。面试时最好说出Laravel中 app()->bind() vs app()->singleton() 的区别。

📁 项目案例:订单中心设计模式实战

项目名称通用订单中心重构 + 多支付渠道对接
业务背景原有订单处理代码耦合严重,新增支付或优惠方式需修改核心类,导致回归测试频繁。
规模指标日均订单 50 万+;对接 6 种支付渠道;优惠规则 12 种;团队 4 人改造,周期 3 个月。
技术栈PHP 8.1 + Laravel 9 + Redis + MySQL + RabbitMQ
核心难点 - 支付方式动态增加,需避免每次改原有类
- 订单状态变更需通知多个下游(库存、物流、推送)
- 优惠策略频繁调整,需支持热切换
设计模式应用
  • 工厂方法:每个支付渠道一个 Creator,生成对应的支付请求对象。
  • 策略模式:订单折扣计算,根据用户等级/活动从策略容器中获取算法。
  • 观察者模式:订单状态变更事件 → 触发库存扣减、物流发单、短信通知。
  • 依赖注入:所有 Service 通过构造函数注入 Repository,便于单测。
面试高频追问 - 如何保证观察者事件处理失败不影响主流程?(队列 + 重试机制)
- 策略模式如何实现运行时热替换?(配置中心 + 缓存策略实例)
- 工厂方法中如何避免大量if-else?(注册表 + 自动发现)
可迁移回答话术 “在订单中心重构中,我们识别出三个变化点:支付方式、优惠策略、通知逻辑。分别用工厂、策略、观察者模式应对。核心原则是:把变化封装在独立的类中,通过接口或事件进行交互。这样做之后,新增一个支付渠道只需要增加一个Creator类并注册,不需要修改任何既有代码。”
💡 面试加分技巧:在描述项目时,主动说出“我们使用了依赖注入容器 + 自动装配”来管理工厂和策略的实例化,体现你对框架底层机制的理解。例如:“我们将所有支付工厂类通过 app()->bind() 注册到容器,使用接口别名自动解析,这样在业务代码中只依赖接口而不依赖具体实现。”