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();
关键点: 建造者模式的核心是「链式调用」和「分步构建」。每个 setter 方法返回 $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']]);
注意: 我曾经踩过浅拷贝的坑。PHP 默认的 clone 是浅拷贝,如果对象里有引用类型的属性(比如数组、对象),克隆后两个对象会共享同一份数据。一定要在 __clone() 里做深拷贝。

3.5 四种模式对比与选型建议

这四种模式各有各的适用场景。我整理了一个表格,方便你快速决策:

模式 核心思想 适用场景 企业项目典型应用
单例模式 全局唯一实例 资源管理类(DB、Redis、日志) 数据库连接池、配置管理器
工厂模式 封装创建逻辑 需要根据条件创建不同子类 支付网关、消息推送渠道
建造者模式 分步构建复杂对象 对象参数多、构建步骤复杂 查询条件构建、报表生成
原型模式 通过克隆创建对象 创建成本高、需要大量相似对象 模板复制、缓存对象
我的建议: 不要为了用设计模式而用设计模式。如果你的项目只有两三个类,直接 new 反而更清晰。当代码开始出现「重复的创建逻辑」或者「构造函数参数超过5个」时,再考虑引入这些模式。

嗯,创建型设计模式就聊到这里。说白了,它们都是在帮你回答一个问题:「这个对象,到底该怎么生出来?」 选对了模式,代码会变得好维护、好扩展。选错了,反而增加复杂度。多在实践中体会,慢慢就有感觉了。