我在PHP里学到的“套路”与“反套路” 设计模式与依赖注入

上周 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 对象。这导致两个后果:

  1. 依赖写死了,没法替换

  2. 测试没法做------你不能 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 对象进去。

这就是"设计"的力量------不是为了让代码更"高级",而是为了让代码更容易被改变

毕竟,软件开发的本质,就是应对变化 。

相关推荐
马士兵教育2 小时前
2026年IT行业基本预测!计算机专业学生就业编程语言Java/C/C++/Python该如何选择?
java·开发语言·c++·人工智能·python·面试·职场和发展
JSON_L2 小时前
endroid/qr-code生成二维码报错
php·二维码
野犬寒鸦2 小时前
面试常问:HTTP 1.0 VS HTTP 2.0 VS HTTP 3.0 的核心区别及底层实现逻辑
服务器·开发语言·网络·后端·面试
漏刻有时2 小时前
CentOS 不定时 OOM 根治方案:PHP-FPM 进程管控 + Swap 扩容 + 全维度监控
android·centos·php
geovindu2 小时前
python: Null Object Pattern
开发语言·python·设计模式
lisus20072 小时前
GO并发统计文件大小
开发语言·后端·golang
梦游钓鱼2 小时前
Logger.h和Logger.cc文件分析
开发语言·c++
CRMEB系统商城2 小时前
CRMEB标准版系统(PHP)v6.0公测版发布,商城主题市场上线~
java·开发语言·小程序·php
yangminlei3 小时前
openclaw对接飞书
开发语言·python·飞书