上周 code review,看到一个同事写的类:
php
class OrderProcessor {
private $db;
private $logger;
private $mailer;
public function __construct() {
$this->db = new MySQLConnection();
$this->logger = new FileLogger();
$this->mailer = new Mailer();
}
public function process($orderId) {
$order = $this->db->query("SELECT * FROM orders WHERE id = $orderId");
// 业务逻辑...
$this->mailer->send($order['email'], '订单已处理');
$this->logger->log('订单处理完成');
}
}
我问:"这段代码有啥问题?"
同事想了想:"好像......耦合度有点高?"
我说:"不止。你现在用的是 MySQL,哪天要换 PostgreSQL,得改这个类。哪天要换成 Redis 日志,还得改这个类。哪天邮件服务换成 SMS 通知,还得改这个类。这代码写死了一切,改一处动全身。"
这让我想起一个老生常谈的话题:设计模式 。很多人觉得设计模式是"面试八股文",实际写代码用不上。但工作了这么多年,我发现恰恰相反------不懂设计模式也能写代码,但懂了之后,写出来的代码能少给后人挖很多坑。
今天不打算像教科书一样罗列 23 种设计模式,而是想聊聊我在 PHP 项目里真正用过的、觉得有用的那些"套路",以及它们是怎么帮我写出更容易维护的代码的。
一、工厂模式:把"new"关进笼子里
先说最常用的工厂模式。
上面那段代码最大的问题,就是直接在构造函数里 new 对象。这导致两个后果:
-
依赖写死了,没法替换
-
测试没法做------你不能 mock 数据库连接
工厂模式就是来解决这个问题的:把创建对象的责任从使用类里抽离出来。
1.1 简单工厂
php
interface DatabaseInterface {
public function query($sql);
}
class MySQLDatabase implements DatabaseInterface {
public function query($sql) {
// MySQL 实现
}
}
class PostgreSQLDatabase implements DatabaseInterface {
public function query($sql) {
// PostgreSQL 实现
}
}
class DatabaseFactory {
public static function create($type) {
return match($type) {
'mysql' => new MySQLDatabase(),
'pgsql' => new PostgreSQLDatabase(),
default => throw new InvalidArgumentException('不支持的数据库类型')
};
}
}
// 使用
$db = DatabaseFactory::create('mysql');
这样,OrderProcessor 就不需要知道具体用哪个数据库,只需要依赖 DatabaseInterface 。
1.2 工厂模式的真正价值
有人会说:"这不就是把 new 从 A 文件挪到 B 文件了吗?"
区别在于:当你要改的时候 。假设项目上线两年后,老板说"我们把数据库从 MySQL 换成 PostgreSQL",传统写法你要在所有用到数据库的地方改代码。用了工厂模式,你只需要改一行------工厂里的 create 方法 。
这就叫 "把变化封装起来" 。
二、单例模式:用得好是神器,用不好是祸根
单例模式可能是被误解最深的模式。它的初衷很简单:确保一个类只有一个实例 。
最常见的应用是数据库连接:
php
final class Database {
private static $instance = null;
private $connection;
private function __construct() {
$this->connection = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection() {
return $this->connection;
}
}
这样写的好处是:整个请求周期只创建一个数据库连接,避免反复建立连接的开销 。
2.1 单例的坑
但单例模式有几个陷阱,踩过的都懂 :
坑一:全局状态。单例本质上就是全局变量,过度依赖全局变量会导致代码难以测试。你的单元测试里可能跑着跑着,上个测试留下的状态污染了下一个测试 。
坑二:并发问题。在 Swoole 这类常驻内存环境下,单例会在多个请求间共享,如果类里有可变状态,就会互相干扰。
坑三:依赖隐藏 。类里直接调用 Database::getInstance(),等于隐式依赖了 Database 类。看代码的人不知道这个类依赖什么,必须读实现才知道 。
我的建议 :单例只适合真正的无状态服务,比如日志类、配置类。而且最好配合依赖注入使用,别在业务代码里直接调用静态方法 。
三、策略模式:把"如果...就..."消灭掉
策略模式是我最喜欢的行为型模式。它的核心思想:把不同的算法封装成独立的类,让它们可以互相替换 。
看个例子,支付方式的选择:
php
interface PaymentStrategy {
public function pay($amount);
}
class AlipayStrategy implements PaymentStrategy {
public function pay($amount) {
echo "使用支付宝支付:{$amount} 元";
// 支付宝具体逻辑
}
}
class WechatStrategy implements PaymentStrategy {
public function pay($amount) {
echo "使用微信支付:{$amount} 元";
// 微信具体逻辑
}
}
class CreditCardStrategy implements PaymentStrategy {
public function pay($amount) {
echo "使用信用卡支付:{$amount} 元";
// 信用卡具体逻辑
}
}
class ShoppingCart {
private $paymentStrategy;
public function setPaymentStrategy(PaymentStrategy $strategy) {
$this->paymentStrategy = $strategy;
}
public function checkout($amount) {
$this->paymentStrategy->pay($amount);
}
}
// 使用
$cart = new ShoppingCart();
$cart->setPaymentStrategy(new AlipayStrategy());
$cart->checkout(199);
如果没有策略模式,你会怎么写?大概率是 if (type == 'alipay') {...} elseif (type == 'wechat') {...},然后随着支付方式增加,这个 if-else 越来越长,改一处可能影响所有地方 。
策略模式把每种支付逻辑独立开,新增一种支付方式只需要加一个新类,不用改老代码------对扩展开放,对修改关闭 。
3.1 策略模式 vs 工厂模式
很多人分不清策略模式和工厂模式。其实核心区别在于目的 :
-
工厂模式:创建对象,得到的是对象本身
-
策略模式:执行算法,得到的是执行结果
工厂模式说"给你对象",策略模式说"帮你干活"。
四、依赖注入:告别"硬编码"的终极方案
前面说的工厂模式解决了对象创建的问题,但还有更好的做法------依赖注入。
4.1 什么是依赖注入
简单说,就是不自己 new,让别人把依赖"注入"进来:
php
class OrderProcessor {
private $db;
private $logger;
private $mailer;
// 所有依赖都通过构造函数传入
public function __construct(
DatabaseInterface $db,
LoggerInterface $logger,
MailerInterface $mailer
) {
$this->db = $db;
$this->logger = $logger;
$this->mailer = $mailer;
}
}
现在这个类不再关心依赖是怎么创建的,只管用 。
4.2 依赖注入容器
当项目变大,手动管理依赖会变得很繁琐:
php
$db = new MySQLDatabase();
$logger = new FileLogger();
$mailer = new Mailer();
$orderProcessor = new OrderProcessor($db, $logger, $mailer);
如果 OrderProcessor 又依赖其他服务,其他服务还有自己的依赖,手动创建就成了噩梦。
这时候需要 依赖注入容器(Dependency Injection Container)。
以 lucatume/di52 为例 :
php
use lucatume\DI52\Container;
$container = new Container();
// 绑定接口到实现
$container->singleton(DatabaseInterface::class, MySQLDatabase::class);
$container->singleton(LoggerInterface::class, FileLogger::class);
$container->singleton(MailerInterface::class, Mailer::class);
// 容器会自动解析依赖
$orderProcessor = $container->get(OrderProcessor::class);
容器的好处是 :
-
自动装配:容器会通过反射分析类的构造函数,自动注入需要的依赖
-
单例管理:可以指定哪些类共享同一个实例
-
延迟实例化:只有用到的时候才会创建对象,节省资源
4.3 接口绑定与上下文绑定
更高级的用法是"什么时候用什么实现" :
php
// 默认情况用 MySQL
$container->bind(RepositoryInterface::class, MySQLRepository::class);
// 但在处理 User 请求时,用 Redis 仓库
$container->when(UserPageRequest::class)
->needs(RepositoryInterface::class)
->give(RedisRepository::class);
// 绑定基本类型
$container->when(UserPageRequest::class)
->needs('$perPage')
->give(20);
这种"上下文绑定"让代码极度灵活,又不用改业务逻辑 。
五、设计模式的"反套路":什么时候不该用
说了这么多设计模式的好处,也得说说"反套路"------过度设计。
有次我看到一个项目,为了处理三种日志类型,引入了策略模式+工厂模式+抽象工厂模式。文件建了十几个,但核心逻辑就三行 if-else。
这就是典型的 "拿着锤子看什么都像钉子" 。
设计模式的"度"在哪里?我的经验是 :
5.1 只有确定会变化的地方才用模式
如果确定"这个支付方式一辈子不会变",写 if-else 也没问题。问题在于"一辈子"太长了------需求永远在变。
但反过来,如果"可能变但概率极低",也可以先写简单的,等真变了再重构。这叫 YAGNI(You Ain't Gonna Need It)原则。
5.2 模式是手段,不是目的
写代码的时候问自己:这代码半年后我看得懂吗?新人接手能改吗?如果加了新模式反而让代码更难理解,说明用错了 。
5.3 结合多态,让代码"活"起来
设计模式和面向对象的多态是天生一对。就像前面工厂模式的例子,通过接口和实现分离,再加上容器的自动装配,写出来的代码就像搭积木 。
六、写在最后:从"写代码"到"设计代码"
回头看开头那个 OrderProcessor,如果用上今天聊的这些"套路",应该长这样:
php
class OrderProcessor {
public function __construct(
private DatabaseInterface $db,
private LoggerInterface $logger,
private NotificationInterface $notifier
) {}
public function process($orderId) {
$order = $this->db->find($orderId);
// 业务逻辑...
$this->notifier->send($order->email, '订单已处理');
$this->logger->info('订单处理完成', ['order_id' => $orderId]);
}
}
然后在容器里组装:
php
$container->singleton(DatabaseInterface::class, MySQLDatabase::class);
$container->singleton(LoggerInterface::class, RedisLogger::class);
$container->singleton(NotificationInterface::class, SMSNotifier::class);
$processor = $container->get(OrderProcessor::class);
想换数据库?改一行。想换日志?改一行。想测试?传 mock 对象进去。
这就是"设计"的力量------不是为了让代码更"高级",而是为了让代码更容易被改变。
毕竟,软件开发的本质,就是应对变化 。