实现Web Service依赖倒置
作者译者王翔发布于 2007年8月1日 下午10时49分
问题的提出
作为面向对象设计的一个基本原则,依赖倒置原则(DIP)在降低模块间耦合度方面有很好的指导意义,他的基本要求和示意图如下:
“高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。”
图1:直接依赖(I)和依赖关系倒置(II)
这么做有什么优势呢?
- 降低Client与ConcreteService的耦合度。
- ConcreteService可以自主的变化,只要符合IService,就可以继续被Client使用。即这种变化对Client透明。
- 应用框架可以Client的上下文为他物色一个合适的ConcreteService,动态构造、动态绑定、运行时动态调用。
在单应用时代,基于接口的开发指导了我们养成这种习惯,但是到了SOA环境下,通常的Web Service开发情况又是怎么样呢?
- 客户程序需要调用某个Web Service,获得它的WSDL和相关数据结构的XSD。
- 然后客户程序调用这个Web Service,一般情况下如果WSDL不变的话,可以一直使用该Web Service,即便那个Web Service后面的实现平台发生变化,但因为绑定关系没有变化,所以客户程序不需要任何修改(偶尔因为版本问题,有可能会进行适应性调整)。
- 如果发现有新的相同服务接口的Service Provider做的不错的化或者把原有Web Service做迁移的话,那就需要重新更新WSDL,编译新的Web Service Client Proxy类,有可能客户程序也要重新编译。
Web Service很好地隔绝了服务定义与服务实现两者的关系,同时它也把可能的备选功能提供者从内部一下子推到整个互联网环境下,怎么让客户程序透明的适应众多可选服务就成了一个挑战。
怎么办?老办法——抽象。
实现Web Service依赖倒置
分析
相信在实践设计模式的过程中,开发人员已经对依赖倒置的概念有了深刻的体验,“不依赖于具体实现,而是依赖于抽象”,整理SOA环境下的Web Service一样需要借鉴这个概念,笔者将之称为“Web Service依赖倒置”。大概逻辑结构变成如下:
图2:概要Web Service依赖倒置后的逻辑关系
但Web Service本身接口是“平的”,没有办法继承,只有用OO语言把它进行包装之后才可以成为对应的类,这时候才能有所谓的“继承”或“接口实现”;所谓“抽象”既可能是接口也可能是抽象类(当然,也可以考虑用实体基类),所以在处理ConcreteWebService与抽象Web Service的时候也有两种方式:
- 通过继承的
- 通过单继承+多接口组合的
笔者更倾向于后者,因为通过组合可以不断扩展。同时考虑到Web Service使用往往在一个分布式的环境中,因此参考RPC中常用的叫法,增加了一一个Stub(用接口IServiceX表示)和Proxy。修改后依赖倒置的关系如下:
图3:分布式环境下多组合服务接口实现的Web Service依赖倒置
实现示例
1、对业务数据建模(XSD):
假设业务对象为报价信息,报价分为报价头和明细(1:0..n),因此结构如下:
图4:报价信息的XSD
XSD
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
xmlns="http://www.visionlogic.com/trade"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.visionlogic.com/trade"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xs:element name="Quote">
<xs:annotation>
<xs:documentation>Comment describing your root element</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element ref="QuoteItem" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="Id" type="xs:string" use="required"/>
<xs:attribute name="Company" type="xs:string" use="required"/>
</xs:complexType>
</xs:element>
<xs:element name="QuoteItem">
<xs:complexType>
<xs:attribute name="ProductId" type="xs:integer" use="required"/>
<xs:attribute name="Price" type="xs:double" use="required"/>
<xs:attribute name="QuantitiveInStock" type="xs:double"/>
</xs:complexType>
</xs:element>
</xs:schema>
2、完成XSD与对象实体的映射:(XSD to Object)
Command通过Visual Studio.Net自带的Xsd.exe进行如下操作。
xsd Quote.xsd /c /n:DemoService
这样就生成了结构大概如下的对应的报价实体类:
C#
using System;
using System.Xml.Serialization;
namespace DemoService
{
[System.SerializableAttribute()]
[XmlTypeAttribute(AnonymousType = true,Namespace = "http://www.visionlogic.com/trade")]
[XmlRootAttribute(Namespace = "http://www.visionlogic.com/trade",IsNullable = false)]
public partial class Quote
{
private QuoteItem[] quoteItemField;
private string idField;
private string companyField;
[XmlElementAttribute("QuoteItem")]
public QuoteItem[] QuoteItem
{
get { return this.quoteItemField; }
set { this.quoteItemField = value; }
}
[XmlAttributeAttribute()]
public string Id
{
get { return this.idField; }
set { this.idField = value; }
}
[XmlAttributeAttribute()]
public string Company
{
get { return this.companyField; }
set { this.companyField = value; }
}
}
[SerializableAttribute()]
[XmlTypeAttribute(AnonymousType = true,IsNullable = false)]
public partial class QuoteItem
{
… …
}
}
3、接着,完成抽象的Web Service定义(optional):
该步骤的目的是获取wsdl定义。这里笔者为了省事,用Visual Studio.Net自动生成,所以写了个抽象的Web Service类,实际开发中完全可以独立编写wsdl文件。
C#
using System.Web.Services;
using System.Xml.Serialization;
namespace DemoService
{
[WebService(Name="QuoteService",Namespace="http://www.visionlogic.com/trade")]
public abstract class QuoteServiceBase : WebService
{
[WebMethod()]
[return:XmlElement("Quote",Namespace="http://www.visoinlogic.com/trade")]
public abstract Quote GetQuote(string id);
}
}
WSDL (Quote.wsdl)
<?xml version="1.0" encoding="utf-8"?>
<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tm="http://microsoft.com/wsdl/mime/textMatching/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:mime="http://schemas.xmlsoap.org/wsdl/mime/" xmlns:tns="http://www.visionlogic.com/trade" xmlns:s1="http://www.visoinlogic.com/trade" xmlns:s="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:http="http://schemas.xmlsoap.org/wsdl/http/" targetNamespace="http://www.visionlogic.com/trade" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/">
<wsdl:types>
<s:schema elementFormDefault="qualified" targetNamespace="http://www.visionlogic.com/trade">
<s:import namespace="http://www.visoinlogic.com/trade" />
<s:element name="GetQuote">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" name="id" type="s:string" />
</s:sequence>
</s:complexType>
</s:element>
<s:element name="GetQuoteResponse">
<s:complexType>
<s:sequence>
<s:element minOccurs="0" maxOccurs="1" ref="s1:Quote" />
</s:sequence>
</s:complexType>
</s:element>
… …
<wsdl:service name="QuoteService">
<wsdl:port name="QuoteServiceSoap" binding="tns:QuoteServiceSoap">
<soap:address location="http://localhost:2401/QuoteServiceBase.asmx" />
</wsdl:port>
<wsdl:port name="QuoteServiceSoap12" binding="tns:QuoteServiceSoap12">
<soap12:address location="http://localhost:2401/QuoteServiceBase.asmx" />
</wsdl:port>
</wsdl:service>
</wsdl:definitions>
4、生成Web Service接口类型:
Command通过Visual Studio.Net自带的Wsdl.exe进行如下操作。
wsdl /n:DemoService /serverinterface /o:IQuoteStub.cs Quote.wsdl Quote.xsd
这样就生成了报价Web Service的抽象接口:
C#
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Web.Services.Description;
using System.Xml.Serialization;
namespace DemoService
{
[WebServiceBindingAttribute(
Name = "QuoteServiceSoap",Namespace = "http://www.visionlogic.com/trade")]
public interface IQuoteServiceSoap
{
[WebMethodAttribute()]
[SoapDocumentMethodAttribute(
"http://www.visionlogic.com/trade/GetQuote",
RequestNamespace = "http://www.visionlogic.com/trade",
ResponseNamespace = "http://www.visionlogic.com/trade",
Use = SoapBindingUse.Literal,
ParameterStyle = SoapParameterStyle.Wrapped)]
[return: XmlElementAttribute("Quote",
Namespace = "http://www.visoinlogic.com/trade")]
Quote GetQuote(string id);
}
}
5、生成具体的报价Web Service:
为了示例的方便,IntranetQuoteService自己“手捏”了一票测试报价数据,至此服务端Web Service工作基本完成,如果需要使用UDDI则还需要把这个具体服务publish出来。
C#
using System;
using System.Web.Services;
using System.Web.Services.Protocols;
namespace DemoService
{
/// <summary>
/// 具体的报价Web Service 功能实现
/// </summary>
[WebService(Namespace = "http://www.visionlogic.com/trade")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class IntranetQuoteService : WebService,IQuoteServiceSoap
{
/// <summary>
/// 实现抽象的Web Service调用
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[WebMethod]
public Quote GetQuote(string id)
{
#region "手捏"出来的测试数据
Quote quote = new Quote();
quote.Id = id;
quote.Company = "deluxe";
QuoteItem[] items = new QuoteItem[2];
items[0] = new QuoteItem();
items[0].QuantitiveInStockSpecified = true;
items[0].ProductId = "Note Bulletin";
items[0].Price = 220;
items[0].QuantitiveInStock = 10;
items[1] = new QuoteItem();
items[1].QuantitiveInStockSpecified = true;
items[1].ProductId = "Pen";
items[1].Price = 3.4;
items[1].QuantitiveInStock = 3000;
quote.QuoteItem = items;
#endregion
return quote;
}
}
}
6、生成客户端Proxy:
Command通过Visual Studio.Net自带的Wsdl.exe进行如下操作。
wsdl /n:Test.Client /o:QuoteProxy.cs Quote.wsdl Quote.xsd
这样就生成了报价Web Service的客户端Proxy,他仅通过最初抽象Web Service的WSDL调用服务端Web Service。实际运行过程中,它并不了解真正使用的时候是由哪个服务提供WSDL中声明到的“GetQuote”方法。
C#
using System.Web.Services;
using System.Threading;
using System.Web.Services.Protocols;
using System.Web.Services.Description;
using System.Xml.Serialization;
using DemoService;
namespace Test.Client
{
/// <summary>
/// Web Service 的客户端 Proxy
/// </summary>
[WebServiceBindingAttribute(
Name="QuoteServiceSoap",
Namespace="http://www.visionlogic.com/trade")]
public class QuoteService : SoapHttpClientProtocol
{
/// <summary>
/// 借助 SOAP 消息调用 Web Service 服务端
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[SoapDocumentMethodAttribute(
"http://www.visionlogic.com/trade/GetQuote",
RequestNamespace="http://www.visionlogic.com/trade",
ResponseNamespace="http://www.visionlogic.com/trade",
Use=SoapBindingUse.Literal,
ParameterStyle=SoapParameterStyle.Wrapped)]
[return: XmlElementAttribute("Quote",
Namespace="http://www.visoinlogic.com/trade")]
public Quote GetQuote(string id)
{
object[] results = this.Invoke("GetQuote",new object[] {id});
return ((Quote)(results[0]));
}
}
}
7、客户程序:
最后,通过单元测试工具检查的客户程序如下:
C#
using System;
using DemoService;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Test.Client
{
/// <summary>
/// 测试用客户程序
/// </summary>
[TestClass]
public class Client
{
/// <summary>
/// 为了简化,这里在客户程序中直接定义了具体报价Web Service的Uri.
/// 实际开发中该信息应该作为服务端的一个配置项登记在Directory之中,
/// 客户程序仅仅通过抽象的服务逻辑名称从Directory中获得。)
/// </summary>
[TestMethod]
public void Test()
{
QuoteService service = new QuoteService();
service.Url = "http://localhost:2401/IntranetQuoteService.asmx";
Quote quote = service.GetQuote("quote:2007-07-15");
Assert.AreEqual<string>("quote:2007-07-15",quote.Id);
Assert.AreEqual<string>("deluxe",quote.Company);
Assert.AreEqual<int>(2,quote.QuoteItem.Length);
Assert.IsNotNull(quote.QuoteItem[0]);
}
}
}
注:为了使用方便,本系列所有示例都没有直接采用IIS作为Web Server宿主,而是采用Visual Studio.Net自带的临时服务进程,因此WSDL和Proxy的使用上,相关端口可能会变化。
进一步改进
上面的示例在客户端处理上不算成功,因为它需要客户程序提供ConcreteService的Uri,怎么改进呢?回忆我们通常对连接串的处置办法:
对上面那个Web Service示例的也如法炮制,增加一个逻辑的Directory机制,实际工程中这个Directory可能就是个UDDI服务,不过这里定义了一个精简对象。
图5:为客户程序增加服务Uri管理目录机制
实现如下
C# IServiceDirectory
using System;
namespace Test.Client
{
/// <summary>
/// 抽象的服务目录接口
/// </summary>
public interface IServiceDirectory
{
/// <summary>
/// 通过索引器实现按名称或取实际服务Uri 的机制。
/// 为了约束客户程序对服务目录的使用,仅提供一个readonly 的访问机制。
/// </summary>
/// <param name="name">逻辑的服务名称</param>
/// <returns>实际服务实体的Uri </returns>
string this[string name] { get;}
}
}
C# LocalServiceDirectory
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
namespace Test.Client
{
class LocalServiceDirectory : IServiceDirectory
{
/// <summary>
/// 保存逻辑服务名称与具体Uri 对应关系的目录字典。
/// </summary>
private static IDictionary<string,string> dictionary = null;
/// <summary>
/// 静态构造的过程中,通过访问配置,获取对应关系。
/// </summary>
static LocalServiceDirectory()
{
NameValueCollection appSettings = ConfigurationManager.AppSettings;
if ((appSettings == null) || (appSettings.Count <= 0)) return;
dictionary = new Dictionary<string,string>();
foreach (string name in appSettings.Keys)
dictionary.Add(name,appSettings[name]);
}
public string this[string name]
{
get
{
string uri;
if (!dictionary.TryGetValue(name,out uri))
return string.Empty;
else
return uri;
}
}
}
}
C# DirectoryServiceFactory
using System;
namespace Test.Client
{
/// <summary>
/// 为了隔离客户程序对实际DirectoryService 类型的以来,引入的服务目录工厂。
/// </summary>
public static class DirectoryServiceFactory
{
/// <summary>
/// 工厂方法。
/// 世纪项目中,实体ServiceDirectory 类型可能运行于远端服务器上,
/// 或者就是UDDI服务,获取IServiceDirectory 过程可能还需要借助代理程序完成。
/// </summary>
/// <returns></returns>
public static IServiceDirectory Create()
{
return new LocalServiceDirectory();
}
}
}
C# 修改后的客户程序
using System;
using DemoService;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Test.Client
{
[TestClass]
public class Client
{
[TestMethod]
public void Test()
{
QuoteService service = new QuoteService();
service.Url = DirectoryServiceFactory.Create()["QuoteService"];
… …
}
}
}
进一步讨论
在有效的隔离了实体Web Service与抽象Web Service的关系后,之前设计模式、架构模式中的那些套路就又有了用武之地,比如Observer、Adapter、Factory、Blackboard、MVC… …,甚至于Visitor这中双因素以来的模式也可以套用,只不过原来的消息变成了XML SOAP、对象实体变成了XSD定义下的各种XML,至于UI上能看到的东西还有需要转换的信息由XSL完成即可。
作者简介: 王翔,软件架构师,主要方向为XML技术、.NET平台开发与集成、领域设计和公钥基础环境应用。近年主要参与数据交换系统、自订制业务领域语言平台项目和信息安全类项目,工余时间喜欢旅游、写作、解趣味数学问题和烹饪。
4条回复
回复