问题的提出
作为面向对象设计的一个基本原则,依赖倒置原则(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, Namespace = "http://www.visionlogic.com/trade")]
[XmlRootAttribute(Namespace = "http://www.visionlogic.com/trade", 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;
#endregionreturn 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,怎么改进呢?回忆我们通常对连接串的处置办法:
- 应用逻辑使用一个逻辑的数据库名称,通过一个数据访问框架调用逻辑的数据库。
- 数据访问框架中有一个类似 ConnectionManager 的机制,负责把逻辑的数据库连接名翻译成实际的连接串。
对上面那个 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 平台开发与集成、领域设计和公钥基础环境应用。近年主要参与数据交换系统、自订制业务领域语言平台项目和信息安全类项目,工余时间喜欢旅游、写作、解趣味数学问题和烹饪。
评论