【51CTO快译】变更总是存在的,包括需求变更、环境变更和过程变更。这些因素加在一起使你的WCF服务也会发生变更,幸运的是,可以在设计之初就采取一些方法来尽量避免这些变更,或者说减少变更给用户和自己带来的影响。
本文探讨的不仅仅是前期如何做才能减少变更次数,同时还讨论了在遇到未曾预见的大型变更前该如何应对。
51CTO编辑推荐:WCF开发基础专题
确定变更
在开始着手处理变更之前,有必要弄清楚在基于WCF的服务中发生变更意味着什么,下面的行为构成了变更:
1、数据契约
(1)增加一个数据成员
(2)移除一个数据成员
(3)重命名一个数据成员
(4)改变数据成员的类型
2、服务契约
(1)增加一个操作
(2)移除一个操作
(3)重命名服务契约
3、操作契约
(1)重命名一个操作
(2)修改操作的签名
这些变更可能源于新的业务需求、硬件整合、业务兼并、新条例或任何其它外部因素,底线是当某些东西超出了开发人员的控制变更外,软件就必须要调整,在WCF世界中处理变更总是有好消息也有坏消息,因为有时候处理起来很简单,但有时候会让你惧怕,但却不得不响应。
WCF中的版本和变更控制
在.Net世界中,处理变更时第一个要考虑的就是如何控制版本,通过版本组装,可以在后续的组件版本中允许无法预料的或有问题的变更,使用这种方式,受影响的客户端可以继续使用旧版本,你就可以避免因变更引起的头痛问题。
那么WCF支持版本控制吗?答案有点担忧。当你在WCF中创建一个数据契约时,这个契约会生成一个XML schema,引用这个schema的用户使用它生成一个代理类,严格地说,数据没有经过这个schema验证,正如你将看到的,这将对服务使用者产生一些异常或令人沮丧的行为。
在进入细节前,仔细研究下面例子自己先熟悉一下,它提供了本文剩余部分讨论的基础:
namespace SampleService
{
[ServiceContract]
public interface IPersonService
{
[OperationContract]
Person GetPerson(int personId);
[OperationContract]
void UpdatePerson(Person p);
}
public class Person
{
private string _firstName = string.Empty;
private string _lastName = string.Empty;
[DataMember]
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
[DataMember]
public string LastName
{
get { return _lastName; }
set { _lastName = value; }
}
}
}
数据契约变更
Person DataContract定义了两个属性:FirstName和LastName,如果某个客户的引用了这个服务,你接着将LastName改为SurName,客户的不会被真正断开,只不过在客户端的代理类中,LastName属性将会显示为空,这时因为当客户的将消息持久化到Person类时,发现没有任何名叫LastName的元素了。
这个简单的变更不会让客户端出现异常错误,但糟糕的是会导致一个异常行为,除非你亲自了解每个客户的应用程序使用的web服务,修改将会是灾难性的,作为一名开发人员,你应该尽一切努力来保护变更给客户带来的影响。
最初,你可以先应用一些最佳实践,帮助那些孤立的客户端应对变更,一个数据契约的升级版本看起来如:
[DataContract(Namespace="http://types.mycompany.com/2009/05/25", Name="PersonContract")]
public class Person : IExtensibleDataObject
{
private string _firstName = string.Empty;
private string _lastName = string.Empty;
private ExtensionDataObject _extensionData;
[DataMember(Name="FirstName")]
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
[DataMember(Name="LastName")]
public string LastName
{
get { return _lastName; }
set { _lastName = value; }
}
public ExtensionDataObject ExtensionData
{
get { return _extensionData; }
set { _extensionData = value; }
}
}
在DataContract和DataMember属性上增加了Namespace、Name和Order参数来控制DataContractSerializer的行为,引用这些服务时会增加一个客户端代理,Name参数会导致串行转换器使用标示的值,而不是真实的公共成员或属性的名字,这种方法允许在内部实现变更,不影响客户端,如下面的变更:
[DataMember(Name="LastName")]
public string SurName
{
get { return _lastName; }
set { _lastName = value; }
}
属性名从LastName变成SurName将会中断现有的客户端,因为客户端使用的Name参数任然是LastName,仅仅内部实现变更了。
第二个显而易见的变更是增加了IExtensibleDataObject接口,实现这个接口让未在契约中明确定义的客户端保留数据,这看起来没什么作用,但是当客户端希望在同一个Person对象上执行处理并返回时就有用了,客户端可以保留新的数据项。例如,使用下面的新成员更新PersonContract不会强制现有的客户端也跟着一起更新:
[DataMember(Name = "MiddleName", Order = 3)]
public string SurName
{
get { return _middleName; }
set { _middleName = value; }
}
事实上,这个成员将允许现有的客户端保留一个值放于MiddleName,实现IExtensibleDataObject对于未来你的数据契约是一个好方法,作为一个最佳实践,你应该在所有数据契约中使用它。
请记住,客户端实际上可以选择一个外部schema验证消息,因此,你在处理数据契约变更时有两件事需要考虑:有schema验证和无schema验证。
当客户端添加了schema验证后,数据契约中任何添加、修改或减去数据项的行为都将导致验证失败,因此,在实际生活照,试验了任何严格的schema验证后,契约就不应该改变了,相反,你应该创建一个全新的契约并在契约中使用不同的命名空间,以表明是新版本。
例如,从实现的视角来看,你应该需要两个独立的服务点来使这两个版本可用:
Original Version: [DataContract(Namespace="http://schemas.mycompany.com/2009/05/25")]
New Version: [DataContract(Namespace="http://schemas.mycompany.com/2009/06/18")]
幸运的是,严格的schema验证不是默认行为,这意味着你在不中断客户端的情况下可以添加或移除数据成员,然而,根据前面讨论过的异常行为,移除一个数据成员不是个好主意,换句话说,增加一个数据成员容易,用户会忽略他们不知道的额外成员。
最关键的是使用DataMember属性的Order参数,使用这个参数告诉串行转化器在XML中各个成员应该显示成怎么样,一个非预期的变更可能会导致XML与原始schema不一致,从一开始就使用Order参数可以避免这个问题,如果你不使用Order参数,串行转化器将按照下面的顺序执行:
1、来自基础类型的成员
2、无Order参数的成员(按字母顺序)
3、有Order参数的成员(按值的顺序)
数据契约变更的最后一种情况是修改数据成员的类型,在这种情况下,最佳的做法是和新的服务契约、实现和终结点一道创建一个新版本的数据契约。
服务契约变更
再说一次,所有服务契约应该按照最佳实践,在ServiceContract属性上同时使用Name和Namespace参数,Person服务契约的一个更新版本看起来如:
[ServiceContract(Name="PersonService", Namespace="http://services.mycompany.com/2009/05/25"]
public interface IPersonService
和数据契约一样,使用Name隔离服务用户和真实接口名,允许内部实现按需变更,Namespace允许你在将来对契约进行版本控制,记住新版本也需要新的终点。
可以在不中断现有用户的情况下往服务契约中添加操作,用户会忽略新增加的操作。另一方面,移除操作将会中断现有用户,如同所有的中断变更,移除操作需要一个新版本和一个新的终点。
操作契约变更
与服务契约和数据契约一样,应该在OperationContract属性上使用Name参数:
[OperationContract(Name="GetPerson"]
Person GetPerson(int personId);
再说一次,在内部实现中用户和变更是隔离的。
最后一个需要考虑的变更是操作契约的签名,这是一个中断变更,有两种解决方案:创建一个新版本或在服务契约上添加一个新操作。
遵守你的承诺
变更是不可避免的,但要做好规划,并遵循一些原则,可以讲WCF服务上变更的影响降到最低,记住,当你发布一个服务时,你应该向用户提供一个承诺,让他们保证遵守契约,在现有的契约上做改动不是一件好事。
为此,请记住下面这些最佳实践:
1、在所有契约上使用Name和Namespace参数;
2、在数据成员上总是使用Order参数;
3、在所有数据契约上实现IExtensibleDataObject;
4、为契约版本控制使用命名空间;
5、记住所有新版本都需要新的终点;
6、使用严格的schema验证时,不要修改契约,创建一个新版本;
7、从服务契约中移除一个操作时,请创建一个新版本;
8、改变一个操作的签名时,请创建一个新版本。
记住这些最佳实践后,在处理你自身或服务用户提出的变更时就会游刃有余了。
原文:Best Practices for Handling Change in Your WCF Applications
作者:Steve Stefanovich
【编辑推荐】