各位观众老爷,大家好!今天咱们聊聊PHP里的“百变星君”——适配器模式 (Adapter Pattern
)。这玩意儿能把两个原本水火不容的接口,硬生生地撮合成一对,让它们愉快地合作。是不是听起来像个媒婆?
别急,咱们先从一个故事开始,更容易理解。
故事:老式插座和新式充电器
话说,你出国旅游,带了个国内的电器,结果发现酒店的插座跟你的插头完全不兼容。咋办?难道要放弃使用心爱的电吹风?当然不是!这时候,就需要一个“转换插头”了,它能把国内的插头转换成国外插座能识别的样式。
适配器模式就像这个“转换插头”,它负责把一个类的接口转换成客户希望的另一个接口。
什么是适配器模式?(官方解释版)
适配器模式属于结构型模式,它允许将一个类的接口转换成客户希望的另一个接口。适配器让原本接口不兼容的类可以一起工作。它主要解决的是接口不兼容的问题。
什么是适配器模式?(通俗解释版)
简单来说,适配器模式就是“搭桥”。当两个类因为接口不兼容而无法直接合作时,我们创建一个适配器类,让它充当中间人,负责把一个类的接口转换成另一个类可以接受的接口。
适配器模式的组成部分
- 目标接口 (Target Interface): 客户期望使用的接口。就像国外酒店的插座。
- 适配器 (Adapter): 负责将源接口转换成目标接口的类。就像转换插头。
- 被适配者 (Adaptee): 需要适配的类,拥有待转换的接口。就像国内的插头。
- 客户端 (Client): 使用目标接口的类。就像你带着电吹风的用户。
适配器模式的两种实现方式
适配器模式主要有两种实现方式:类适配器和对象适配器。
-
类适配器 (Class Adapter): 通过多重继承来实现适配。适配器类同时继承目标接口和被适配者类。
- 优点: 实现简单,直接继承被适配者的功能。
- 缺点: 要求使用多重继承,在某些语言中可能不支持(PHP支持),且耦合度较高。
PHP 类适配器代码示例
<?php // 目标接口: 鸟 interface Bird { public function fly(): string; public function makeSound(): string; } // 被适配者: 火鸡 class Turkey { public function gobble(): string { return "Gobble gobble"; } public function flyShortDistance(): string { return "Flying a short distance"; } } // 适配器: 火鸡适配器,让火鸡也能像鸟一样飞和叫 class TurkeyAdapter extends Turkey implements Bird { public function fly(): string { // 火鸡飞不远,所以模拟多飞几次 return "Trying to fly like a bird: " . $this->flyShortDistance() . ", " . $this->flyShortDistance() . ", " . $this->flyShortDistance(); } public function makeSound(): string { return $this->gobble(); // 火鸡的叫声 } } // 客户端代码 $turkey = new Turkey(); $turkeyAdapter = new TurkeyAdapter(); echo "火鸡:n"; echo " " . $turkey->gobble() . "n"; echo " " . $turkey->flyShortDistance() . "nn"; echo "火鸡适配器 (假装是鸟):n"; echo " " . $turkeyAdapter->makeSound() . "n"; echo " " . $turkeyAdapter->fly() . "n"; ?>
代码解释:
Bird
是目标接口,定义了fly()
和makeSound()
方法,代表鸟应该有的行为。Turkey
是被适配者,它有gobble()
(火鸡叫) 和flyShortDistance()
(短距离飞行) 方法。TurkeyAdapter
是适配器,它继承了Turkey
类,并实现了Bird
接口。 在fly()
方法中,我们模拟了火鸡努力像鸟一样飞,调用了flyShortDistance()
方法多次。 在makeSound()
方法中,直接返回了火鸡的叫声gobble()
。- 客户端代码创建了一个
Turkey
实例和一个TurkeyAdapter
实例,并通过适配器让火鸡“假装”成鸟。
-
对象适配器 (Object Adapter): 通过组合(持有被适配者的实例)来实现适配。适配器类持有被适配者的实例,并在方法中调用被适配者的方法来实现目标接口。
- 优点: 耦合度较低,更加灵活。可以适配多个被适配者。
- 缺点: 需要持有被适配者的实例,略微增加了复杂度。
PHP 对象适配器代码示例
<?php // 目标接口: 鸟 interface Bird { public function fly(): string; public function makeSound(): string; } // 被适配者: 火鸡 class Turkey { public function gobble(): string { return "Gobble gobble"; } public function flyShortDistance(): string { return "Flying a short distance"; } } // 适配器: 火鸡适配器 (对象适配器),让火鸡也能像鸟一样飞和叫 class TurkeyAdapter implements Bird { private $turkey; // 持有火鸡的实例 public function __construct(Turkey $turkey) { $this->turkey = $turkey; } public function fly(): string { // 火鸡飞不远,所以模拟多飞几次 return "Trying to fly like a bird: " . $this->turkey->flyShortDistance() . ", " . $this->turkey->flyShortDistance() . ", " . $this->turkey->flyShortDistance(); } public function makeSound(): string { return $this->turkey->gobble(); // 火鸡的叫声 } } // 客户端代码 $turkey = new Turkey(); $turkeyAdapter = new TurkeyAdapter($turkey); // 将火鸡实例传递给适配器 echo "火鸡:n"; echo " " . $turkey->gobble() . "n"; echo " " . $turkey->flyShortDistance() . "nn"; echo "火鸡适配器 (假装是鸟):n"; echo " " . $turkeyAdapter->makeSound() . "n"; echo " " . $turkeyAdapter->fly() . "n"; ?>
代码解释:
Bird
和Turkey
的定义与类适配器示例相同。TurkeyAdapter
实现了Bird
接口,但它没有继承Turkey
类,而是持有一个Turkey
类的实例 ($turkey
)。- 在
TurkeyAdapter
的构造函数中,我们接收一个Turkey
实例,并将其赋值给$this->turkey
。 fly()
和makeSound()
方法通过调用$this->turkey
的方法来实现目标接口。- 客户端代码创建了一个
Turkey
实例,并将其传递给TurkeyAdapter
的构造函数。
类适配器 vs 对象适配器
特性 | 类适配器 | 对象适配器 |
---|---|---|
实现方式 | 多重继承 | 组合(持有被适配者的实例) |
耦合度 | 较高 | 较低 |
灵活性 | 较低 | 较高 |
适用场景 | 被适配者是类,且可以使用多重继承 | 被适配者是对象,或者需要适配多个被适配者 |
依赖关系 | 依赖于被适配者的实现细节 | 不依赖于被适配者的实现细节,只依赖于其接口 |
适配器模式的应用场景
- 兼容旧代码: 当你需要使用一个旧的类,但它的接口与你的系统不兼容时,可以使用适配器模式来解决。
- 集成第三方库: 当你使用第三方库,但它的接口与你的系统不一致时,可以使用适配器模式来进行适配。
- 重构: 在重构过程中,如果需要修改类的接口,但又不想影响现有代码,可以使用适配器模式来保持兼容性。
- 统一接口: 当你有多个类,它们的功能相似,但接口不同时,可以使用适配器模式来统一它们的接口。
实际案例:数据库适配器
PHP的PDO(PHP Data Objects)就是一个很好的适配器模式的应用。PDO提供了一个统一的接口来访问不同的数据库系统(MySQL, PostgreSQL, SQLite等)。
<?php
// 假设我们有一个旧的数据库类
class OldDatabase
{
public function query(string $sql): array
{
// 模拟查询数据库
echo "Executing old database query: " . $sql . "n";
return ['old_data' => 'result'];
}
}
// 目标接口:统一的数据库接口
interface DatabaseInterface
{
public function execute(string $sql): array;
}
// 适配器:将旧的数据库类适配到新的接口
class OldDatabaseAdapter implements DatabaseInterface
{
private $oldDatabase;
public function __construct(OldDatabase $oldDatabase)
{
$this->oldDatabase = $oldDatabase;
}
public function execute(string $sql): array
{
return $this->oldDatabase->query($sql);
}
}
// 新的代码中使用统一的数据库接口
class NewService
{
private $database;
public function __construct(DatabaseInterface $database)
{
$this->database = $database;
}
public function getData(string $query): array
{
return $this->database->execute($query);
}
}
// 使用旧的数据库类
$oldDatabase = new OldDatabase();
$adapter = new OldDatabaseAdapter($oldDatabase);
// 新的服务可以使用适配后的数据库
$newService = new NewService($adapter);
$data = $newService->getData("SELECT * FROM users");
print_r($data);
?>
代码解释:
OldDatabase
是一个旧的数据库类,它有一个query()
方法,用于执行SQL查询。DatabaseInterface
是目标接口,定义了一个execute()
方法,用于执行SQL查询。OldDatabaseAdapter
是适配器,它实现了DatabaseInterface
接口,并将OldDatabase
适配到新的接口。execute()
方法调用了OldDatabase
的query()
方法。NewService
是新的服务类,它依赖于DatabaseInterface
接口。 通过使用适配器,NewService
可以使用旧的数据库类,而无需修改自身的代码。
适配器模式的优点
- 提高了类的复用性: 可以将不兼容的类组合在一起工作。
- 增加了系统的灵活性: 可以方便地替换被适配者,而无需修改客户端代码。
- 符合开闭原则: 可以在不修改现有代码的情况下,添加新的适配器。
适配器模式的缺点
- 增加了系统的复杂性: 需要创建额外的适配器类。
- 可能会降低性能: 适配器可能会引入额外的开销。
注意事项
- 适配器模式应该谨慎使用。 如果接口不兼容的问题可以通过修改其中一个类的接口来解决,那么最好不要使用适配器模式。
- 适配器的设计应该尽量简单。 适配器只应该负责接口的转换,而不应该包含过多的业务逻辑。
总结
适配器模式是一个非常有用的设计模式,它可以帮助我们解决接口不兼容的问题,提高代码的复用性和灵活性。 但是,它也会增加系统的复杂性,因此应该谨慎使用。记住,设计模式是工具,用得好能事半功倍,用不好反而会适得其反。
希望今天的讲解对大家有所帮助! 下次再见!