概述
上文说到了WCF和传统面向对象编程中不太一致的地方之一:操作重载(Operation Overload),本文讲述WCF 另外一个不太符合OO之处:服务契约和数据契约的继承关系。在面向对象的大原则中有下面两个原则
1) 依赖倒置原则
2) Liskov替换原则
依赖倒置原则强调的是实现依赖于抽象,抽象不依赖于实现 ,而Liskov原则强调的是子类必须可以替换其基类,这在anytao大作<<你必须知道的.Net>>中都有详细的阐述。本文无意阐述这两个原则的细节,想了解OO原则的知识,可以阅读王兄的大作。本文只探讨WCF架构下对这两个原则的辩证统一关系。
WCF架构的特征
在弄清楚WCF在上两个OO原则矛盾统一的关系之前,我想有必要先了解WCF的架构,清楚了WCF架构之后,才能更清楚地明白为何WCF中对上述原则的辩证关系!我们先来看下WCF 通讯的工作原理
请看上面的WCF体系结构图(该图原出处<<WCF服务编程>>一书),从图中我们看出WCF通讯双方是存在明显的分界的,尽管WCF也支持in-proc,但这种分界依然存在。我们知道接口和抽象类都是对现实世界的一种抽象描述,它们基于的是现实中的真实场景。比如公鸡能报晓,猴子能上树,老鼠能盗洞,公鸡母鸡都是鸡,鸡鸭鹅全是家禽等等。这些都是人类在长期社会生活中,对现实世界的一种认识!这种认识是存在地域特性的,比如有些地区视蛇为毒虫猛兽,如果给蛇作个接口的话,会包含如下云云:void EatPeople();它会吃人,这种印象很不好,但是另外一些地区可能就将蛇作为图腾,他们眼中蛇是神圣的,如果让他们描述蛇,他们会说: void ProtectPeople();蛇能庇佑人类!同样对事物的分类也是如此。隐喻到软件开发中,我们在一个边界下定义的接口规范和类的层次对于其他边界下的系统是否一定通用呢?答案是否定的。在WCF中,服务与客户是完全松散的耦合,他们之间完全没有必要了解对方的具体实现,如果不是用到WCF,二者老死都可以不相往来。但二者之间加入WCF之后便有了联系,我的理解是代理(Proxy)便是二者之间的红娘,它起到了桥梁,纽带,中介的作用。既然是中介,那么他就应该一碗水端平,不能因为服务端的自身问题给客户端带来不必要的负担,反之亦然!也就是说WCF服务端定义的一些层级概念是服务端的规范,这些规范针对客户端来说,是否适用,那要看客户端的具体业务逻辑,所以代理这个红娘就不能将服务端的逻辑强加给客户端。下面我们就从服务契约(ServiceContract)的层级关系和数据协定(DataContract)的层级关系两个方面看看WCF框架是如何体现上述的特征的。
服务契约的层级关系
闲言少叙,我们采用下面的场景来做演示,场景如下:
Mp3是一个能播放音乐的机器,而录音机是一款能录音的机器,当前的大部分手机呢,除了原有的接打电话,收发短信等功能,它还有一些扩展功能,比如播放音乐的功能,录音的功能。而对于现实中某些个体而言,他可能只会用到手机功能的全部或者一部分,比如一个人用到了全部的功能,它所认识的手机便是:能播打电话,能收发短信,能放歌,能录音的机器,而另外一个人他只用到放歌的功能,对于他来讲,手机就= mp3播放器,同理,如果他只用到录音功能,那在他看来手机就是个录音机。 |
用程序实现如下,按照WCF实现的通常步骤,我们先来实现契约部分,契约部分我们定义三个服务契约:
IMp3.cs
public interface IMp3
{
[OperationContract]
void PlaySound(string soundFile);
}
IRecorder.cs
public interface IRecorder
{
[OperationContract]
void Record();
}
ITelephone.cs
public interface ITelephone:IMp3,IRecorder
{
[OperationContract]
void Call(string to);
[OperationContract]
void Pickup(string from);
[OperationContract]
void ReceiveMessage(string from);
[OperationContract]
void SendMessage(string to);
}
接下来,我们实现服务的实现部分,我们在服务实现中,只需要实现一个Telephoe便可以完成契约中全部的功能列表了
Telephone.cs
using log = System.Console;
public class Telephone:Contracts.ITelephone
{
public void Call(string to)
{
log.WriteLine("telephone is calling");
}
public void Pickup(string from)
{
log.WriteLine("telphone is pickuping.");
}
public void ReceiveMessage(string from)
{
log.WriteLine("telephone is receiving private message");
}
public void SendMessage(string to)
{
log.WriteLine("telephone is sending private message");
}
public void PlaySound(string soundFile)
{
log.WriteLine("telephone is playing");
}
public void Record()
{
log.WriteLine("telephone is recording");
}
}
此时我们先来看一下服务端服务契约的层级关系图:
下面我们实现一个托管,这部分代码不重要,和本文想要阐述的知识也不太相关,所以可以略过,只知道我们上一步中的服务已经被托管起来了。托管代码为:
Uri baseAddress = new Uri("net.tcp://127.0.0.1:1236");
ServiceHost host = new ServiceHost(typeof(Services.Telephone), baseAddress);
host.AddServiceEndpoint(typeof(Contracts.IMp3), new NetTcpBinding(),"mp3");
host.AddServiceEndpoint(typeof(Contracts.IRecorder), new NetTcpBinding(), "recorder");
host.AddServiceEndpoint(typeof(Contracts.ITelephone), new NetTcpBinding(), "tel");
ServiceMetadataBehavior metaBehavior = host.Description.Behaviors.Find<ServiceMetadataBehavior>();
if (metaBehavior == null)
{
metaBehavior = new ServiceMetadataBehavior();
host.Description.Behaviors.Add(metaBehavior);
}
BindingElement bindingElement = new TcpTransportBindingElement();
CustomBinding metaBind = new CustomBinding(bindingElement);
host.AddServiceEndpoint(typeof(IMetadataExchange), metaBind, "MEX");
host.Open();
Console.WriteLine("service is running");
Console.Read();
host.Close();
用Svcutil.exe生成代理文件Proxy.cs,打开它,我们会发现如下的代码:
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行库版本:2.0.50727.1433
//
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(ConfigurationName="IMp3")]
public interface IMp3
{
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IMp3/PlaySound", ReplyAction="http://tempuri.org/IMp3/PlaySoundResponse")]
void PlaySound(string soundFile);
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public interface IMp3Channel : IMp3, System.ServiceModel.IClientChannel
{
}
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public partial class Mp3Client : System.ServiceModel.ClientBase<IMp3>, IMp3
{
public Mp3Client()
{
}
public Mp3Client(string endpointConfigurationName) :
base(endpointConfigurationName)
{
}
public Mp3Client(string endpointConfigurationName, string remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public Mp3Client(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public Mp3Client(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
base(binding, remoteAddress)
{
}
public void PlaySound(string soundFile)
{
base.Channel.PlaySound(soundFile);
}
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(ConfigurationName="IRecorder")]
public interface IRecorder
{
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IRecorder/Record", ReplyAction="http://tempuri.org/IRecorder/RecordResponse")]
void Record();
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public interface IRecorderChannel : IRecorder, System.ServiceModel.IClientChannel
{
}
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public partial class RecorderClient : System.ServiceModel.ClientBase<IRecorder>, IRecorder
{
public RecorderClient()
{
}
public RecorderClient(string endpointConfigurationName) :
base(endpointConfigurationName)
{
}
public RecorderClient(string endpointConfigurationName, string remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public RecorderClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public RecorderClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
base(binding, remoteAddress)
{
}
public void Record()
{
base.Channel.Record();
}
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(ConfigurationName="ITelephone")]
public interface ITelephone
{
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IMp3/PlaySound", ReplyAction="http://tempuri.org/IMp3/PlaySoundResponse")]
void PlaySound(string soundFile);
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IRecorder/Record", ReplyAction="http://tempuri.org/IRecorder/RecordResponse")]
void Record();
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ITelephone/Call", ReplyAction="http://tempuri.org/ITelephone/CallResponse")]
void Call(string to);
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ITelephone/Pickup", ReplyAction="http://tempuri.org/ITelephone/PickupResponse")]
void Pickup(string from);
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ITelephone/ReceiveMessage", ReplyAction="http://tempuri.org/ITelephone/ReceiveMessageResponse")]
void ReceiveMessage(string from);
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/ITelephone/SendMessage", ReplyAction="http://tempuri.org/ITelephone/SendMessageResponse")]
void SendMessage(string to);
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public interface ITelephoneChannel : ITelephone, System.ServiceModel.IClientChannel
{
}
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public partial class TelephoneClient : System.ServiceModel.ClientBase<ITelephone>, ITelephone
{
public TelephoneClient()
{
}
public TelephoneClient(string endpointConfigurationName) :
base(endpointConfigurationName)
{
}
public TelephoneClient(string endpointConfigurationName, string remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public TelephoneClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public TelephoneClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
base(binding, remoteAddress)
{
}
public void PlaySound(string soundFile)
{
base.Channel.PlaySound(soundFile);
}
public void Record()
{
base.Channel.Record();
}
public void Call(string to)
{
base.Channel.Call(to);
}
public void Pickup(string from)
{
base.Channel.Pickup(from);
}
public void ReceiveMessage(string from)
{
base.Channel.ReceiveMessage(from);
}
public void SendMessage(string to)
{
base.Channel.SendMessage(to);
}
}
从上面的代码中可以看出客户端代理的服务契约的层级关系如下:
从上面的图和代理代码中我们可以看出,ITelephone这个接口与Imp3,IRecorder之间已经没有了继承关系。而是直接将Imp3,IRecorder中的功能添加到了ITelephone中,这样一来客户端代理中的TelephoneClient便不依赖于IMp3和IRecorder,也就从根本上更改了他们之间的层次关系。这样做的好处很明显,如果客户端需要全部功能,它只需要获得ITelephone和TelephoneClient便可以了,客户没必要知道IMp3和IRecorder的存在。同样针对IMp3和IRecorder也是这样的道理。
数据契约的继承关系
在面向对象中,Liskov强调的是任何时候,子类都应该能替换其基类,但在WCF中情形又有所改变,我们根据下面的情形来做演示程序
在服务端有一个订单的数据协定Order,而在客户端重新定义对象OrderDetail继承Order,此时试图用OrderDetail的实例替换Order实例调用WCF服务,会有什么结果? |
根据情形,我们写如下代码:
在服务端契约中,添加数据协定Order.cs
public class Order
{
[DataMember]
public string OrderName
{
get;
set;
}
}
和使用该协定的服务契约IOrderManager.cs
[ServiceContract]
public interface IOrderManager
{
[OperationContract]
void Process(Order order);
}
实现IOrderManager的服务为:OrderManager.cs
using log = System.Console;
public class OrderManager:IOrderManager
{
public void Process(Order order)
{
log.WriteLine("OrderManager is processing order.");
}
}
此时,进行托管和生成代理类,代理类代码如下:
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行库版本:2.0.50727.1433
//
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace Contracts
{
using System.Runtime.Serialization;
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Name="Order", Namespace="http://schemas.datacontract.org/2004/07/Contracts")]
public partial class Order : object, System.Runtime.Serialization.IExtensibleDataObject
{
private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
private string OrderNameField;
public System.Runtime.Serialization.ExtensionDataObject ExtensionData
{
get
{
return this.extensionDataField;
}
set
{
this.extensionDataField = value;
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
public string OrderName
{
get
{
return this.OrderNameField;
}
set
{
this.OrderNameField = value;
}
}
}
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(ConfigurationName="IOrderManager")]
public interface IOrderManager
{
[System.ServiceModel.OperationContractAttribute(Action="http://tempuri.org/IOrderManager/Process", ReplyAction="http://tempuri.org/IOrderManager/ProcessResponse")]
void Process(Contracts.Order order);
}
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public interface IOrderManagerChannel : IOrderManager, System.ServiceModel.IClientChannel
{
}
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public partial class OrderManagerClient : System.ServiceModel.ClientBase<IOrderManager>, IOrderManager
{
public OrderManagerClient()
{
}
public OrderManagerClient(string endpointConfigurationName) :
base(endpointConfigurationName)
{
}
public OrderManagerClient(string endpointConfigurationName, string remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public OrderManagerClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) :
base(endpointConfigurationName, remoteAddress)
{
}
public OrderManagerClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) :
base(binding, remoteAddress)
{
}
public void Process(Contracts.Order order)
{
base.Channel.Process(order);
}
}
此时在客户端增加一个OrderDetail类,使其继承Order,代码为:
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Name = "OrderDetail", Namespace = "http://schemas.datacontract.org/2004/07/Contracts")]
public partial class OrderDetail : Order, System.Runtime.Serialization.IExtensibleDataObject
{
private System.Runtime.Serialization.ExtensionDataObject extensionDataField;
private DateTime CreateTimeField;
public System.Runtime.Serialization.ExtensionDataObject ExtensionData
{
get
{
return this.extensionDataField;
}
set
{
this.extensionDataField = value;
}
}
[System.Runtime.Serialization.DataMemberAttribute()]
public DateTime CreateTime
{
get
{
return this.CreateTimeField;
}
set
{
this.CreateTimeField = value;
}
}
}
我们先来查看一下当前在客户端的类型的关系图
从图中我们可以清晰地看出OrderDetail继承了OrderDetail,如果按照Liskov原则,用OrderDetail这个字类应该完全能替代Order这个基类。
此时我们编写客户端调用代码如下:
Contracts.Order order = new Contracts.Order();
orderService.Process(order);
Contracts.OrderDetail orderDetail = new Contracts.OrderDetail();
orderService.Process(orderDetail);
执行的时候,便会在代码orderService.Process(orderDetail);发生如下的异常:
由此可见,Liskov原则在WCF程序的服务端与客户端之间是不适用的,原因在上图的异常说明中已经描述的很清楚了。当然如果非要保持这种继承关系,WCF也提供了相应的机制。但只是一种变相的策略,却不是从根本上要校正此类问题。
小结
在面向对象中的依赖倒置和Liskov原则都是有边界限制的,针对WCF来讲,服务端所设定的契约关系层次和数据的继承关系不能强制的适用于客户端,这充分体现了面向服务的松散耦合的特征,虽然有悖于OO的设计原则,但也恰恰体现了面向服务的优点,且这种做法更适应变化。
视频,课件以及源码下载
如果您懒得看下面的文字,您按下面的提示下载视频教程,里面还有ppt和源代码