技术开发 频道

Programming WCF Services:数据契约

    在WCF的数据契约中,很明显地体现出WCF还不能够完全支持面向对象的设计思想。在第2章对服务契约的描述中,对契约的继承层级的处理方式来看,已经体现了这一缺陷的端倪。而对于数据契约而言,更是进一步暴露了这样的缺陷。

    首先WCF并不支持Liskov替换原则(LSP),“默认情况下,我们不能用数据契约的子类去替换基类。” 考虑如下的服务契约:
[ServiceContract] interface IContactManager { //Cannot accept Customer object here: [OperationContract] void AddContact(Contact contact); //Cannot return Customer objects here: [OperationContract] Contact[] GetContacts( ); }
    假定客户端同时定义了一个Customer类:
[DataContract] class Customer : Contact { [DataMember] public int OrderNumber; }
    以下代码能够成功通过编译,但在运行时却会失败:
Contact contact = new Customer( ); contact.FirstName = "Juval"; contact.LastName = "Lowy"; ContactManagerClient proxy = new ContactManagerClient( ); //Service call will fail: proxy.AddContact(contact); proxy.Close( );
    因为在这个例子中,我们传递了一个Customer对象,而不是Contact对象。由于服务无法识别Customer对象,也就无法反序列化它所接收到的Contact对象。

    虽然WCF引入了Known Types(已知类型)来解决这一问题,然而对于理解面向对象思想的设计者而言,这样的设计无疑会引入父类与子类之间的耦合度。因为在我们设计父类的时候,就必须事先知道子类的定义。当我们需要扩展子类时,还需要修改父类的定义。

    WCF引入的服务已知类型,比较已知类型而言,有一定程度的改善。因为它可以将父类与子类在层级上的耦合度缩小到方法级上。但这样的耦合,依然是不可接受的。例如:
[DataContract] class Contact {...} [DataContract] class Customer : Contact {...} [ServiceContract] interface IContactManager { [OperationContract] [ServiceKnownType(typeof(Customer))] void AddContact(Contact contact); [OperationContract] Contact[] GetContacts( ); }
    当然,服务已知类型也可以应用到契约接口上,此时,该契约以及实现该契约的所有服务包含的所有操作都能够接收已知的子类。

    为了解决这一问题,WCF提供了配置已知类型的方法。例如:
<system.runtime.serialization> <dataContractSerializer> <declaredTypes> <add type = "Contact,Host,Version=1.0.0.0,Culture=neutral, PublicKeyToken=null"> <knownType type = "Customer,MyClassLibrary,Version=1.0.0.0, Culture=neutral,PublicKeyToken=null"/> </add> </declaredTypes> </dataContractSerializer> </system.runtime.serialization>
    注意上述的配置文件中,我们配置的已知类型必须是类型的fullname。包括命名空间、版本号、Culture等。虽然这种方式可以避免在增加子类的情况下,修改代码、重新编译和重新部署,但无疑加重了开发者的负担,尤其是对配置文件的管理以及后期的维护。

    不过,“如果已知类型对于另一个程序集而言是内部(internal)类型,要添加一个已知类型,只有使用配置文件声明它。”

    总之,在WCF中要实现面向对象的多态,还未能做到非常好的。如果能够将KnownType特性应用到子类上,为子类指名它所继承的父类,无疑更加利于类的扩展。遗憾的是WCF未能做到这一点。

    如果数据契约本身实现了一个接口,情况就变得有趣了。从服务端的定义来看,这样的数据契约仍然可以通过服务已知类型在服务契约上指定实现了数据契约接口的子数据契约类型。例如,数据契约Contact类实现了接口IContact:
interface IContact { string FirstName {get;set;} string LastName {get;set;} } [DataContract] class Contact : IContact {...}
    那么在处理数据契约Contact的服务契约中,如果契约的操作需要以抽象方式,定义IContact类型的参数,就必须使用ServiceKnownType特性指名其实现类Contact,如下所示:
[ServiceContract] [ServiceKnownType(typeof(Contact))] interface IContactManager { [OperationContract] void AddContact(IContact contact); [OperationContract] IContact[] GetContacts( ); }
    注意,此时不能利用KnownType特性,将其直接应用到IContact接口上,因为导出的元数据无法包含接口本身。

    服务端的定义无疑符合面向接口编程思想,除了增加了ServiceKnownType之外,整个设计还算优雅。然而根据这样的定义所导出的服务契约,却未免显得差强人意,如下所示:
[ServiceContract] public interface IContactManager { [OperationContract] [ServiceKnownType(typeof(Contact))] [ServiceKnownType(typeof(object[]))] void AddContact(object contact); [OperationContract] [ServiceKnownType(typeof(Contact))] [ServiceKnownType(typeof(object[]))] object[] GetContacts( ); }
    导出定义中,将应用到契约的ServiceKnownType特性应用到了每个操作上,并且为每个操作都指定了具体的数据契约子类以及一个object[]类型。特别要注意,在操作的返回值与参数中,原来的IContact类型全部被转换为了object类型。原因在于,客户端并没有IContact接口的定义。基于object的契约定义无疑不具备类型安全性。

    解决办法自然是在客户端中增加IContact接口的定义。如此,客户端定义就可以修改为:
[ServiceContract] public interface IContactManager { [OperationContract] [ServiceKnownType(typeof(Contact))] void AddContact(IContact contact); [OperationContract] [ServiceKnownType(typeof(Contact))] IContact[] GetContacts( ); }
    但是,我们不能以具体的数据契约类型Contact,来替换原来的object类型。因为替换为具体的数据契约类型,则客户端的服务契约就与服务端的服务契约不兼容了。所以,下面的定义是错误的:
[ServiceContract] public interface IContactManager { [OperationContract] void AddContact(Contact contact); [OperationContract] Contact[] GetContacts( ); }
0
相关文章