云计算百科
云计算领域专业知识百科平台

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

1. 引言

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

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

2. 为什么需要工厂模式

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

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

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

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

#mermaid-svg-TMeRvlX05jM0q7BS{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TMeRvlX05jM0q7BS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TMeRvlX05jM0q7BS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TMeRvlX05jM0q7BS .error-icon{fill:#552222;}#mermaid-svg-TMeRvlX05jM0q7BS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TMeRvlX05jM0q7BS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TMeRvlX05jM0q7BS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TMeRvlX05jM0q7BS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TMeRvlX05jM0q7BS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TMeRvlX05jM0q7BS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TMeRvlX05jM0q7BS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TMeRvlX05jM0q7BS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TMeRvlX05jM0q7BS .marker.cross{stroke:#333333;}#mermaid-svg-TMeRvlX05jM0q7BS svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TMeRvlX05jM0q7BS p{margin:0;}#mermaid-svg-TMeRvlX05jM0q7BS .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-TMeRvlX05jM0q7BS .cluster-label text{fill:#333;}#mermaid-svg-TMeRvlX05jM0q7BS .cluster-label span{color:#333;}#mermaid-svg-TMeRvlX05jM0q7BS .cluster-label span p{background-color:transparent;}#mermaid-svg-TMeRvlX05jM0q7BS .label text,#mermaid-svg-TMeRvlX05jM0q7BS span{fill:#333;color:#333;}#mermaid-svg-TMeRvlX05jM0q7BS .node rect,#mermaid-svg-TMeRvlX05jM0q7BS .node circle,#mermaid-svg-TMeRvlX05jM0q7BS .node ellipse,#mermaid-svg-TMeRvlX05jM0q7BS .node polygon,#mermaid-svg-TMeRvlX05jM0q7BS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TMeRvlX05jM0q7BS .rough-node .label text,#mermaid-svg-TMeRvlX05jM0q7BS .node .label text,#mermaid-svg-TMeRvlX05jM0q7BS .image-shape .label,#mermaid-svg-TMeRvlX05jM0q7BS .icon-shape .label{text-anchor:middle;}#mermaid-svg-TMeRvlX05jM0q7BS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TMeRvlX05jM0q7BS .rough-node .label,#mermaid-svg-TMeRvlX05jM0q7BS .node .label,#mermaid-svg-TMeRvlX05jM0q7BS .image-shape .label,#mermaid-svg-TMeRvlX05jM0q7BS .icon-shape .label{text-align:center;}#mermaid-svg-TMeRvlX05jM0q7BS .node.clickable{cursor:pointer;}#mermaid-svg-TMeRvlX05jM0q7BS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TMeRvlX05jM0q7BS .arrowheadPath{fill:#333333;}#mermaid-svg-TMeRvlX05jM0q7BS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TMeRvlX05jM0q7BS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TMeRvlX05jM0q7BS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TMeRvlX05jM0q7BS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TMeRvlX05jM0q7BS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TMeRvlX05jM0q7BS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TMeRvlX05jM0q7BS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TMeRvlX05jM0q7BS .cluster text{fill:#333;}#mermaid-svg-TMeRvlX05jM0q7BS .cluster span{color:#333;}#mermaid-svg-TMeRvlX05jM0q7BS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TMeRvlX05jM0q7BS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TMeRvlX05jM0q7BS rect.text{fill:none;stroke-width:0;}#mermaid-svg-TMeRvlX05jM0q7BS .icon-shape,#mermaid-svg-TMeRvlX05jM0q7BS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TMeRvlX05jM0q7BS .icon-shape p,#mermaid-svg-TMeRvlX05jM0q7BS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TMeRvlX05jM0q7BS .icon-shape .label rect,#mermaid-svg-TMeRvlX05jM0q7BS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TMeRvlX05jM0q7BS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TMeRvlX05jM0q7BS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TMeRvlX05jM0q7BS :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

工厂创建:创建逻辑集中

业务代码

LoggerFactory::create(type)

FileLogger

RedisLogger

直接 new:调用方承担创建细节

业务代码

new FileLogger(path)

new RedisLogger(host, port)

3. 简单工厂

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

<?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

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

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();
}
}

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

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

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

#mermaid-svg-jUlVjmXrCGhCRQvf{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jUlVjmXrCGhCRQvf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jUlVjmXrCGhCRQvf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jUlVjmXrCGhCRQvf .error-icon{fill:#552222;}#mermaid-svg-jUlVjmXrCGhCRQvf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jUlVjmXrCGhCRQvf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jUlVjmXrCGhCRQvf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jUlVjmXrCGhCRQvf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jUlVjmXrCGhCRQvf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jUlVjmXrCGhCRQvf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jUlVjmXrCGhCRQvf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jUlVjmXrCGhCRQvf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jUlVjmXrCGhCRQvf .marker.cross{stroke:#333333;}#mermaid-svg-jUlVjmXrCGhCRQvf svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jUlVjmXrCGhCRQvf p{margin:0;}#mermaid-svg-jUlVjmXrCGhCRQvf g.classGroup text{fill:#9370DB;stroke:none;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-jUlVjmXrCGhCRQvf g.classGroup text .title{font-weight:bolder;}#mermaid-svg-jUlVjmXrCGhCRQvf .cluster-label text{fill:#333;}#mermaid-svg-jUlVjmXrCGhCRQvf .cluster-label span{color:#333;}#mermaid-svg-jUlVjmXrCGhCRQvf .cluster-label span p{background-color:transparent;}#mermaid-svg-jUlVjmXrCGhCRQvf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jUlVjmXrCGhCRQvf .cluster text{fill:#333;}#mermaid-svg-jUlVjmXrCGhCRQvf .cluster span{color:#333;}#mermaid-svg-jUlVjmXrCGhCRQvf .nodeLabel,#mermaid-svg-jUlVjmXrCGhCRQvf .edgeLabel{color:#131300;}#mermaid-svg-jUlVjmXrCGhCRQvf .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-jUlVjmXrCGhCRQvf .label text{fill:#131300;}#mermaid-svg-jUlVjmXrCGhCRQvf .labelBkg{background:#ECECFF;}#mermaid-svg-jUlVjmXrCGhCRQvf .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-jUlVjmXrCGhCRQvf .classTitle{font-weight:bolder;}#mermaid-svg-jUlVjmXrCGhCRQvf .node rect,#mermaid-svg-jUlVjmXrCGhCRQvf .node circle,#mermaid-svg-jUlVjmXrCGhCRQvf .node ellipse,#mermaid-svg-jUlVjmXrCGhCRQvf .node polygon,#mermaid-svg-jUlVjmXrCGhCRQvf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jUlVjmXrCGhCRQvf .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-jUlVjmXrCGhCRQvf g.clickable{cursor:pointer;}#mermaid-svg-jUlVjmXrCGhCRQvf g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-jUlVjmXrCGhCRQvf g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-jUlVjmXrCGhCRQvf .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-jUlVjmXrCGhCRQvf .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-jUlVjmXrCGhCRQvf .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-jUlVjmXrCGhCRQvf .dashed-line{stroke-dasharray:3;}#mermaid-svg-jUlVjmXrCGhCRQvf .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-jUlVjmXrCGhCRQvf #compositionStart,#mermaid-svg-jUlVjmXrCGhCRQvf .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-jUlVjmXrCGhCRQvf #compositionEnd,#mermaid-svg-jUlVjmXrCGhCRQvf .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-jUlVjmXrCGhCRQvf #dependencyStart,#mermaid-svg-jUlVjmXrCGhCRQvf .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-jUlVjmXrCGhCRQvf #dependencyStart,#mermaid-svg-jUlVjmXrCGhCRQvf .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-jUlVjmXrCGhCRQvf #extensionStart,#mermaid-svg-jUlVjmXrCGhCRQvf .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-jUlVjmXrCGhCRQvf #extensionEnd,#mermaid-svg-jUlVjmXrCGhCRQvf .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-jUlVjmXrCGhCRQvf #aggregationStart,#mermaid-svg-jUlVjmXrCGhCRQvf .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-jUlVjmXrCGhCRQvf #aggregationEnd,#mermaid-svg-jUlVjmXrCGhCRQvf .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-jUlVjmXrCGhCRQvf #lollipopStart,#mermaid-svg-jUlVjmXrCGhCRQvf .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-jUlVjmXrCGhCRQvf #lollipopEnd,#mermaid-svg-jUlVjmXrCGhCRQvf .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-jUlVjmXrCGhCRQvf .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-jUlVjmXrCGhCRQvf .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jUlVjmXrCGhCRQvf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jUlVjmXrCGhCRQvf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jUlVjmXrCGhCRQvf :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

创建

创建

创建

创建

«interface»

LoggerAbstractFactory

+createLogger() : Logger

+createFormatter() : Formatter

FileLoggerAbstractFactory

+createLogger() : Logger

+createFormatter() : Formatter

EchoLoggerAbstractFactory

+createLogger() : Logger

+createFormatter() : Formatter

«interface»

Logger

+log(message) : void

«interface»

Formatter

+format(message) : string

6. 单例模式实现与弊端

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

<?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,防止克隆。
  • 禁止反序列化,避免绕过单例约束。
  • 静态属性持有唯一实例。

弊端:

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

    #mermaid-svg-JUD0RnL8NBWqPRrx{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JUD0RnL8NBWqPRrx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JUD0RnL8NBWqPRrx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JUD0RnL8NBWqPRrx .error-icon{fill:#552222;}#mermaid-svg-JUD0RnL8NBWqPRrx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JUD0RnL8NBWqPRrx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JUD0RnL8NBWqPRrx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JUD0RnL8NBWqPRrx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JUD0RnL8NBWqPRrx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JUD0RnL8NBWqPRrx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JUD0RnL8NBWqPRrx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JUD0RnL8NBWqPRrx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JUD0RnL8NBWqPRrx .marker.cross{stroke:#333333;}#mermaid-svg-JUD0RnL8NBWqPRrx svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JUD0RnL8NBWqPRrx p{margin:0;}#mermaid-svg-JUD0RnL8NBWqPRrx .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-JUD0RnL8NBWqPRrx .cluster-label text{fill:#333;}#mermaid-svg-JUD0RnL8NBWqPRrx .cluster-label span{color:#333;}#mermaid-svg-JUD0RnL8NBWqPRrx .cluster-label span p{background-color:transparent;}#mermaid-svg-JUD0RnL8NBWqPRrx .label text,#mermaid-svg-JUD0RnL8NBWqPRrx span{fill:#333;color:#333;}#mermaid-svg-JUD0RnL8NBWqPRrx .node rect,#mermaid-svg-JUD0RnL8NBWqPRrx .node circle,#mermaid-svg-JUD0RnL8NBWqPRrx .node ellipse,#mermaid-svg-JUD0RnL8NBWqPRrx .node polygon,#mermaid-svg-JUD0RnL8NBWqPRrx .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JUD0RnL8NBWqPRrx .rough-node .label text,#mermaid-svg-JUD0RnL8NBWqPRrx .node .label text,#mermaid-svg-JUD0RnL8NBWqPRrx .image-shape .label,#mermaid-svg-JUD0RnL8NBWqPRrx .icon-shape .label{text-anchor:middle;}#mermaid-svg-JUD0RnL8NBWqPRrx .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JUD0RnL8NBWqPRrx .rough-node .label,#mermaid-svg-JUD0RnL8NBWqPRrx .node .label,#mermaid-svg-JUD0RnL8NBWqPRrx .image-shape .label,#mermaid-svg-JUD0RnL8NBWqPRrx .icon-shape .label{text-align:center;}#mermaid-svg-JUD0RnL8NBWqPRrx .node.clickable{cursor:pointer;}#mermaid-svg-JUD0RnL8NBWqPRrx .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JUD0RnL8NBWqPRrx .arrowheadPath{fill:#333333;}#mermaid-svg-JUD0RnL8NBWqPRrx .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JUD0RnL8NBWqPRrx .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JUD0RnL8NBWqPRrx .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JUD0RnL8NBWqPRrx .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JUD0RnL8NBWqPRrx .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JUD0RnL8NBWqPRrx .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JUD0RnL8NBWqPRrx .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JUD0RnL8NBWqPRrx .cluster text{fill:#333;}#mermaid-svg-JUD0RnL8NBWqPRrx .cluster span{color:#333;}#mermaid-svg-JUD0RnL8NBWqPRrx div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JUD0RnL8NBWqPRrx .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JUD0RnL8NBWqPRrx rect.text{fill:none;stroke-width:0;}#mermaid-svg-JUD0RnL8NBWqPRrx .icon-shape,#mermaid-svg-JUD0RnL8NBWqPRrx .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JUD0RnL8NBWqPRrx .icon-shape p,#mermaid-svg-JUD0RnL8NBWqPRrx .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JUD0RnL8NBWqPRrx .icon-shape .label rect,#mermaid-svg-JUD0RnL8NBWqPRrx .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JUD0RnL8NBWqPRrx .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JUD0RnL8NBWqPRrx .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JUD0RnL8NBWqPRrx :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    是

    否

    构造函数私有,被拦截

    __clone 私有,被拦截

    __wakeup 抛异常

    调用 Config::getInstance()

    $instance 是否为 null

    new self() 并赋值给静态属性

    直接返回已有实例

    返回唯一实例

    外部 new

    失败

    clone 对象

    unserialize 反序列化

    7. 依赖注入替代单例

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

    <?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

    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 即可。

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

    "OrderService""FileLogger""LoggerFactory""Container""调用方""OrderService""FileLogger""LoggerFactory""Container""调用方"#mermaid-svg-h0uEhHi1cmtSqUpF{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-h0uEhHi1cmtSqUpF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-h0uEhHi1cmtSqUpF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-h0uEhHi1cmtSqUpF .error-icon{fill:#552222;}#mermaid-svg-h0uEhHi1cmtSqUpF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-h0uEhHi1cmtSqUpF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-h0uEhHi1cmtSqUpF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-h0uEhHi1cmtSqUpF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-h0uEhHi1cmtSqUpF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-h0uEhHi1cmtSqUpF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-h0uEhHi1cmtSqUpF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-h0uEhHi1cmtSqUpF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-h0uEhHi1cmtSqUpF .marker.cross{stroke:#333333;}#mermaid-svg-h0uEhHi1cmtSqUpF svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-h0uEhHi1cmtSqUpF p{margin:0;}#mermaid-svg-h0uEhHi1cmtSqUpF .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h0uEhHi1cmtSqUpF text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-h0uEhHi1cmtSqUpF .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-h0uEhHi1cmtSqUpF .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-h0uEhHi1cmtSqUpF .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-h0uEhHi1cmtSqUpF .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-h0uEhHi1cmtSqUpF #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-h0uEhHi1cmtSqUpF .sequenceNumber{fill:white;}#mermaid-svg-h0uEhHi1cmtSqUpF #sequencenumber{fill:#333;}#mermaid-svg-h0uEhHi1cmtSqUpF #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-h0uEhHi1cmtSqUpF .messageText{fill:#333;stroke:none;}#mermaid-svg-h0uEhHi1cmtSqUpF .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h0uEhHi1cmtSqUpF .labelText,#mermaid-svg-h0uEhHi1cmtSqUpF .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-h0uEhHi1cmtSqUpF .loopText,#mermaid-svg-h0uEhHi1cmtSqUpF .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-h0uEhHi1cmtSqUpF .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-h0uEhHi1cmtSqUpF .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-h0uEhHi1cmtSqUpF .noteText,#mermaid-svg-h0uEhHi1cmtSqUpF .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-h0uEhHi1cmtSqUpF .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h0uEhHi1cmtSqUpF .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h0uEhHi1cmtSqUpF .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-h0uEhHi1cmtSqUpF .actorPopupMenu{position:absolute;}#mermaid-svg-h0uEhHi1cmtSqUpF .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-h0uEhHi1cmtSqUpF .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-h0uEhHi1cmtSqUpF .actor-man circle,#mermaid-svg-h0uEhHi1cmtSqUpF line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-h0uEhHi1cmtSqUpF :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}get(OrderService::class)检查 instances 缓存new OrderService(logger)create('file', options)new FileLogger(path)返回 Logger 实例返回 OrderServicecreate(1001)log('info', '订单 1001 创建成功')

    9. 总结

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

    实践建议:

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

    #mermaid-svg-sJJzT1gW1jjQjiIJ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sJJzT1gW1jjQjiIJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sJJzT1gW1jjQjiIJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sJJzT1gW1jjQjiIJ .error-icon{fill:#552222;}#mermaid-svg-sJJzT1gW1jjQjiIJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sJJzT1gW1jjQjiIJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sJJzT1gW1jjQjiIJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sJJzT1gW1jjQjiIJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sJJzT1gW1jjQjiIJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sJJzT1gW1jjQjiIJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sJJzT1gW1jjQjiIJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sJJzT1gW1jjQjiIJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sJJzT1gW1jjQjiIJ .marker.cross{stroke:#333333;}#mermaid-svg-sJJzT1gW1jjQjiIJ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sJJzT1gW1jjQjiIJ p{margin:0;}#mermaid-svg-sJJzT1gW1jjQjiIJ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-sJJzT1gW1jjQjiIJ .cluster-label text{fill:#333;}#mermaid-svg-sJJzT1gW1jjQjiIJ .cluster-label span{color:#333;}#mermaid-svg-sJJzT1gW1jjQjiIJ .cluster-label span p{background-color:transparent;}#mermaid-svg-sJJzT1gW1jjQjiIJ .label text,#mermaid-svg-sJJzT1gW1jjQjiIJ span{fill:#333;color:#333;}#mermaid-svg-sJJzT1gW1jjQjiIJ .node rect,#mermaid-svg-sJJzT1gW1jjQjiIJ .node circle,#mermaid-svg-sJJzT1gW1jjQjiIJ .node ellipse,#mermaid-svg-sJJzT1gW1jjQjiIJ .node polygon,#mermaid-svg-sJJzT1gW1jjQjiIJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sJJzT1gW1jjQjiIJ .rough-node .label text,#mermaid-svg-sJJzT1gW1jjQjiIJ .node .label text,#mermaid-svg-sJJzT1gW1jjQjiIJ .image-shape .label,#mermaid-svg-sJJzT1gW1jjQjiIJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-sJJzT1gW1jjQjiIJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sJJzT1gW1jjQjiIJ .rough-node .label,#mermaid-svg-sJJzT1gW1jjQjiIJ .node .label,#mermaid-svg-sJJzT1gW1jjQjiIJ .image-shape .label,#mermaid-svg-sJJzT1gW1jjQjiIJ .icon-shape .label{text-align:center;}#mermaid-svg-sJJzT1gW1jjQjiIJ .node.clickable{cursor:pointer;}#mermaid-svg-sJJzT1gW1jjQjiIJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sJJzT1gW1jjQjiIJ .arrowheadPath{fill:#333333;}#mermaid-svg-sJJzT1gW1jjQjiIJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sJJzT1gW1jjQjiIJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sJJzT1gW1jjQjiIJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sJJzT1gW1jjQjiIJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sJJzT1gW1jjQjiIJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sJJzT1gW1jjQjiIJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sJJzT1gW1jjQjiIJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sJJzT1gW1jjQjiIJ .cluster text{fill:#333;}#mermaid-svg-sJJzT1gW1jjQjiIJ .cluster span{color:#333;}#mermaid-svg-sJJzT1gW1jjQjiIJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sJJzT1gW1jjQjiIJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sJJzT1gW1jjQjiIJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-sJJzT1gW1jjQjiIJ .icon-shape,#mermaid-svg-sJJzT1gW1jjQjiIJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sJJzT1gW1jjQjiIJ .icon-shape p,#mermaid-svg-sJJzT1gW1jjQjiIJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sJJzT1gW1jjQjiIJ .icon-shape .label rect,#mermaid-svg-sJJzT1gW1jjQjiIJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sJJzT1gW1jjQjiIJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sJJzT1gW1jjQjiIJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sJJzT1gW1jjQjiIJ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    否

    否

    是

    否

    是

    是

    是

    否

    需要创建对象

    是否需要全局唯一实例

    产品类型是否会持续扩展

    简单工厂

    是否涉及多个产品维度

    工厂方法

    抽象工厂

    是否无状态且确实需要全局访问

    单例(做好防克隆/防反序列化)

    依赖注入 + 容器共享实例

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » PHP 中的设计模式:工厂模式与单例
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!