写代码不难,写出好代码难。

很多初学者,写代码都是想到哪写到哪,怎么方便怎么来。写小项目的时候还能应付,但项目一大,代码就变得混乱不堪——类与类之间耦合严重,改一个地方要动很多地方,新增功能要改大量代码,维护起来非常痛苦。

这时候,就需要设计模式了。

设计模式(Design Pattern)是前辈们在软件开发中总结出来的最佳实践,是解决特定问题的一套成熟方案。它不是具体的代码,而是一种思想、一种套路、一种解决问题的思维方式。掌握了设计模式,就能写出更优雅、更易维护、更易扩展的代码。

设计模式最初是由GoF(Gang of Four,四人帮)在1994年出版的《设计模式:可复用面向对象软件的基础》一书中提出的,共23种设计模式。这些模式,经过了几十年的验证,至今仍然是面向对象编程的经典。

PHP作为一门面向对象的语言,设计模式同样适用。主流PHP框架(Laravel、Symfony等)都大量使用了设计模式。掌握设计模式,不仅能让你写出更好的代码,也能让你更好地理解框架的源码。

今天分享PHP中最常用的几种设计模式,帮你从"写能跑的代码"进阶到"写优雅的代码"。

设计模式的分类

GoF的23种设计模式,分为三大类:

  1. 创建型模式(Creational):处理对象创建的机制,在不指定具体类的情况下创建对象。包括:工厂方法、抽象工厂、单例、建造者、原型。
  2. 结构型模式(Structural):处理类和对象的组合,形成更大的结构。包括:适配器、桥接、组合、装饰器、外观、享元、代理。
  3. 行为型模式(Behavioral):处理类和对象之间的通信和职责分配。包括:责任链、命令、解释器、迭代器、中介者、备忘录、观察者、状态、策略、模板方法、访问者。

今天重点讲PHP中最常用的几种:单例、工厂、观察者、策略、装饰器、适配器、外观。

1. 单例模式(Singleton)

目的:保证一个类只有一个实例,并提供一个全局访问点。

使用场景:数据库连接、配置管理、日志记录等,只需要一个实例的场景。

class Database
{
    private static $instance = null;
    private $pdo;

    // 构造函数私有,禁止外部new
    private function __construct()
    {
        $this->pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
    }

    // 禁止克隆
    private function __clone() {}

    // 禁止反序列化
    private function __wakeup() {}

    // 全局唯一访问点
    public static function getInstance()
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    public function getConnection()
    {
        return $this->pdo;
    }
}

// 使用
$db = Database::getInstance();
$pdo = $db->getConnection();

要点:构造函数私有、静态变量保存实例、静态方法获取实例、禁止克隆和反序列化。

注意:单例模式虽然好用,但要慎用。它会引入全局状态,增加耦合,不利于测试。只有确实只需要一个实例的时候才用。

2. 工厂模式(Factory)

目的:定义一个创建对象的接口,让子类决定实例化哪个类。

使用场景:需要根据不同条件创建不同对象,且不希望客户端知道具体创建逻辑的场景。

简单工厂

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

class FileLogger implements Logger
{
    public function log($message)
    {
        file_put_contents('app.log', $message . "\n", FILE_APPEND);
    }
}

class DatabaseLogger implements Logger
{
    public function log($message)
    {
        // 写入数据库
    }
}

// 简单工厂
class LoggerFactory
{
    public static function create($type)
    {
        switch ($type) {
            case 'file':
                return new FileLogger();
            case 'database':
                return new DatabaseLogger();
            default:
                throw new InvalidArgumentException("Unknown logger type: $type");
        }
    }
}

// 使用
$logger = LoggerFactory::create('file');
$logger->log('Hello World');

工厂方法

interface LoggerFactory
{
    public function createLogger(): Logger;
}

class FileLoggerFactory implements LoggerFactory
{
    public function createLogger(): Logger
    {
        return new FileLogger();
    }
}

class DatabaseLoggerFactory implements LoggerFactory
{
    public function createLogger(): Logger
    {
        return new DatabaseLogger();
    }
}

// 使用
$factory = new FileLoggerFactory();
$logger = $factory->createLogger();
$logger->log('Hello World');

要点:定义产品接口,具体产品实现接口,工厂类负责创建产品实例。

好处:客户端不需要知道具体创建逻辑,新增产品类型只需要新增工厂类,符合开闭原则。

3. 观察者模式(Observer)

目的:定义对象间的一对多依赖,当一个对象状态改变时,所有依赖它的对象都会收到通知并自动更新。

使用场景:事件监听、消息通知、订阅发布等场景。PHP的SPL扩展提供了SplSubject和SplObserver接口。

// 主题(被观察者)
class User implements SplSubject
{
    private $observers = [];
    private $name;

    public function attach(SplObserver $observer)
    {
        $this->observers[] = $observer;
    }

    public function detach(SplObserver $observer)
    {
        $key = array_search($observer, $this->observers, true);
        if ($key !== false) {
            unset($this->observers[$key]);
        }
    }

    public function notify()
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }

    public function setName($name)
    {
        $this->name = $name;
        $this->notify(); // 状态改变,通知观察者
    }

    public function getName()
    {
        return $this->name;
    }
}

// 观察者
class EmailNotifier implements SplObserver
{
    public function update(SplSubject $subject)
    {
        echo "发送邮件:用户名称已更新为" . $subject->getName() . "\n";
    }
}

class LogWriter implements SplObserver
{
    public function update(SplSubject $subject)
    {
        echo "记录日志:用户名称已更新为" . $subject->getName() . "\n";
    }
}

// 使用
$user = new User();
$user->attach(new EmailNotifier());
$user->attach(new LogWriter());
$user->setName('张三'); // 会自动通知所有观察者

要点:主题维护观察者列表,提供attach/detach/notify方法,状态改变时调用notify通知所有观察者。

好处:主题和观察者解耦,新增观察者不需要修改主题代码,符合开闭原则。

4. 策略模式(Strategy)

目的:定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换。

使用场景:需要在运行时选择不同算法(如不同的支付方式、不同的排序算法、不同的缓存策略)。

// 策略接口
interface PaymentStrategy
{
    public function pay($amount);
}

// 具体策略
class AlipayStrategy implements PaymentStrategy
{
    public function pay($amount)
    {
        echo "使用支付宝支付" . $amount . "元\n";
    }
}

class WechatPayStrategy implements PaymentStrategy
{
    public function pay($amount)
    {
        echo "使用微信支付" . $amount . "元\n";
    }
}

class CreditCardStrategy implements PaymentStrategy
{
    public function pay($amount)
    {
        echo "使用信用卡支付" . $amount . "元\n";
    }
}

// 上下文
class Payment
{
    private $strategy;

    public function setStrategy(PaymentStrategy $strategy)
    {
        $this->strategy = $strategy;
    }

    public function pay($amount)
    {
        $this->strategy->pay($amount);
    }
}

// 使用
$payment = new Payment();

// 用支付宝支付
$payment->setStrategy(new AlipayStrategy());
$payment->pay(100);

// 切换到微信支付
$payment->setStrategy(new WechatPayStrategy());
$payment->pay(200);

要点:定义策略接口,具体策略实现接口,上下文类持有策略引用,运行时可切换策略。

好处:算法可以自由切换,新增算法不需要修改上下文代码,符合开闭原则;避免了大量的if-else或switch。

5. 装饰器模式(Decorator)

目的:动态地给一个对象添加一些额外的职责,而不改变其结构。

使用场景:需要在运行时动态地给对象添加功能,且不希望通过继承来扩展(继承会导致类爆炸)。

// 组件接口
interface Coffee
{
    public function cost();
    public function description();
}

// 具体组件
class SimpleCoffee implements Coffee
{
    public function cost()
    {
        return 10;
    }

    public function description()
    {
        return "简单咖啡";
    }
}

// 装饰器基类
abstract class CoffeeDecorator implements Coffee
{
    protected $coffee;

    public function __construct(Coffee $coffee)
    {
        $this->coffee = $coffee;
    }
}

// 具体装饰器
class MilkDecorator extends CoffeeDecorator
{
    public function cost()
    {
        return $this->coffee->cost() + 2;
    }

    public function description()
    {
        return $this->coffee->description() . " + 牛奶";
    }
}

class SugarDecorator extends CoffeeDecorator
{
    public function cost()
    {
        return $this->coffee->cost() + 1;
    }

    public function description()
    {
        return $this->coffee->description() . " + 糖";
    }
}

class WhipDecorator extends CoffeeDecorator
{
    public function cost()
    {
        return $this->coffee->cost() + 3;
    }

    public function description()
    {
        return $this->coffee->description() . " + 奶油";
    }
}

// 使用
$coffee = new SimpleCoffee();
echo $coffee->description() . ":" . $coffee->cost() . "元\n";

// 加牛奶
$coffee = new MilkDecorator($coffee);
echo $coffee->description() . ":" . $coffee->cost() . "元\n";

// 加糖
$coffee = new SugarDecorator($coffee);
echo $coffee->description() . ":" . $coffee->cost() . "元\n";

// 加奶油
$coffee = new WhipDecorator($coffee);
echo $coffee->description() . ":" . $coffee->cost() . "元\n";

要点:装饰器和被装饰者实现同一个接口,装饰器持有被装饰者的引用,在调用被装饰者方法前后添加新功能。

好处:可以动态地、灵活地给对象添加功能,比继承更灵活,避免类爆炸。

6. 适配器模式(Adapter)

目的:将一个类的接口转换成客户希望的另一个接口,使得原本不兼容的类可以一起工作。

使用场景:需要使用一个已有的类,但它的接口不符合需求;需要创建一个可复用的类,与不相关的类或不可预知的类协作。

// 目标接口
interface Target
{
    public function request();
}

// 被适配者(已有类,接口不兼容)
class Adaptee
{
    public function specificRequest()
    {
        echo "被适配者的特殊请求\n";
    }
}

// 适配器
class Adapter implements Target
{
    private $adaptee;

    public function __construct(Adaptee $adaptee)
    {
        $this->adaptee = $adaptee;
    }

    public function request()
    {
        // 转换接口调用
        $this->adaptee->specificRequest();
    }
}

// 使用
$adaptee = new Adaptee();
$adapter = new Adapter($adaptee);
$adapter->request(); // 客户端通过Target接口调用,实际执行的是Adaptee的方法

实际例子:PHP的数据库扩展,mysql、mysqli、PDO接口不一样,可以用适配器模式统一接口。

要点:适配器实现目标接口,持有被适配者的引用,在目标接口方法中调用被适配者的方法。

好处:可以让不兼容的类一起工作,不需要修改已有类的代码。

7. 外观模式(Facade)

目的:为子系统中的一组接口提供一个一致的界面,定义一个高层接口,使得子系统更容易使用。

使用场景:子系统很复杂,客户端需要和很多类交互;需要分层,每层之间通过外观通信。

// 子系统类
class CPU
{
    public function start() { echo "CPU启动\n"; }
    public function shutdown() { echo "CPU关闭\n"; }
}

class Memory
{
    public function load() { echo "内存加载\n"; }
    public function free() { echo "内存释放\n"; }
}

class HardDrive
{
    public function read() { echo "硬盘读取\n"; }
    public function write() { echo "硬盘写入\n"; }
}

// 外观类
class Computer
{
    private $cpu;
    private $memory;
    private $hardDrive;

    public function __construct()
    {
        $this->cpu = new CPU();
        $this->memory = new Memory();
        $this->hardDrive = new HardDrive();
    }

    // 简化的启动接口
    public function start()
    {
        echo "开始启动电脑...\n";
        $this->cpu->start();
        $this->memory->load();
        $this->hardDrive->read();
        echo "电脑启动完成\n";
    }

    // 简化的关闭接口
    public function shutdown()
    {
        echo "开始关闭电脑...\n";
        $this->hardDrive->write();
        $this->memory->free();
        $this->cpu->shutdown();
        echo "电脑关闭完成\n";
    }
}

// 使用
$computer = new Computer();
$computer->start();   // 客户端只需要调用一个方法,不需要和子系统的多个类交互
$computer->shutdown();

要点:外观类封装子系统的复杂操作,提供简单的高层接口给客户端。

好处:简化客户端使用,客户端不需要知道子系统的细节,降低耦合。

设计模式的原则

设计模式背后,有几个核心的面向对象设计原则:

  1. 单一职责原则(SRP):一个类只做一件事。
  2. 开闭原则(OCP):对扩展开放,对修改关闭。
  3. 里氏替换原则(LSP):子类可以替换父类。
  4. 接口隔离原则(ISP):接口要小而专一。
  5. 依赖倒置原则(DIP):依赖抽象,不依赖具体。
  6. 组合优于继承:优先用组合,而不是继承来扩展功能。
  7. 最少知识原则(迪米特法则):一个对象应该对其他对象有最少的了解。

设计模式的注意事项

  1. 不要为了用模式而用模式:设计模式是工具,不是目的。只有当确实需要的时候才用,不要过度设计。
  2. 理解模式的思想,而不是死记代码:设计模式的核心是思想,代码只是实现。理解了思想,才能灵活运用。
  3. 重构中引入模式:不要一开始就设计模式,先写简单的代码,在重构过程中,发现需要模式的时候再引入。
  4. 模式可以组合使用:实际项目中,多种模式可以组合使用,比如工厂+策略+观察者。
  5. 语言特性可能替代模式:PHP的一些特性(如闭包、trait、匿名类)可能可以简化某些模式的实现,要灵活运用。

总结

设计模式是前辈们总结的最佳实践,能帮你写出更优雅、更易维护、更易扩展的代码。

常用的设计模式:

  1. 单例模式:保证一个类只有一个实例
  2. 工厂模式:根据条件创建不同对象
  3. 观察者模式:一对多的依赖,状态改变自动通知
  4. 策略模式:封装一系列算法,可互相替换
  5. 装饰器模式:动态给对象添加功能
  6. 适配器模式:转换接口,让不兼容的类一起工作
  7. 外观模式:为复杂子系统提供简单接口

设计模式的核心原则:单一职责、开闭原则、里氏替换、接口隔离、依赖倒置、组合优于继承。

设计模式不是万能的,不要为了用模式而用模式。要理解模式的思想,在合适的场景灵活运用。

希望这篇文章能帮你掌握常用的设计模式,写出更优雅的PHP代码。