概述
上文說到了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
[ServiceContract]
public interface IMp3
{
[OperationContract]
void PlaySound(string soundFile);
}
IRecorder.cs
[ServiceContract]
public interface IRecorder
{
[OperationContract]
void Record();
}
ITelephone.cs
[ServiceContract]
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
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="", ReplyAction="")]
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="", ReplyAction="")]
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="", ReplyAction="")]
void PlaySound(string soundFile);
[System.ServiceModel.OperationContractAttribute(Action="", ReplyAction="")]
void Record();
[System.ServiceModel.OperationContractAttribute(Action="", ReplyAction="")]
void Call(string to);
[System.ServiceModel.OperationContractAttribute(Action="", ReplyAction="")]
void Pickup(string from);
[System.ServiceModel.OperationContractAttribute(Action="", ReplyAction="")]
void ReceiveMessage(string from);
[System.ServiceModel.OperationContractAttribute(Action="", ReplyAction="")]
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
[DataContract]
public class Order
{
[DataMember]
public string OrderName
{
get;
set;
}
}
和使用該協定的服務契約IOrderManager.cs
IOrderManager.cs
[ServiceContract]
public interface IOrderManager
{
[OperationContract]
void Process(Order order);
}
實現IOrderManager的服務為:OrderManager.cs
OrderManager
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="")]
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="", ReplyAction="")]
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,代碼為:
OrderDetail.cs
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Runtime.Serialization", "3.0.0.0")]
[System.Runtime.Serialization.DataContractAttribute(Name = "OrderDetail", Namespace = "")]
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這個基類。
此時我們編寫客戶端調用代碼如下:
IOrderManager orderService = new OrderManagerClient(new NetTcpBinding(), new EndpointAddress("net.tcp://127.0.0.1:1237/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的設計原則,但也恰恰體現了面向服務的優點,且這種做法更適應變化。