Pulpcode

捕获,搅碎,拼接,吞咽

0%

请求三方接口的解决方案

前言

之前的工作任务,大多一直在做这样一件事,请求第三方接口,并将接口数据进行解析成固定格式后返回。之后在做业务处理,包括更新数据库状态之类的。

这里想展示一种解决方案,能够解决以下的问题,包括:

  1. 请求出现异常,包括链接超时,解析异常,请求错误的处理。
  2. 方便的进行单元测试。
  3. 方便的进行扩展。

注意第3条是指数据来源可能是不同的url,请求和相应格式都不同,但是要做的业务逻辑却是相同的,如何做到方便扩展,接入新的三方时,不用该业务代码。

还有第2点,特别提到了单元测试,这里我是指模拟测试,因为在真实情况下,这些类似于创建订单的接口,不是你想调用就调用的,甚至是有些接口,比如银行的服务,在调用的时候都是有成本的,这个时候使用模拟类来完成单元测试就非常有必要了。在这个例子中我们使用的python的mock对象。

下面我会写一个假的例子,来讲解我的解决方案,实际上我们大多数调用的三方接口,并不是走查询这么简单,其实更多是一些post接口,包括下单,和通知。就算是查询接口,可能也不会走get,而是走的post协议。

假设例子

我们假设有两个公司提供天气的查询服务,一个公司叫如风,一个公司叫烟云。
如风公司返回的数据类似于:

1
2
3
4
5
6
{"weatherinfo": {
"city": "北京",
"cityid": "101010100",
"weather": "多云"
}
}

烟云公司返回的格式类似于:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<weatherinfo>
<city>北京</city>
<cityid>101010100</cityid>
<weather>多云</weather>
</weatherinfo>

这样我就要写一个三方接入基类,然后每次接入新的基类,只需要继承此基类,实现相应的接口,当然你可以说python的鸭子类型根本不需要继承,但是之所以继承还是考虑到一些重用的部分,包括打印日志(维护一个日志对象)之类的。只不过这些不算这篇博客的重点罢了,所以你会发现这个例子直接使用类对象,调用类方法就行了,根本没有必要创建对象,因为你没有什么状态需要维护的,那其实是我精简了很多东西罢了。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Weather(object):
def generate_request(self, params):
"""
生成发送的请求报文
"""
raise NotImplementedError

def parser_response(self, response):
"""
解析三方响应报文
"""
raise NotImplementedError

def send_request(self, request):
"""
发送请求给服务器
"""
raise NotImplementedError

你看,我把这三个接口拿出来,就是为了方便的替换。

  1. generate_request(params)
  2. parser_response(response)
  3. send_request(request)

这里需要说明的是,generate_request的params和parser_response的返回体,都是标准的结构,在java中可能是一个类,在python中可能为了方便,字典就够了,之所以说标准是因为,标准才能使你的业务代码不被修改,就能接入新的三方。

而且你可能会说generate和send这两个方法也是可以合并到一起的,我之所以分开是因为send的方式可能会有所不同,我的请求库可能既要在flask上使用,又要在tornado上使用,而在tornado上使用的是异步客户端去实现了,这个时候send的代码自然不同了。

还有我没有使用python的abc类,而是使用了NotImplementedError,我觉得这个方式简单粗暴。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class RuFengWeather(Weather):
def generate_request(self, params):
"""
通过传入城市代码,返回请求url,和请求体。
:return url, body:
"""
return "http://www.rufeng.com/weather/{}".format(params["city_code"]), None

def parser_response(self, response):
"""
解析返回数据,返回格式为json:
{"weatherinfo": {
"city": "北京",
"cityid": "101010100",
"weather": "多云"
}
:params response: 请求返回体
:return 是否解析成功, 解析结果:
True, 解析结果
False, None
"""
try:
response = json.loads(response)
return True, response["weatherinfo"]["weather"]
except:
return False, u"解析返回结果异常"

def send_request(self, url, body):
try:
response = requests.get(url)
return True, response.content
except:
return False, u"请求服务器错误"

我们实现了如风的天气处理类,这里的generate_request,返回了两部分,分别是请求的url和请求的body体。当然因为我们这个例子走的是比较简单的get,所以body体就是None了。

之前看过我博客的同学也一定理解,操作结果+业务结果,这样的双返回逻辑,这里就不再重复了。

另一个三方这里就不再写了,

1
2
3
4
5
def get_weather(weather, city_code):
params = {"city_code": city_code}
url, body = weather.generate_request(params)
response = weather.send_request(url, body)
return weather.parser_response(response)

我再不想写任何类了,这里写一个简单函数来返回查询结果。

单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def test_well_send_request_success():
d = {"weatherinfo": {
"city": "北京",
"cityid": "101010100",
"weather": "多云"
}
}
send_request = mock.Mock()
send_request.return_value = json.dumps(d)
weather = RuFengWeather()
weather.send_request = send_request
expect = (True, u"多云")
actual = get_weather(weather, "101010100")
assert expect == actual

def test_bad_send_request_fail():
d = "xxxx"
send_request = mock.Mock()
send_request.return_value = json.dumps(d)
weather = RuFengWeather()
weather.send_request = send_request
expect = (False, u"解析返回结果异常")
actual = get_weather(weather, "101010100")
assert expect == actual

注意,这里我写了两个单元测试,使用了python的nosetest,还有一个是Mock对象来模拟接口的返回值。你会发现我测试了正常的返回结果和乱七八糟的返回结果。包括一些写单测的心得,我会在之后写博客单独讨论,这里不再重复。

这样就满足了我说的那三个需求。