深入理解设计模式:适配器(Adapter)
什么是适配器模式?
适配器模式(Adapter Pattern)是一种结构型设计模式,它允许将一个类的接口转换成客户期望的另一个接口。适配器让原本由于接口不兼容而不能一起工作的类可以协同工作。
这个模式的核心思想是:转换接口。就像我们生活中常见的电源适配器,可以将不同国家的插头标准转换为适合中国插座的标准,让来自不同国家的电器设备能够在同一套供电系统中正常工作。
适配器的核心要素
1. 目标接口(Target Interface)
这是客户端期望使用的接口。2. 适配者(Adaptee)
这是需要被适配的现有类,其接口与目标接口不兼容。3. 适配器(Adapter)
这是核心角色,负责将适配者的接口转换成目标接口。实际应用场景
场景一:第三方库集成
假设我们需要使用一个老版本的日志库,但它提供的接口与我们项目要求的接口不一致:
// 老版本日志库
interface OldLogger {
void writeLog(String message, int level);
}
// 新版本项目需要的接口
interface NewLogger {
void logInfo(String message);
void logError(String message);
}
// 适配器实现
class LoggerAdapter implements NewLogger {
private OldLogger oldLogger;
public LoggerAdapter(OldLogger oldLogger) {
this.oldLogger = oldLogger;
}
@Override
public void logInfo(String message) {
// 将INFO级别映射为level=1
oldLogger.writeLog(message, 1);
}
@Override
public void logError(String message) {
// 将ERROR级别映射为level=5
oldLogger.writeLog(message, 5);
}
}
场景二:数据库驱动兼容性
不同数据库厂商提供的JDBC驱动接口可能略有差异,我们可以使用适配器来统一接口:
// MySQL驱动接口
interface MySQLDriver {
ResultSet executeQueryMySQL(String sql);
boolean connectMySQL(String url, String user, String password);
}
// PostgreSQL驱动接口
interface PostgreSQLDriver {
QueryResult executeQueryPG(String sql);
Connection connectPG(String connString);
}
// 通用数据库接口
interface UniversalDatabase {
DataSet query(String sql);
boolean connect(String connectionInfo);
}
// MySQL适配器
class MySQLAdapter implements UniversalDatabase {
private MySQLDriver mysqlDriver;
public MySQLAdapter(MySQLDriver driver) {
this.mysqlDriver = driver;
}
@Override
public DataSet query(String sql) {
ResultSet rs = mysqlDriver.executeQueryMySQL(sql);
return convertResultSetToDataSet(rs);
}
@Override
public boolean connect(String connectionInfo) {
// 解析连接信息并调用MySQL驱动
String[] parts = connectionInfo.split(":");
return mysqlDriver.connectMySQL(parts[0], parts[1], parts[2]);
}
}
适配器的实现方式
1. 对象适配器(常用)
通过组合的方式实现适配器:
from abc import ABC, abstractmethod
目标接口
class MediaPlayer(ABC):
@abstractmethod
def play(self, audiotype: str, filename: str):
pass
适配者
class AdvancedMediaPlayer:
def playvlc(self, filename: str):
print(f"Playing vlc file: {filename}")
def playmp4(self, filename: str):
print(f"Playing mp4 file: {filename}")
适配器
class MediaAdapter(MediaPlayer):
def init(self, audiotype: str):
self.advancedplayer = None
if audiotype == "vlc":
self.advancedplayer = AdvancedMediaPlayer()
elif audiotype == "mp4":
self.advancedplayer = AdvancedMediaPlayer()
def play(self, audiotype: str, filename: str):
if audiotype == "vlc":
self.advancedplayer.playvlc(filename)
elif audiotype == "mp4":
self.advancedplayer.playmp4(filename)
客户端代码
class AudioPlayer(MediaPlayer):
def init(self):
self.mediaadapter = None
def play(self, audiotype: str, filename: str):
# 对于支持的格式直接播放
if audiotype in ["mp3", "wav"]:
print(f"Playing {audiotype} file: {filename}")
# 对于不支持的格式,使用适配器
else:
self.mediaadapter = MediaAdapter(audiotype)
self.mediaadapter.play(audiotype, file_name)
2. 类适配器
通过继承的方式实现适配器(需要支持多重继承的语言):
// TypeScript中的类适配器示例
interface Target {
request(): string;
}
// 需要适配的类
class Adaptee {
specificRequest(): string {
return ".eetpadA eht fo roivaheb laicepS";
}
}
// 类适配器
class Adapter extends Adaptee implements Target {
public request(): string {
// 转换接口
const result = this.specificRequest();
return Adapted: ${result.split("").reverse().join("")};
}
}
// 使用
function clientCode(target: Target): void {
console.log(target.request());
}
const adapter = new Adapter();
clientCode(adapter); // 输出: "Adapted: Seppahc eht fo .tcejorP"
适配器模式的优缺点
优点
- 解耦性:客户端不需要知道适配者的具体实现
- 灵活性:可以在运行时切换不同的适配器
- 复用性:可以将现有的适配者集成到新的系统中
- 开闭原则:对扩展开放,对修改关闭
缺点
- 复杂性增加:增加了中间层,可能导致系统变得复杂
- 性能开销:额外的封装层可能带来微小的性能损耗
- 过度使用风险:如果系统设计得当,可能根本不需要适配器
何时使用适配器模式?
- 需要使用现有的类,但其接口不符合你的需求
- 创建一个可复用的类,该类可以与不相关的类或不可预见的类协同工作
- 想要使用已经存在的子类,但无法修改其接口以匹配父类接口
- 需要转换不同数据格式或协议
实际应用建议
- 优先考虑接口重构:在引入适配器之前,先考虑是否可以通过重构使接口一致
- 保持简单:只在真正需要时才使用适配器模式
- 文档化:清晰地记录适配器的作用和转换逻辑
- 测试覆盖:确保适配器的转换逻辑得到充分测试