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,防止克隆。
- 禁止反序列化,避免绕过单例约束。
- 静态属性持有唯一实例。
弊端:
单例的获取流程与常见破坏入口如下:
#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;}
否
否
是
否
是
是
是
否
需要创建对象
是否需要全局唯一实例
产品类型是否会持续扩展
简单工厂
是否涉及多个产品维度
工厂方法
抽象工厂
是否无状态且确实需要全局访问
单例(做好防克隆/防反序列化)
依赖注入 + 容器共享实例
网硕互联帮助中心






评论前必须登录!
注册