3. 设计模式(创建型):单例模式、工厂模式、建造者模式、原型模式在企业项目中的应用
创建型设计模式,说白了就是管「对象怎么生出来」的。很多新手写代码,直接 new 一把梭,项目小的时候还行,一旦上了规模,你就发现到处都是 new,耦合得死死的,改一个地方牵一发动全身。
我个人习惯,在项目初期就把创建对象的逻辑收拢起来。今天聊的这四种模式,是我在企业项目里用得最多的。嗯,咱们一个一个来看。
3.1 单例模式:一个类只能有一个实例
单例模式,顾名思义,就是保证一个类在整个进程生命周期里,只有一个实例。你想想看,数据库连接、配置管理器、日志记录器,这些东西有必要搞多个吗?
我在项目中遇到过最典型的场景:一个高并发订单系统,每个请求都去 new 一个 Redis 连接对象。结果呢?连接数直接爆了,数据库被打挂。后来改成单例,一个进程只维护一个连接池,问题就解决了。
class DatabaseConnection
{
private static ?self $instance = null;
private PDO $pdo;
private function __construct(array $config)
{
// 私有构造函数,外部不能 new
$this->pdo = new PDO(
$config['dsn'],
$config['user'],
$config['pass']
);
}
public static function getInstance(array $config = []): self
{
if (self::$instance === null) {
self::$instance = new self($config);
}
return self::$instance;
}
// 防止克隆
private function __clone() {}
// 防止反序列化
public function __wakeup()
{
throw new \Exception("Cannot unserialize singleton");
}
}
// 使用
$db = DatabaseConnection::getInstance($config);
3.2 工厂模式:把创建对象的逻辑封装起来
工厂模式,说白了就是「你要什么,我给你造什么」。你不需要知道对象是怎么 new 出来的,只需要告诉工厂你要什么类型就行。
我记得有一次重构一个支付模块,原来代码里到处都是 if ($type == 'alipay') { new Alipay(); } 这种判断。后来我引入工厂模式,所有支付对象的创建都交给工厂,新增支付方式只需要加一个类和一个注册,不用改业务代码。
interface PaymentInterface
{
public function pay(float $amount): bool;
}
class Alipay implements PaymentInterface
{
public function pay(float $amount): bool
{
// 支付宝支付逻辑
return true;
}
}
class WechatPay implements PaymentInterface
{
public function pay(float $amount): bool
{
// 微信支付逻辑
return true;
}
}
class PaymentFactory
{
private static array $map = [
'alipay' => Alipay::class,
'wechat' => WechatPay::class,
];
public static function create(string $type): PaymentInterface
{
if (!isset(self::$map[$type])) {
throw new \InvalidArgumentException("不支持的支付类型: {$type}");
}
return new self::$map[$type]();
}
}
// 使用
$payment = PaymentFactory::create('alipay');
$payment->pay(99.9);
3.3 建造者模式:分步构建复杂对象
建造者模式,适合那种「参数特别多、构建步骤特别复杂」的对象。你想想看,一个订单对象可能有几十个字段,如果全塞到构造函数里,调用方根本记不住顺序。
我在项目中遇到过最典型的场景:报表查询条件构建。用户可能选日期范围、筛选状态、排序方式、分页参数……如果全用构造函数,那代码简直没法看。用建造者模式,一步步构建,清晰多了。
class ReportQuery
{
private string $startDate;
private string $endDate;
private ?string $status = null;
private string $orderBy = 'created_at';
private int $page = 1;
private int $pageSize = 20;
// 私有构造函数,只能通过 Builder 创建
private function __construct() {}
public static function builder(): ReportQueryBuilder
{
return new ReportQueryBuilder();
}
// getter 方法...
}
class ReportQueryBuilder
{
private ReportQuery $query;
public function __construct()
{
$this->query = new ReportQuery();
}
public function setDateRange(string $start, string $end): self
{
// 这里可以做日期校验
$this->query->startDate = $start;
$this->query->endDate = $end;
return $this;
}
public function setStatus(?string $status): self
{
$this->query->status = $status;
return $this;
}
public function setPage(int $page, int $pageSize = 20): self
{
$this->query->page = $page;
$this->query->pageSize = $pageSize;
return $this;
}
public function build(): ReportQuery
{
// 可以在这里做最终校验
if (empty($this->query->startDate)) {
throw new \InvalidArgumentException("开始日期不能为空");
}
return $this->query;
}
}
// 使用
$query = ReportQuery::builder()
->setDateRange('2024-01-01', '2024-12-31')
->setStatus('completed')
->setPage(1, 50)
->build();
$this,最后调用 build() 产出最终对象。
3.4 原型模式:通过克隆来创建对象
原型模式,说白了就是「复制粘贴」。当创建一个对象的成本很高(比如需要查数据库、发网络请求),而你又需要多个相似对象时,直接克隆一个现有的,比重新创建快得多。
我记得有一次做报表导出功能,需要生成大量结构相同但数据不同的报表模板。如果每次都重新加载模板配置、读取样式定义,性能太差了。后来我用原型模式,先创建一个「原型对象」,然后克隆它,只修改差异部分。
class ReportTemplate
{
private array $headers;
private array $styles;
private array $data;
public function __construct()
{
// 模拟从数据库加载模板配置
$this->headers = ['ID', '名称', '金额', '时间'];
$this->styles = [
'font' => '14px',
'color' => '#333',
'border' => '1px solid #ccc',
];
}
// 实现克隆方法
public function __clone()
{
// 深拷贝:确保引用类型也被复制
$this->headers = array_merge([], $this->headers);
$this->styles = array_merge([], $this->styles);
$this->data = array_merge([], $this->data);
}
public function setData(array $data): void
{
$this->data = $data;
}
public function render(): string
{
// 渲染报表
return json_encode([
'headers' => $this->headers,
'styles' => $this->styles,
'data' => $this->data,
]);
}
}
// 使用
$baseTemplate = new ReportTemplate(); // 只创建一次,成本高
$report1 = clone $baseTemplate;
$report1->setData([['1', '张三', 100, '2024-01-01']]);
$report2 = clone $baseTemplate;
$report2->setData([['2', '李四', 200, '2024-01-02']]);
clone 是浅拷贝,如果对象里有引用类型的属性(比如数组、对象),克隆后两个对象会共享同一份数据。一定要在 __clone() 里做深拷贝。
3.5 四种模式对比与选型建议
这四种模式各有各的适用场景。我整理了一个表格,方便你快速决策:
| 模式 | 核心思想 | 适用场景 | 企业项目典型应用 |
|---|---|---|---|
| 单例模式 | 全局唯一实例 | 资源管理类(DB、Redis、日志) | 数据库连接池、配置管理器 |
| 工厂模式 | 封装创建逻辑 | 需要根据条件创建不同子类 | 支付网关、消息推送渠道 |
| 建造者模式 | 分步构建复杂对象 | 对象参数多、构建步骤复杂 | 查询条件构建、报表生成 |
| 原型模式 | 通过克隆创建对象 | 创建成本高、需要大量相似对象 | 模板复制、缓存对象 |
new 反而更清晰。当代码开始出现「重复的创建逻辑」或者「构造函数参数超过5个」时,再考虑引入这些模式。
嗯,创建型设计模式就聊到这里。说白了,它们都是在帮你回答一个问题:「这个对象,到底该怎么生出来?」 选对了模式,代码会变得好维护、好扩展。选错了,反而增加复杂度。多在实践中体会,慢慢就有感觉了。