什么是REST服务?
如果您不知道答案,那么,请您花5分钟时间来浏览一下这个教程:
(就5分钟,您就读一下吧。)
(图片来自Bing搜索)
您已经知道了,由于REST是基于HTTP协议的服务,它有以下优点:
- 平台无关——例如用Unix架的服务器提供的服务,可以在PC、MAC等其它终端,甚至手机上调用;
- 语言无关——C#架的服务,可以被Java甚至JavaScript调用;
- 便于跨防火墙存取数据;
接下来,用一个例子,来说明怎么设计一组类(三个),用来封装并调用REST服务。
(如果你想知道用.NET怎么创建REST服务?请花30分钟左右看看:An Introduction To RESTful Services With WCF)
任务:封装Bing地图的REST服务,来计算一个两地的驾驶距离。(你知道从徐家汇开车到人民广场要多少路吗? )
要调用一个REST服务,要知道两个要素:
- URL模板——发送请求的字符串的格式;
- XML响应——用XML表达的返回的结果;
Bing Map的URL模板如下(来源于MSDN):
http://dev.virtualearth.net/REST/v1/Routes/travelMode?wayPoint.1=wayPoint1&heading=heading&waypoint.2=wayPoint2&wayPoint.n=wayPointn
&optimize=optimize&avoid=avoidOptions&distanceBeforeFirstTurn=distanceBeforeFirstTurn&timeType=timeType&dateTime=dateTime&maxSolutions=maxSolutions
&routePathOutput=routePathOutput&tolerances=tolerance1,tolerance2,tolerancen&distanceUnit=distanceUnit&key=BingMapsKey
-.-||好长的字符串啊。
对应的XML响应就不“如下”了,大家根据需要看MSDN吧。为了便于直观理解,MSDN同时提供了一些例子。
另外,对于Bing地图的开发,还需要有一个对应的应用程序Key,这个Key用于识别调用Bing地图服务的应用程序。大家可以在这儿免费申请和查看开发者KEY。
我的Key是:我不告诉你。您就自己申请一个吧。
这样,准备就做得差不多了。
既然是实战,达成目标优先,因此,设计一个类来达成目标。
这个类得有一个方法可以计算距离:
- 它的返回值是一个double类型;
- 它需要根据参数来计算距离,最直观的参数就是URL;
- 我们可以用WebClient来触发一个进行一个Http请求;
- 返回的结果是HTML,因此,我们LinqToXml来进行处理;
所以,就有了如下的代码:
1: using System;
2: using System.Linq;
3: using System.Net;
4: using System.Xml.Linq;
5:
6: namespace BingMapRouteDistanceDemo
7: {
8: public class RouteDistanceCalculator
9: {
10: public double GetDistance(string requestUrl)
11: {
12: WebClient client = new WebClient();
13: string resultXML = client.DownloadString(requestUrl);
14: XElement xml = XElement.Parse(resultXML);
15: string resultString = xml.Descendants("TravelDistance").First().Value;
16: return Convert.ToDouble(resultString);
17: }
18: }
19: }
嗯,您是不是已经准备拍砖了? (千万冷静,除非您正想换显示器愁没借口……)
至少,这个类基本上能够正常用(其实是用不起来的)。如果把这样一个类应用到项目中,会有哪些问题?
您一定已经看出来了,至少有两个明显的问题(欢迎补充):
- 输入参数是一个字符串——而且是一个长长的字符串
- 构建这个字符串的任务交给用户,效率低,函数难用且极易出错;
- 这么一串长长的字符串中出了错,你也不知道到底哪儿错了,得一个一个排查。
让我们看看,如果要调用这个类,大概的代码是什么样子的:
1: RouteDistanceCalculator route = new RouteDistanceCalculator();
2: Console.WriteLine(
3: route.GetDistance(
4: @"http://dev.virtualearth.net/REST/V1/Routes?wp.0=37.779160067439079,-122.42004945874214&wp.1=32.715685218572617,-117.16172486543655&key=BingMapsKey")
5: );
如果您熟悉数据库编程,想想手动编写数据库连接字符串的感觉就有体会了。
- 当前类既处理输入(URL),还处理输出(XML),不符合单一责任原则——不解释;
在封装REST服务时,这两个问题也较普遍。我们分别来解决它们。
(图片来自Bing搜索)
首先,我们可以构建一个专门用于创建URL请求的类,这个类有以下特征:
- 把提供给用户输入的每一个参数对应于一个属性;参数的值对应于属性的值;
- 暴露一个方法,用于返回最终的URL;
示意代码如下,文后附完整项目。
1: public class RouteDistanceCalculator
2: {
3: // Pay attention to change of the parameter type.
4: public double GetDistance(RouteRequestBuilder requestBuilder)
5: {
6: WebClient client = new WebClient();
7: // Builder have the responsibility of forming query string.
8: string resultXML = client.DownloadString(requestBuilder.GetQueryString());
9: ...
10: }
11: }
12:
13: public class RouteRequestBuilder
14: {
15: #region Public Method
16: /// <summary>
17: /// Go over properties one by another to construct the query uri.
18: /// </summary>
19: public virtual string GetQueryString()
20: {
21: StringBuilder resultBuilder = new StringBuilder();
22: resultBuilder.Append(BasicQueryUri);
23: // Construct for travel mode
24: resultBuilder.AppendFormat("{0}?", TravelMode.ToString());
25: // ...
26: return resultBuilder.ToString();
27: }
28: #endregion
29:
30: #region Query conditions
31: public IList<Location> WayPoints { get; set; }
32: public TravelMode TravelMode { get; set; }
33: public TransitTimeType TravelTimeType { get; set; }
34: public DateTime TravelDateTime { get; set; }
35: #endregion
36: }
-
有一个方法或者属性,可以获取到原始的结果——例子中webClient返回的XML;
-
把用户关心的内容,以属性的形式暴露出来;
-
用户无须知道XML与属性值的对应关系;
由于用户不需要知道XML的细节,因此,对于这个类来说,如果把结果保存到ResultString属性中,我们不需要暴露它的读方法,意味着这是一个“只写”属性,那么,我们就可以把它做成一个方法。
在这里,我们只关心行驶距离,因此,它应该有一个叫Distance的属性,返回一个double类型的值。
形成的类主要代码如下:
1: public class RouteResponseResolver
2: {
3: #region Properties
4: /// <summary>
5: /// Get distance of the route.
6: /// </summary>
7: public double Distance
8: {
9: get
10: {
11: return GetDistance();
12: }
13: }
14: #endregion
15:
16: #region Private Methods
17: private double GetDistance()
18: {
19: // Covert xml into property
20: }
21: #endregion
22:
23: #region Public Methods
24: /// <summary>
25: /// Update the result string to resolve with
26: /// </summary>
27: /// <param name="resultString"></param>
28: public void UpdateResultString(string resultString)
29: {
30: // Update result string
31: }
32: #endregion
33: }
对应的,我们就需要修改一下RouteDistanceCalculator类,把RouteResponseResolver也作为一个参数传入:
1: public class RouteDistanceCalculator
2: {
3: public double GetDistance(RouteRequestBuilder requestBuilder, RouteResponseResolver resultResolver)
4: {
5: WebClient client = new WebClient();
6: string resultXML = client.DownloadString(requestBuilder.GetQueryString());
7: resultResolver.UpdateResultString(resultXML);
8: return resultResolver.Distance;
9: }
10: }
费了这么多神,我们得到了什么?让我们再来看一下,现在的用户如何来调用RouteDistanceCalculator.
1: static void Main(string[] args)
2: {
3: RouteDistanceCalculator route = new RouteDistanceCalculator();
4: RouteRequestBuilder requestBuilder = new RouteRequestBuilder()
5: {
6: TravelMode = TravelMode.Driving,
7: WayPoints = new List<Location>()
8: {
9: // Location for Xujiahui, Shanghai
10: new Location(){ Latitude=31.1970231682062, Longitude=121.441092118621},
11: // Location for people's square
12: new Location(){ Latitude=31.233944, Longitude=121.467813}
13: }
14: };
15: RouteResponseResolver responseResolver = new RouteResponseResolver();
16: var distance = route.GetDistance(requestBuilder, responseResolver);
17: Console.WriteLine("Drive distance between Xujiahui & People's square are: {0} km.", distance);
18: }
可以看到,通过RouteRequestBuilder类,我们指定了路径类型为驾程,一共涉及到两个点,分别是徐家汇和人民广场;我们用responseResolver来解析查询到的结果……
- 由于请求不再要求用户输入字符串,出错机率减小,即使出错,调试也相对容易许多;
- 对于RouteDistanceCalculator来说,请求和响应解析的类是可以被替换的;
最后,揭晓徐家汇到人民广场的车程:
(图片来自Bing搜索)
最后的后面:
下一步……在这里的请求生成和响应解析类都是具体类,如果把它们抽象成接口,会进一步降低它们之间的耦合性。
小结一下:
一个REST服务总可以封装成三个类:
- 调用类——调用以下两个类;
- 请求生成类——以属性形式接受查询条件的输入,提供生成请求URL的方法;
- 响应解析类——用于解析返回的结果,以属性返回用户感兴趣的数据;
如果将后两个类抽象成对应接口,即可实现请求、响应与调用类之间的解耦合。
资源:
An Introduction To RESTful Services With WCF
示例代码:为了方便大家看上下类的关系,我把所有的类全放在一个文件里了,一般不推荐这样做;
另外,使用示例前,请把文件底部的BingMapKey换成您自己的Key。
Little knowledge is dangerous.