PHP 中的设计模式:工厂模式与单例

1. 引言

在 PHP 项目里,随着业务代码不断膨胀,对象的创建与共享往往会变成一团乱麻:new 散落在各处、数据库连接被反复实例化、日志对象到处传递。设计模式正是解决这类问题的成熟经验总结。

本文聚焦 PHP 中最常用、也最容易被误用的两类创建型模式------工厂模式 与单例模式,并给出一个可落地的日志记录器案例。

2. 为什么需要工厂模式

直接 new 一个对象看似简单,但会带来几个问题:

  • 耦合:调用方必须知道具体类名与构造参数。
  • 难以替换 :想从 FileLogger 换成 RedisLogger,需要改动所有调用点。
  • 构造复杂:对象初始化需要多步配置时,重复代码会迅速蔓延。

工厂模式的核心思想是:把「创建对象」这件事从业务逻辑中抽离出来,由专门的工厂负责。

用一张图概括「直接 new」与「工厂创建」的差别:

flowchart LR subgraph A["直接 new:调用方承担创建细节"] A1["业务代码"] --> A2["new FileLogger(path)"] A1 --> A3["new RedisLogger(host, port)"] end subgraph B["工厂创建:创建逻辑集中"] B1["业务代码"] --> B2["LoggerFactory::create(type)"] B2 --> B3["FileLogger"] B2 --> B4["RedisLogger"] end

3. 简单工厂

简单工厂(Simple Factory)并不是 GoF 23 种模式之一,但它是理解工厂家族的最佳起点。

php 复制代码
<?php

interface Logger
{
    public function log(string $message): void;
}

class FileLogger implements Logger
{
    public function __construct(private string $path) {}

    public function log(string $message): void
    {
        file_put_contents($this->path, $message . PHP_EOL, FILE_APPEND);
    }
}

class EchoLogger implements Logger
{
    public function log(string $message): void
    {
        echo $message . PHP_EOL;
    }
}

class LoggerFactory
{
    public static function create(string $type): Logger
    {
        return match ($type) {
            'file' => new FileLogger(__DIR__ . '/app.log'),
            'echo' => new EchoLogger(),
            default => throw new InvalidArgumentException("未知的日志类型: {$type}"),
        };
    }
}

$logger = LoggerFactory::create('file');
$logger->log('用户登录成功');

优点:调用方只需关心类型字符串,创建逻辑集中在一处。

缺点 :新增类型必须修改 LoggerFactory,违反开闭原则;工厂类会随着类型增多而膨胀。

4. 工厂方法

工厂方法(Factory Method)把「创建哪个具体产品」的决定权下放给子类,从而让新增产品无需修改已有工厂。

php 复制代码
<?php

abstract class LoggerFactory
{
    abstract protected function createLogger(): Logger;

    public function log(string $message): void
    {
        $this->createLogger()->log($message);
    }
}

class FileLoggerFactory extends LoggerFactory
{
    protected function createLogger(): Logger
    {
        return new FileLogger(__DIR__ . '/app.log');
    }
}

class EchoLoggerFactory extends LoggerFactory
{
    protected function createLogger(): Logger
    {
        return new EchoLogger();
    }
}

$factory = new FileLoggerFactory();
$factory->log('订单创建成功');

优点:新增日志类型只需新增一个工厂子类,符合开闭原则。

缺点:类的数量成倍增长,每增加一个产品就要增加一个工厂。

5. 抽象工厂模式

当产品不止一个维度时(例如「日志记录器 + 日志格式化器」),抽象工厂(Abstract Factory)就派上用场了。它用于创建一系列相关或相互依赖的对象。

php 复制代码
<?php

interface Formatter
{
    public function format(string $message): string;
}

class JsonFormatter implements Formatter
{
    public function format(string $message): string
    {
        return json_encode(['msg' => $message, 'time' => date('c')], JSON_UNESCAPED_UNICODE);
    }
}

class PlainFormatter implements Formatter
{
    public function format(string $message): string
    {
        return '[' . date('Y-m-d H:i:s') . '] ' . $message;
    }
}

interface LoggerAbstractFactory
{
    public function createLogger(): Logger;
    public function createFormatter(): Formatter;
}

class FileLoggerAbstractFactory implements LoggerAbstractFactory
{
    public function createLogger(): Logger
    {
        return new FileLogger(__DIR__ . '/app.log');
    }

    public function createFormatter(): Formatter
    {
        return new PlainFormatter();
    }
}

class EchoLoggerAbstractFactory implements LoggerAbstractFactory
{
    public function createLogger(): Logger
    {
        return new EchoLogger();
    }

    public function createFormatter(): Formatter
    {
        return new JsonFormatter();
    }
}

优点:保证同一产品族内的对象彼此兼容,切换产品族只需替换工厂。

缺点:扩展新的产品维度(比如再加一个「日志输出目标」)需要修改所有工厂接口,代价较高。

抽象工厂的产品族关系可以这样理解:

classDiagram class LoggerAbstractFactory { <<interface>> +createLogger() Logger +createFormatter() Formatter } class FileLoggerAbstractFactory { +createLogger() Logger +createFormatter() Formatter } class EchoLoggerAbstractFactory { +createLogger() Logger +createFormatter() Formatter } class Logger { <<interface>> +log(message) void } class Formatter { <<interface>> +format(message) string } LoggerAbstractFactory <|.. FileLoggerAbstractFactory LoggerAbstractFactory <|.. EchoLoggerAbstractFactory FileLoggerAbstractFactory ..> Logger : 创建 FileLoggerAbstractFactory ..> Formatter : 创建 EchoLoggerAbstractFactory ..> Logger : 创建 EchoLoggerAbstractFactory ..> Formatter : 创建

6. 单例模式实现与弊端

单例模式(Singleton)保证一个类只有一个实例,并提供全局访问点。

php 复制代码
<?php

class Config
{
    private static ?Config $instance = null;

    private array $data = [];

    private function __construct() {}

    private function __clone() {}

    public function __wakeup()
    {
        throw new RuntimeException('禁止反序列化单例');
    }

    public static function getInstance(): self
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function set(string $key, mixed $value): void
    {
        $this->data[$key] = $value;
    }

    public function get(string $key, mixed $default = null): mixed
    {
        return $this->data[$key] ?? $default;
    }
}

Config::getInstance()->set('app_name', 'demo');
echo Config::getInstance()->get('app_name');

实现要点:

  • 构造函数私有化,禁止外部 new。
  • 私有化 __clone,防止克隆。
  • 禁止反序列化,避免绕过单例约束。
  • 静态属性持有唯一实例。

弊端:

  1. 隐藏依赖 :类内部直接调用 Config::getInstance(),依赖关系不可见,测试时难以替换。
  2. 全局状态:任何地方都能修改单例,容易产生难以追踪的副作用。
  3. 并发与生命周期问题:在常驻内存的运行时(如 Swoole、RoadRunner)中,单例会跨请求存活,可能造成数据污染。
  4. 违反单一职责:类既要负责业务,又要负责自身的实例管理。

单例的获取流程与常见破坏入口如下:

flowchart TD A[&#34;调用 Config::getInstance()&#34;] --> B{&#34;$instance 是否为 null&#34;} B -- &#34;是&#34; --> C[&#34;new self() 并赋值给静态属性&#34;] B -- &#34;否&#34; --> D[&#34;直接返回已有实例&#34;] C --> E[&#34;返回唯一实例&#34;] D --> E F[&#34;外部 new&#34;] -.->|&#34;构造函数私有,被拦截&#34;| X[&#34;失败&#34;] G[&#34;clone 对象&#34;] -.->|&#34;__clone 私有,被拦截&#34;| X H[&#34;unserialize 反序列化&#34;] -.->|&#34;__wakeup 抛异常&#34;| X

7. 依赖注入替代单例

依赖注入(Dependency Injection)把「获取依赖」变成「接收依赖」,让依赖关系显式化,同时保留「只创建一次」的能力------由容器负责生命周期管理。

php 复制代码
<?php

class Config
{
    public function __construct(private array $data = []) {}

    public function get(string $key, mixed $default = null): mixed
    {
        return $this->data[$key] ?? $default;
    }
}

class UserService
{
    public function __construct(private Config $config) {}

    public function register(string $name): string
    {
        $appName = $this->config->get('app_name', 'app');
        return "{$appName}: 用户 {$name} 注册成功";
    }
}

// 简易容器:同一个 key 只创建一次,等价于「受控的单例」
class Container
{
    private array $instances = [];
    private array $bindings = [];

    public function bind(string $id, callable $resolver): void
    {
        $this->bindings[$id] = $resolver;
    }

    public function get(string $id): mixed
    {
        if (isset($this->instances[$id])) {
            return $this->instances[$id];
        }
        if (!isset($this->bindings[$id])) {
            throw new RuntimeException("未注册的服务: {$id}");
        }
        return $this->instances[$id] = ($this->bindings[$id])($this);
    }
}

$container = new Container();
$container->bind(Config::class, fn () => new Config(['app_name' => 'demo']));
$container->bind(UserService::class, fn (Container $c) => new UserService($c->get(Config::class)));

echo $container->get(UserService::class)->register('张三');

对比单例,依赖注入的优势在于:

  • 依赖显式声明在构造函数中,阅读代码即可知道类需要什么。
  • 单元测试可以直接传入 mock 对象,无需反射修改静态属性。
  • 生命周期由容器统一管理,常驻内存场景下可按请求重建。

8. 实际案例:日志记录器

把前面的内容串起来,做一个支持多驱动、可注入、可测试的日志记录器。

php 复制代码
<?php

interface Logger
{
    public function log(string $level, string $message): void;
}

class FileLogger implements Logger
{
    public function __construct(private string $path) {}

    public function log(string $level, string $message): void
    {
        $line = sprintf('[%s] %s: %s%s', date('Y-m-d H:i:s'), strtoupper($level), $message, PHP_EOL);
        file_put_contents($this->path, $line, FILE_APPEND);
    }
}

class NullLogger implements Logger
{
    public function log(string $level, string $message): void
    {
        // 测试环境静默
    }
}

class LoggerFactory
{
    public static function create(string $driver, array $options = []): Logger
    {
        return match ($driver) {
            'file' => new FileLogger($options['path'] ?? __DIR__ . '/app.log'),
            'null' => new NullLogger(),
            default => throw new InvalidArgumentException("不支持的日志驱动: {$driver}"),
        };
    }
}

class OrderService
{
    public function __construct(private Logger $logger) {}

    public function create(int $orderId): void
    {
        // 业务逻辑
        $this->logger->log('info', "订单 {$orderId} 创建成功");
    }
}

// 组装:工厂负责创建,容器负责共享
$container = new Container();
$container->bind(Logger::class, fn () => LoggerFactory::create('file', ['path' => __DIR__ . '/order.log']));
$container->bind(OrderService::class, fn (Container $c) => new OrderService($c->get(Logger::class)));

$container->get(OrderService::class)->create(1001);

这个案例体现了三者的分工:

  • 工厂 :屏蔽 FileLogger 的构造细节,调用方只传驱动名。
  • 容器(受控单例):保证整个请求内只创建一个日志实例,避免重复打开文件句柄。
  • 依赖注入 :OrderService 不关心日志从哪来,测试时注入 NullLogger 即可。

整个案例的组装与调用链路如下:

sequenceDiagram participant Client as &#34;调用方&#34; participant C as &#34;Container&#34; participant F as &#34;LoggerFactory&#34; participant L as &#34;FileLogger&#34; participant S as &#34;OrderService&#34; Client->>C: get(OrderService::class) C->>C: 检查 instances 缓存 C->>S: new OrderService(logger) C->>F: create('file', options) F->>L: new FileLogger(path) F-->>C: 返回 Logger 实例 C-->>Client: 返回 OrderService Client->>S: create(1001) S->>L: log('info', '订单 1001 创建成功')

9. 总结

模式 解决的问题 适用场景 主要代价
简单工厂 集中创建逻辑 类型少、变化不频繁 违反开闭原则
工厂方法 新增产品不改旧代码 产品类型会持续扩展 类数量膨胀
抽象工厂 创建产品族 多维度、需保证兼容 扩展维度成本高
单例 全局唯一实例 无状态工具、配置读取 隐藏依赖、难测试

实践建议:

  1. 优先使用依赖注入,把单例降级为容器中的「共享实例」。
  2. 产品类型少时用简单工厂,类型会持续增长时再演进到工厂方法。
  3. 只有在确实需要全局唯一、且无状态时,才考虑单例,并做好防克隆、防反序列化。

最后用一张决策图收束全文的选型思路:

flowchart TD A[&#34;需要创建对象&#34;] --> B{&#34;是否需要全局唯一实例&#34;} B -- &#34;否&#34; --> C{&#34;产品类型是否会持续扩展&#34;} C -- &#34;否&#34; --> D[&#34;简单工厂&#34;] C -- &#34;是&#34; --> E{&#34;是否涉及多个产品维度&#34;} E -- &#34;否&#34; --> F[&#34;工厂方法&#34;] E -- &#34;是&#34; --> G[&#34;抽象工厂&#34;] B -- &#34;是&#34; --> H{&#34;是否无状态且确实需要全局访问&#34;} H -- &#34;是&#34; --> I[&#34;单例(做好防克隆/防反序列化)&#34;] H -- &#34;否&#34; --> J[&#34;依赖注入 + 容器共享实例&#34;]
相关推荐
vipxieliang1 小时前
ThinkPHP 8.0 注解路由实战:PHP 8 原生 Attribute 方案
php·thinkphp
evoxwht2 小时前
给 CRMEB v8 商品中台加一个「课程」商品类型
php
evoxwht2 小时前
给 CRMEB V8 给 MCP 服务加一个分销工具
php
IT召唤狮14 小时前
开源企业级零信任项目Pangolin,零信任能挡住PHP核弹漏洞吗?
开发语言·开源·php
大鹏说大话20 小时前
PHP 大文件上传解决方案:分片上传完整思路
开发语言·php
niucloud-admin21 小时前
NIUCLOUD PHP SAAS上门家政使用文档--服务订单:订单回访
php
大黄说说1 天前
Composer 常见报错汇总:镜像、依赖冲突、环境坑一站式排查
php·composer
安易算力1 天前
智算中心网络架构:RDMA/IB/以太网选型深度解析与优化实践
网络·架构·php
程序员大雄学编程1 天前
Linux 磁盘满了怎么排查?df / du / find 三步定位大文件
linux·运维·php