【AICon】 如何构建高效的 RAG 系统?RAG 技术在实际应用中遇到的挑战及应对策略?>>> 了解详情
写点什么

实现 Web Service 依赖倒置

  • 佚名

  • 2007-08-01
  • 本文字数:14797 字

    阅读完需:约 49 分钟

问题的提出

作为面向对象设计的一个基本原则,依赖倒置原则(DIP)在降低模块间耦合度方面有很好的指导意义,他的基本要求和示意图如下:

“高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于细节。细节应该依赖于抽象。”

图 1:直接依赖(I)和依赖关系倒置(II)

这么做有什么优势呢?

  • 降低 Client 与 ConcreteService 的耦合度。
  • ConcreteService 可以自主的变化,只要符合 IService,就可以继续被 Client 使用。即这种变化对 Client 透明。
  • 应用框架可以 Client 的上下文为他物色一个合适的 ConcreteService,动态构造、动态绑定、运行时动态调用。

在单应用时代,基于接口的开发指导了我们养成这种习惯,但是到了 SOA 环境下,通常的 Web Service 开发情况又是怎么样呢?

  1. 客户程序需要调用某个 Web Service,获得它的 WSDL 和相关数据结构的 XSD。
  2. 然后客户程序调用这个 Web Service,一般情况下如果 WSDL 不变的话,可以一直使用该 Web Service,即便那个 Web Service 后面的实现平台发生变化,但因为绑定关系没有变化,所以客户程序不需要任何修改(偶尔因为版本问题,有可能会进行适应性调整)。
  3. 如果发现有新的相同服务接口的 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;

            #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,怎么改进呢?回忆我们通常对连接串的处置办法:

  • 应用逻辑使用一个逻辑的数据库名称,通过一个数据访问框架调用逻辑的数据库。
  • 数据访问框架中有一个类似 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 平台开发与集成、领域设计和公钥基础环境应用。近年主要参与数据交换系统、自订制业务领域语言平台项目和信息安全类项目,工余时间喜欢旅游、写作、解趣味数学问题和烹饪。
2007-08-01 22:491668
用户头像

发布了 61 篇内容, 共 93747 次阅读, 收获喜欢 0 次。

关注

评论

发布
暂无评论
发现更多内容

这份阿里P8撰写的面试笔记就是逊了!才帮助十几个人圆了大厂梦

Java 架构 面试 程序人生 编程语言

2021年常见面试真题汇总,含了14个技术栈,已助我成功拿到腾讯offer!

Java 架构 架构师 java面试

40K成功入职:六年开发终获小米Offer(附面经+面试题+答案详解)

Java spring 程序员

引航计划|AI|优质合集手把手带你玩转AI

Nydia

AI 引航计划

JavaScript 脚本优化的 10 个技巧

devpoint

JavaScript 性能优化 10月月更

《Go 开发指南》-管理 Go 环境

看山

Effective-go 10月月更

(实战篇)漫游语音识别技术—带你走进语音识别技术的世界

攻城先森

深度学习 音视频 nlp 语音识别

太牛了!同事凭借这份Java面试题1000道手册(pdf)入职了阿里,定级P7

Java spring 程序员 架构 编程语言

后端的另一种打开方式-路由还能这么玩~

Bob

微服务 后端 网络 服务 引航计划

7. 简单生成器函数,Pool 实现多进程程序,异常管理,浏览器版本帮助手册

梦想橡皮擦

10月月更

为什么赛博朋克里总少不了日本元素?

脑极体

架构实战营 模块九 作业

脉醉

架构实战营

Apache ShenYu源码阅读系列-基于ZooKeeper的数据同步

子夜2104

Java 网关 shenyu

架构实战营 毕业总结和毕业设计项目

李东旭

「架构实战营」

毕业设计项目

燕燕 yen yen

架构是训练营

手把手带你做好项目管理|引航计划|管理

石云升

项目管理 管理 引航计划 技术专题合集

电商秒杀系统

feitian

uni-app 在mac电脑连接安卓手机进行真机调试

达摩

uni-app app调试

不愧是阿里P8架构师总结的Java面试笔记,上线仅七天,Github标星55K

Java 程序员 架构 面试 后端

【LeetCode】字符串中的单词数Java题解

Albert

算法 LeetCode 10月月更

(mode4)千万级学生管理系统考试试卷存储方案

消失的子弹

架构 云原生

如何评估一个软件的项目费用?

石云升

项目管理 管理 引航计划 内容合集 10月月更

手把手带你做好团队管理|引航计划|管理

石云升

团队管理 管理 引航计划 技术专题合集

Vue3 + TypeScript 开发实践总结

程序员海军

大前端 Vue3 引航计划

从头开始(概率)学HMM:精讲第一课-隐马尔可夫模型定义

herosunly

AI 引航计划 内容合集

架构1期模块九作业

五只羊

架构实战营

让我薪资提升了个新高度!这份阿里核心教程:理解JVM垃圾回收器手册帮了我大忙!

Java 架构 面试 程序人生 编程语言

安全逆向分析实战

网络安全学海

Linux 网络安全 信息安全 WEB安全 漏洞分析

华为大佬的“百万级”MySQL笔记,基础+优化+架构一键搞定

Java 架构 面试 程序人生 编程语言

微博评论系统架构设计

guangbao

(深入篇)漫游语音识别技术—带你走进语音识别技术的世界

攻城先森

深度学习 音视频 nlp 语音识别

实现Web Service依赖倒置_.NET_InfoQ精选文章