问题:API接口不统一
问题场景 :
假设你要发送邮件,但不同服务商调用方式不同。比如发送邮件,SendGrid、Mailgun、Microsoft Graph 都能发,但调用方式天差地别
SendGrid 的调用方式:
ini
# SendGrid SDK的调用方式
sg = sendgrid.SendGridAPIClient(api_key='SG.xxx')
message = Mail(
from_email='from@example.com',
to_emails='to@example.com',
subject='Hello',
plain_text_content='World'
)
response = sg.send(message)
Mailgun 的调用方式:
perl
# Mailgun SDK的调用方式
requests.post(
"https://api.mailgun.net/v3/your-domain/messages",
auth=("api", "your-api-key"),
data={
"from": "from@example.com",
"to": "to@example.com",
"subject": "Hello",
"text": "World"
}
)
问题:看到区别了吗?参数名不一样(to_emails vs to),调用方式不一样(对象方法 vs HTTP请求),返回格式也不一样。
如果直接在业务代码中调用这些SDK:
python
# 订单服务中
def send_order_confirmation(order_id):
sg = sendgrid.SendGridAPIClient(api_key='SG.xxx')
message = Mail(...)
sg.send(message)
# 用户服务中
def send_welcome_email(user_email):
sg = sendgrid.SendGridAPIClient(api_key='SG.xxx')
message = Mail(...)
sg.send(message)
# ... 还有20个地方都在调用SendGrid
如果要换成Mailgun,得把所有地方都改一遍。
当然,我们可以封装下
python
# 封装成一个函数
def send_email(to, subject, content, provider='sendgrid'):
if provider == 'sendgrid':
sg = sendgrid.SendGridAPIClient(api_key='SG.xxx')
message = Mail(from_email=..., to_emails=to, ...)
sg.send(message)
elif provider == 'mailgun':
requests.post("https://api.mailgun.net/v3/...",
auth=("api", "..."),
data={"from": ..., "to": to, ...})
elif provider == 'microsoft_graph':
# 又是另一套代码
...
# 业务代码就可以这样用了
send_email("user@example.com", "Hello", "World", provider='sendgrid')
封装后确实好多了,业务代码清爽了。但是这种封装方式依然有问题。
函数越来越长 :新增服务商要在这个函数中继续加一个分支,代码越来越长。找个某个特定的服务商要在这个很长的函数中找,容易改错。总而言之,就是不好维护。
当然,可以进一步优化,把不同服务商封装到不同的类中。
python
class SendGridEmail:
def send(self, to, subject, content):
# SendGrid 的代码
...
class MailgunEmail:
def send(self, to, subject, content):
# Mailgun 的代码
...
好多了,但是,这里有个关键问题 :接口不统一 !
看回最开始的问题:
- SendGrid 用
to_emails,Mailgun 用to - SendGrid 用
plain_text_content,Mailgun 用text
如果只是简单封装到类里,接口还是不统一:
python
# 每个服务商的接口不一样,业务代码还是要判断,而且调用方式还不一样!
if provider == 'sendgrid':
email = SendGridEmail()
email.send(to_emails=to, subject=subject, plain_text_content=content) # 参数名不一样
elif provider == 'mailgun':
email = MailgunEmail()
email.send(to=to, subject=subject, text=content) # 参数名不一样
适配器模式的作用**:统一接口 我们想要业务代码只关心"发邮件"这个功能,不关心用哪个服务商。
python
# 业务代码只需要这样写,不管用哪个服务商
manager.send_email(
to="user@example.com",
subject="Hello",
content="World"
)
# 切换服务商?改配置文件即可,无需修改发邮件代码
适配器模式
什么是适配器模式?
核心思想 :让所有服务商的类实现同一个接口。
简单说 :适配器模式就是定义一个统一的接口(抽象类),让不同的实现类都遵循这个接口,这样业务代码就可以统一调用,不需要关心具体是哪个实现。
生活中的例子:中国手机插头插不进欧洲插座,买个转换插头就行了。转换插头做了什么?
- 不改变你的手机
- 不改变欧洲的插座
- 只是在中间做了个"翻译",统一了接口
抽象类 (Abstract Class)就是定义了接口但自己不实现具体功能的类。它就像一个"模板",告诉子类"你必须实现这些方法"。
在 Python 中,使用 ABC(Abstract Base Class)和 @abstractmethod 来定义抽象类:
python
from abc import ABC, abstractmethod
class BaseEmailAdapter(ABC): # ABC 表示这是一个抽象基类
@abstractmethod # 这个装饰器表示子类必须实现这个方法
def send(self, to, subject, content):
pass # 抽象方法不写具体实现,只是定义接口
适配器模式的实现
python
# 1. 先定义统一接口(抽象类)
from abc import ABC, abstractmethod
class BaseEmailAdapter(ABC): # 抽象基类
@abstractmethod
def send(self, to, subject, content): # 统一的参数
"""子类必须实现这个方法"""
pass
# 2. 每个服务商实现这个接口(继承抽象类)
class SendGridAdapter(BaseEmailAdapter):
def send(self, to, subject, content): # 必须和接口一样
# SendGrid 的代码,内部转换成 SendGrid 的调用方式
sg = sendgrid.SendGridAPIClient(api_key=self.api_key)
message = Mail(
from_email=self.from_email,
to_emails=to, # 统一接口用 to,内部转换成 SendGrid 的 to_emails
subject=subject,
plain_text_content=content # 统一接口用 content,内部转换成 plain_text_content
)
return sg.send(message)
class MailgunAdapter(BaseEmailAdapter):
def send(self, to, subject, content): # 必须和接口一样
# Mailgun 的代码,内部转换成 Mailgun 的调用方式
requests.post(
"https://api.mailgun.net/v3/your-domain/messages",
auth=("api", self.api_key),
data={
"from": self.from_email,
"to": to, # 统一接口用 to,Mailgun 也用 to,直接传
"subject": subject,
"text": content # 统一接口用 content,内部转换成 Mailgun 的 text
}
)
def create_adapter(provider):
if provider == 'sendgrid':
return SendGridAdapter(...)
elif provider == 'mailgun':
return MailgunAdapter(...)
# 3. 业务代码只需要依赖接口,不需要知道具体是哪个服务商
adapter = create_adapter(provider) # 根据配置创建适配器(这里只有一次 if-else)
adapter.send(to, subject, content) # 统一调用,参数都一样!
策略模式:优化选择逻辑
核心思想
策略模式:封装选择逻辑,根据情况选择使用哪个策略
两种实现方式
方式1:if-else(简单实现) 根据 provider 选择不同的适配器。
python
def create_adapter(provider):
if provider == 'sendgrid':
return SendGridAdapter(...)
elif provider == 'mailgun':
return MailgunAdapter(...)
# 添加新服务商?又要加一个 elif
问题 :每次添加新服务商,都要改这个函数,加新的 elif。
方式2:字典映射(优雅实现)
python
class EmailManager:
def __init__(self):
# 策略映射:把服务商名称映射到对应的适配器类
self._adapter_classes = {
'sendgrid': SendGridAdapter,
'mailgun': MailgunAdapter,
'microsoft_graph': MicrosoftGraphAdapter
}
def _create_adapter(self, provider):
# 从字典中获取对应的适配器类
adapter_class = self._adapter_classes.get(provider)
if not adapter_class:
raise ValueError(f"不支持的邮件服务商: {provider}")
return adapter_class(api_key=..., from_email=...)
def send_email(self, to, subject, content):
# 1. 从配置获取服务商
provider = self._get_provider_from_config()
# 2. 创建适配器(策略模式:根据配置选择)
adapter = self._create_adapter(provider)
# 3. 调用适配器的 send 方法(适配器模式:统一接口)
return adapter.send(to, subject, content)
两种模式的协作
完整流程
scss
业务代码
↓
EmailManager.send_email() (策略模式:选择适配器)
↓
_create_adapter(provider) (策略模式:根据配置选择)
↓
SendGridAdapter / MailgunAdapter (适配器模式:统一接口)
↓
adapter.send() (适配器模式:统一调用)
↓
SendGrid API / Mailgun API (第三方API)
总结
适配器模式
解决的问题 :接口不统一
解决方案 :定义统一接口(抽象类),所有适配器都实现相同的接口
价值:业务代码可以统一调用,不需要关心具体实现
策略模式
核心:封装选择逻辑,根据情况选择使用哪个策略
反思
人生怎么过都可以。
你想要怎样的人生?