浅谈WebService的版本兼容性设计
在现在大型的项目或者软件开发中,一般都会有很多种终端, PC端比如Winform、WebForm,移动端,比如各种Native客户端(iOS, Android, WP),Html5等,我们要满足以上所有这些客户端的需求,实现前后端的分离,一种最常见的做法是,编写WebService API来为以上客户端提供数据。近年来越来越多的企业或者网站支持Restfull方式的WebServiceAPI,比如当当网开源Dubbox,扩展Dubbo服务框架支持REST风格远程调用,这个是Java版本的,在.NET中ServiceStack天生支持Restfull风格的WebService。本文主要以ServiceStack为基础探讨,浅谈WebService的兼容性设计。
1.软件的兼容性
在软件持续更新升级的过程中,API 也是需要不断更新,这时就需要考虑客户端升级以及兼容性的问题。当前有很多用户可能由于多种原因,尤其是Native用户,不可能及时升级到最新版,所以需要提供对老版本的API的向后兼容。在API设计之初,我们需要考虑一些问题以及解决方法。
后向兼容性(Backward_compatibility),或者向下兼容,是指对于给定的输入,较老版本的产品或者技术,也能够输出相同的结果。如果一个产品或者API在设计之初就能够为新的标准考虑,能够满足接收,读取,查看旧的标准或者格式,那么这个产品就称之为后向兼容,比如一些数据格式或者通讯协议,在新版本推出时都会充分考虑后向兼容问题。如果对一个产品的改进破坏了后向兼容性,则称之为破坏性的改动(breaking change),相信大家都遇到过这种情况。
- App长久没更新,落后很多个版本之后,再次打开改App会提示升级到最新版,或者直接帮你强制升级。
- 使用新版的TortoiseSVN打开老版本TortoiseSVN创建的工程的时候,会提示需要升级项目工程才能打开。
这种情况一般发生在版本的改动比较大,或者对较老版本的支持成本比较大,在这种情况下,一般还需要为客户提供从老版本迁移到新版本的工具或者解决方案。
兼容性有很多种类型比如 API 的兼容, 二进制dll的兼容性,以及数据文档的兼容。
关于API的兼容性其实涉及到API的设计。相关文档和书籍有很多,关于API设计的书可以参考Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries (2nd Edition) 和 How to Design a Good API and Why it Matters
本文主要探讨WebService开发的API的向后兼容性。
2. WebService 的后向兼容性
在关于开发WebService框架上,这里不免又要谈一下WCF和ServiceStack的设计理念和区别。
在ServiceStack中,鼓励使用Message-based 式的设计,因为远程服务调用是很耗时,我们应该尽量一次多传输需要处理的数据,而避免来回多次调用。在WCF中,通过一些工具,使得开发者能够像调用本地方法一样调用远程方法,这样会使人产生误解,实际上调用远程方法会比调用远程方法慢上成千上万倍。ServiceStack在设计之初就受Martine Flowler 的 Data Transfer Object 模式的启发:
“ When you're working with a remote interface, such as Remote Facade (388), each call to it is expensive. As a result you need to reduce the number of calls, and that means that you need to transfer more data with each call. One way to do this is to use lots of parameters. However, this is often awkward to program - indeed, it's often impossible with languages such as Java that return only a single value. The solution is to create a Data Transfer Object that can hold all the data for the call. It needs to be serializable to go across the connection. Usually an assembler is used on the server side to transfer data between the DTO and any domain objects. ” |
在API的设计上WCF鼓励将WebService作为普通的C#方法调用,这是一种基于普通的基于PRC 方式的调用。比如:
public interface IWcfCustomerService { Customer GetCustomerById(int id); List<Customer> GetCustomerByIds(int[] id); Customer GetCustomerByUserName(string userName); List<Customer> GetCustomerByUserNames(string[] userNames); Customer GetCustomerByEmail(string email); List<Customer> GetCustomerByEmails(string[] emails); }
以上WebService方法就是通过id,username或者email获取用户或者用户列表。如果使用ServiceStack的基于Message-base风格的API设计,接口就是:
public class Customers : IReturn<List<Customer>> { public int[] Ids { get; set; } public string[] UserNames { get; set; } public string[] Emails { get; set; } }
在ServiceStack中,所有的请求信息都包装在这个Customers的DTO中,他并不依赖于服务端方法的签名。最简单的好处在于使用message-base的设计在于wcf中的任意RPC组合都可以使用一个ServiceStack中的远程消息组合,并且只需要服务端的一次实现。
闲话说了这么多,现在来看看如何设计WebService的后向兼容性。谈到WebService,大家都会想到WCF,关于WCF的后向兼容,在Codeproject上,有人写了三篇文章WCF Backwards Compatibility and Versioning Strategies(part1,part2,part3),由于ServiceStack仅支持Poco方式的请求参数,并且写在Poco中的字段都是必须的,没有WCF 中的对字段的 [DataMember(IsRequired = true)] 和 [DataMember(IsRequired = false)] 来标识字段是否可选,所以WCF支持的RPC方式的参数(Part1文章中的后向兼容算法)ServiceStack中无法做测试,这里对比做Part2文章中的测试。并且测试的时候,测试添加和移除字段对Service调用的影响。
建立测试之前,我们先建立一个基本的ServiceStack程序。 这个程序和前文中介绍一样,是一个简单的 ServiceStack序。
3. 基础
使用ServiceStack创建服务,基本的工程结构有三个。
ServiceModel这一层主要是定义 WebService中的请求参数和返回参数DTO, Employ中的代码如下:
namespace WebApplication1.ServiceModel { [Route("/Employ/{EmpId}/{EmpName}")] public class Employ : IReturn<EmployResponse> { public string EmpId { get; set; } public string EmpName { get; set; } } public class EmployResponse { public string EmpId { get; set; } public string EmpName { get; set; } } }
代码定义了请求参数DTO Employ对象,约定了其返回类型为 EmployResponse,这里继承IReturn< EmployResponse >是为了方便测试。 这里面指定了这个WebService的请求对象是Employ,返回对象是EmployResponse,并且通过’ /Employ/{EmpId}/{EmpName}’这样的方式来调用服务为Employ对象赋值。
ServiceInterface这一层是服务实现层。里面的EmployServices直接继承自ServiceStack中的Service对象。
namespace WebApplication1.ServiceInterface { public class EmployServices : Service { public EmployResponse Any(Employ request) { return new EmployResponse { EmpId = request.EmpId, EmpName = request.EmpName}; } } }
这里Any表示这个Restfull请求支持Post和Get两种方式,请求参数类型Hello和返回值类型EmployResponse在Model中已经定义。我们不关心这个方法的名称,因为可以通过Rest路由来进行访问。
WebApplication和ConsoleApplicaiton是ServiceInterface的服务宿主层,我们可以使用ASP.NET 将服务部署到IIS上,也可以通过控制台程序进行部署以方便测试。
Web宿主很简单,我们定义一个类继承自AppHostbase,并提供包含有服务的程序集即可:
namespace WebApplication1 { public class AppHost : AppHostBase { /// <summary> /// Default constructor. /// Base constructor requires a name and assembly to locate web service classes. /// </summary> public AppHost() : base("WebApplication1", typeof(EmployServices).Assembly) { } /// <summary> /// Application specific configuration /// This method should initialize any IoC resources utilized by your web service classes. /// </summary> /// <param name="container"></param> public override void Configure(Container container) { //Config examples //this.AddPlugin(new PostmanFeature()); //this.AddPlugin(new CorsFeature()); } } }
然后,在网站启动的时候,在Application_Start方法中初始化即可:
namespace WebApplication1 { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { new AppHost().Init(); } } }
现在我们就可以通过Web的方式来查看我们创建的service服务:
可以通过Post Http的方式采用Json格式调用WebService服务,比如我们可以构造Json格式,将内容Post到 地址,http://localhost:28553/json/reply/ Employ:
{"EmpId":"p1","EmpName":"zhangsan"}
返回值为:
{"EmpId":"p1","EmpName":"zhangsan"}
或者直接在地址栏里输入:http://localhost:28553/Employ/p1/zhangshan
不过在开发的时候,我们通常采用第一种方式,将参数序列化为json字符串,然后post到我们部署的地址上。
以上是服务端代码,部署好了之后,客户端需要进行调用,调用的时候,我们需要引用ServiceModel这里面的请求和返回值实体类型。在部署了WebService之后,我们也可以通过引用WebService的方式来进行引用字段。
新建一个控制台应用程序,将上面的ServiceModel编译为dll之后,拷贝到新建的控制台程序下面,然后引用这个dll,客户端调用代码如下,我们采用了Json的方式传送数据,当然您可以选择其他的数据格式进行传输。代码如下:
class Program { static void Main(string[] args) { Console.Title = "ServiceStack Console Client"; using (var client = new JsonServiceClient("http://localhost:28553/")) { EmployResponse employResponse = client.Send<EmployResponse>(new Employ { EmpId="1", EmpName="zhangshan"}); if (employResponse != null) { Console.WriteLine(string.Format("EmoplyId:{0},EmployName:{1}",employResponse.EmpId,employResponse.EmpName)); } } Console.ReadKey(); } }