写代码不难,写出好代码难。
很多初学者,写代码都是想到哪写到哪,怎么方便怎么来。写小项目的时候还能应付,但项目一大,代码就变得混乱不堪——类与类之间耦合严重,改一个地方要动很多地方,新增功能要改大量代码,维护起来非常痛苦。
这时候,就需要设计模式了。
设计模式(Design Pattern)是前辈们在软件开发中总结出来的最佳实践,是解决特定问题的一套成熟方案。它不是具体的代码,而是一种思想、一种套路、一种解决问题的思维方式。掌握了设计模式,就能写出更优雅、更易维护、更易扩展的代码。
设计模式最初是由GoF(Gang of Four,四人帮)在1994年出版的《设计模式:可复用面向对象软件的基础》一书中提出的,共23种设计模式。这些模式,经过了几十年的验证,至今仍然是面向对象编程的经典。
PHP作为一门面向对象的语言,设计模式同样适用。主流PHP框架(Laravel、Symfony等)都大量使用了设计模式。掌握设计模式,不仅能让你写出更好的代码,也能让你更好地理解框架的源码。
今天分享PHP中最常用的几种设计模式,帮你从"写能跑的代码"进阶到"写优雅的代码"。
设计模式的分类
GoF的23种设计模式,分为三大类:
- 创建型模式(Creational):处理对象创建的机制,在不指定具体类的情况下创建对象。包括:工厂方法、抽象工厂、单例、建造者、原型。
- 结构型模式(Structural):处理类和对象的组合,形成更大的结构。包括:适配器、桥接、组合、装饰器、外观、享元、代理。
- 行为型模式(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();要点:外观类封装子系统的复杂操作,提供简单的高层接口给客户端。
好处:简化客户端使用,客户端不需要知道子系统的细节,降低耦合。
设计模式的原则
设计模式背后,有几个核心的面向对象设计原则:
- 单一职责原则(SRP):一个类只做一件事。
- 开闭原则(OCP):对扩展开放,对修改关闭。
- 里氏替换原则(LSP):子类可以替换父类。
- 接口隔离原则(ISP):接口要小而专一。
- 依赖倒置原则(DIP):依赖抽象,不依赖具体。
- 组合优于继承:优先用组合,而不是继承来扩展功能。
- 最少知识原则(迪米特法则):一个对象应该对其他对象有最少的了解。
设计模式的注意事项
- 不要为了用模式而用模式:设计模式是工具,不是目的。只有当确实需要的时候才用,不要过度设计。
- 理解模式的思想,而不是死记代码:设计模式的核心是思想,代码只是实现。理解了思想,才能灵活运用。
- 重构中引入模式:不要一开始就设计模式,先写简单的代码,在重构过程中,发现需要模式的时候再引入。
- 模式可以组合使用:实际项目中,多种模式可以组合使用,比如工厂+策略+观察者。
- 语言特性可能替代模式:PHP的一些特性(如闭包、trait、匿名类)可能可以简化某些模式的实现,要灵活运用。
总结
设计模式是前辈们总结的最佳实践,能帮你写出更优雅、更易维护、更易扩展的代码。
常用的设计模式:
- 单例模式:保证一个类只有一个实例
- 工厂模式:根据条件创建不同对象
- 观察者模式:一对多的依赖,状态改变自动通知
- 策略模式:封装一系列算法,可互相替换
- 装饰器模式:动态给对象添加功能
- 适配器模式:转换接口,让不兼容的类一起工作
- 外观模式:为复杂子系统提供简单接口
设计模式的核心原则:单一职责、开闭原则、里氏替换、接口隔离、依赖倒置、组合优于继承。
设计模式不是万能的,不要为了用模式而用模式。要理解模式的思想,在合适的场景灵活运用。
希望这篇文章能帮你掌握常用的设计模式,写出更优雅的PHP代码。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录