第6章 服务模式 在 .NET 中实现 Service Interface

mac2022-06-30  120

上下文

您 的应用程序部署在 Microsoft Windows? 操作系统上。您决定将应用程序的某一块功能作为 ASP.NET Web Service 公开。互操作性是一个关键问题,因此您无法使用仅在 Microsoft .NET Framework 中出现的复杂数据类型。

背景

通 常,当您将音频光盘 (CD) 插入计算机时,您用来播放 CD 的程序会显示与音乐有关的各种信息。此信息可能包括曲目信息、封面画、评论等。为了演示 Service Interface 模式的实现,下面以此为例来实现一个 ASP.NET Web Service。

实现策略

Service Interface 描述了接口机制与应用逻辑的分离方法。接口负责实现并执行将要公开的服务的合约,而应用逻辑则负责以特定方式实现接口所使用的业务功能。本示例使用 ASP.NET Web Service 来实现服务接口。

注意:此处显示的应用逻辑是 Table Data Gateway 模式的示例。在典型的应用程序中,该实现还要提供一些附加的业务功能。为了重点讲述 Service Interface,本示例忽略了这些附加业务功能。

Service Interface 实现

ASP.NET Web Service 用于实现 Service Interface。通过将服务接口实现为 Web Service,任意数目的全异系统都能使用 Internet 标准(如 XML、SOAP 和 HTTP)来访问这部分功能。在创建用于支持应用程序互操作性的基础结构方面,Web Service 严重依赖于 XML 以及其他 Internet 标准的接受程度。

由于焦点在于服务使用者与提供者之间的互操作性,因 此您不能依赖在不同的平台上可能有、也可能没有的复杂类型。因此,您需要定义一个提供互操作性的合约。下面描述的方法包括使用 XML 架构定义数据传输对象、使用平台特有工具生成数据传输对象,然后依赖该平台实现使用数据传输对象的服务接口代码。这并不是唯一可以使用的方法。.NET Framework 可以自动生成所有功能块。但是,在一些情况下,它所生成的服务接口所具有的互操作不是很好。另一方面,您可以使用 Web Services 描述语言 (WSDL) 和 XML 架构来指定接口,然后使用 wsdl.exe 实用程序为您的应用程序生成服务接口。

合约

Service Interface 中指出,存在允许服务提供者与服务使用者进行互操作的合约。将该合约实现为 ASP.NET Web Service 需要完成三个方面的工作:

指定 XML 架构。在服务使用者与提供者之间传输的数据的定义是使用 XML 架构来指定的。服务的输入是 long 类型的简单变量;因此此方案不需要架构,因为简单类型已构建到 SOAP 规范中。但是,Web Service 的返回类型不是简单类型,因此必须使用 XML 架构来指定此类型。在本示例中,架构包含在 Recording.xsd 文件中。数据传输对象。.NET framework 有一个名为 xsd.exe 的工具,只要为该工具指定一个 XML 架构,它就可以生成由实现 Web Service 的代码所使用的数据传输对象。在本示例中,数据传输对象的名称是 Recording,并且包含在 Recording.cs 文件中。服务接口实现。这是一个类,该类继承自 System.Web.Services.WebServices,并指定了至少一个标记有 [WebMethod] 属性的方法。在本示例中,该类的名称为 RecordingCatalog,并且包含在RecordingCatalog.asmx.cs 文件中。该类负责发出对服务实现的调用,并负责将服务实现的输出转换为 Web Service 将使用的格式。转换数据的功能封装在名为 RecordingAssembler 的类中,并包含在RecordingCatalog.asmx.cs 文件中。该类是组装器的一个示例,而组装器是 Mapper 模式的一个变体。[Fowler03]

下图描绘了实现服务接口的各个类之间的关系。

图 1 服务接口类

Recording.xsd

将要传输到客户端的信息的定义是使用 XML 架构指定的。下列构架定义了两种复杂类型:Recording 和 Track。

<?xml version="1.0" encoding="utf-8" ?> <xs:schema xmlns:tns="http://msdn.microsoft.com/practices" elementFormDefault="qualified" targetNamespace="http://msdn.microsoft.com/patterns" xmlns:xs="http://www.w3.org/2001/XMLSchema">    <xs:element name="Recording" type="tns:Recording" />    <xs:complexType name="Recording">       <xs:sequence>          <xs:element minOccurs="1" maxOccurs="1" name="id" type="xs:long" />          <xs:element minOccurs="1" maxOccurs="1" name="title" type="xs:string" />          <xs:element minOccurs="1" maxOccurs="1" name="artist" type="xs:string" />          <xs:element minOccurs="0" maxOccurs="unbounded" name="Track" type="tns:Track" />       </xs:sequence>    </xs:complexType>    <xs:complexType name="Track">       <xs:sequence>          <xs:element minOccurs="1" maxOccurs="1" name="id" type="xs:long" />          <xs:element minOccurs="1" maxOccurs="1" name="title" type="xs:string" />          <xs:element minOccurs="1" maxOccurs="1" name="duration" type="xs:string" />       </xs:sequence>    </xs:complexType> </xs:schema>

Recording 类型有 ID、艺术家、标题以及大量的 Track 类型。 Track 类型还有 ID、标题以及持续时间元素。

Recording.cs

前面已提到,.NET Framework 有一个 xsd.exe 命令行工具,该工具接受 XML 架构作为输入,并输出可以在程序中使用的类。所生成的类用作 Web Service 的返回值。用于生成 Recording.cs 类的命令如下:

xsd /classes Recording.xsd   The output that was produced by running this command is shown below:  //------------------------------------------------------------------------------ // <autogenerated> //     This code was generated by a tool. //     Runtime Version: 1.0.3705.288 // //     Changes to this file may cause incorrect behavior and will be lost if  //     the code is regenerated. // </autogenerated> //------------------------------------------------------------------------------ //  // This source code was auto-generated by xsd, Version=1.0.3705.288. //  using System.Xml.Serialization; /// <remarks/> [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://msdn.microsoft.com/practices")] [System.Xml.Serialization.XmlRootAttribute(Namespace="http://msdn.microsoft.com/practices", IsNullable=false)] public class Recording {     /// <remarks/>     public long id;     /// <remarks/>     public string title;     /// <remarks/>     public string artist;     /// <remarks/>     [System.Xml.Serialization.XmlElementAttribute("Track")]     public Track[] Track; } /// <remarks/> [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://msdn.microsoft.com/practices")] public class Track {     /// <remarks/>     public long id;     /// <remarks/>     public string title;     /// <remarks/>     public string duration; }

RecordingCatalog.asmx.cs

在定义该类之后,您需要实现实际的 Web Service 实现。该类封装了所有 Service Interface 行为。要公开的服务通过使用 [WebMethod] 属性来显式定义。

[WebMethod] public Recording Get(long id) { /*  */ }

Get 方法接受 id 作为输入,并返回 Recording 对象。按照 XML 架构的说明,Recording 还可以包括许多 Track 对象。

下面是具体的实现。

using System.ComponentModel; using System.Data; using System.Web.Services; namespace ServiceInterface {    [WebService(Namespace="http://msdn.microsoft.com/practices")]    public class RecordingCatalog : System.Web.Services.WebService    {       private RecordingGateway gateway;        public RecordingCatalog()       {          gateway = new RecordingGateway();             InitializeComponent();       }       #region Component Designer generated code          //        #endregion       [WebMethod]       public Recording Get(long id)       {          DataSet ds = RecordingGateway.GetRecording(id);          return RecordingAssembler.Assemble(ds);       }    } }

Get 方法通过调用 RecordingGateway 获得一个 DataSet。然后,它通过调用 RecordingAssembler.Assemble 方法将 DataSet 转换为所生成的 Recording 和 Track 对象。

RecordingAssembler.cs

之所以将该类作为服务接口的一部分,是因为需要将应用逻辑的输出转换为要通过 Web Service 发送出去的对象。RecordingAssembler 类负责将服务实现的返回类型(本例中为 ADO.NET DataSet)转换为上一步所生成的 Recording 和 Track 类型。

using System; using System.Collections; using System.Data; public class RecordingAssembler {    public static Recording Assemble(DataSet ds)    {       DataTable recordingTable = ds.Tables["recording"];       if(recordingTable.Rows.Count == 0) return null;       DataRow row = recordingTable.Rows[0];       Recording recording = new Recording();       recording.id = (long)row["id"];       string artist = (string)row["artist"];       recording.artist = artist.Trim();       string title = (string)row["title"];       recording.title = title.Trim();       ArrayList tracks = new ArrayList();       DataTable trackTable = ds.Tables["track"];       foreach(DataRow trackRow in trackTable.Rows)       {          Track track = new Track();          track.id = (long)trackRow["id"];          string trackTitle = (string)trackRow["title"];          track.title = trackTitle.Trim();          string duration = (string)trackRow["duration"];          track.duration = duration.Trim();          tracks.Add(track);       }       recording.Track = (Track[])tracks.ToArray(typeof(Track));       return recording;    } }

Assembler 类通常要复杂一些。它们的工作是从一种表示法转换为另一种表示法,因此它们通常都很简单,但总是依赖于两种表示法。这种依赖性使得它们容易受这两种表示法变化的影响。

虽 然组装器很有用,但是如果已有满足您需要的可选组装器,那么您不必自己创建。但如果需要,您可以使用 XML 序列化功能来创建一个 XMLDataDocument 实例,再将它与 DataSet 关联,然后返回 XML。

应用逻辑

对于大多数企业应用而言,本示例中的应用逻辑可能过于简单。这是因为使用此模式重点是为了实现Service Interface ,以便更完整地展示实现部分,而不是作为一个代表性示例。此实现使用 Table Data Gateway 来从数据库中检索数据。Table Data Gateway 类(称为 RecordingGateway)检索 recording 记录以及与 recording 关联的 track 记录。结果以单个 DataSet 的形式返回。有关所使用的数据库架构以及 DataSet 的详细讨论,请参阅在 .NET 中使用 DataSet 实现 Data Transfer Object。

RecordingGateway.cs

该类用两个结果集填充 DataSet:recording 和 track。客户端传递所需要的 recording 记录的 ID。该类在数据库中执行两个查询以填充 DataSet。最后,该类定义 recording 与其 track 记录之间的关系。

using System; using System.Collections; using System.Data; using System.Data.SqlClient; public class RecordingGateway {    public static DataSet GetRecording(long id)    {       String selectCmd =           String.Format(          "select * from recording where id = {0}",          id);       SqlConnection myConnection =           new SqlConnection( "server=(local);database=recordings;Trusted_Connection=yes");       SqlDataAdapter myCommand =  new SqlDataAdapter(selectCmd, myConnection);       DataSet ds = new DataSet();       myCommand.Fill(ds, "recording");       String trackSelect =           String.Format(          "select * from Track where recordingId = {0} order by Id",          id);       SqlDataAdapter trackCommand =  new SqlDataAdapter(trackSelect, myConnection);       trackCommand.Fill(ds, "track");       ds.Relations.Add("RecordingTracks",          ds.Tables["recording"].Columns["id"],          ds.Tables["track"].Columns["recordingId"]);       return ds;    } }

注意:这个示例并不是向 DataSet 中填入数据的唯一方式。从数据库中检索此数据的方法有很多种。例如,可以使用存储过程。

测试

单元测试的重点是测试该实现的内部方面。可以用一个单元测试来测试从数据库 (RecordingGatewayFixture) 中检索信息,而其他测试则用来测试 DataSet 到 Recording 和 Track 对象 (RecordingAssemblerFixture) 的转换。

RecordingGatewayFixture

RecordingGatewayFixture 类用于测试 RecordingGateway 的输出(这是一个 DataSet)。通过该测试可以验证在给定 ID 的情况下是否从数据库中检索到包含 recording 和 track 信息的正确 DataSet 。

using NUnit.Framework; using System.Data; [TestFixture] public class RecordingGatewayFixture {    private DataSet ds;    private DataTable recordingTable;     private DataRelation relationship;    private DataRow[] trackRows;     [SetUp]    public void Init()    {       ds = RecordingGateway.GetRecording(1234);       recordingTable = ds.Tables["recording"];       relationship = recordingTable.ChildRelations[0];       trackRows = recordingTable.Rows[0].GetChildRows(relationship);    }    [Test]    public void RecordingCount()    {       Assertion.AssertEquals(1, recordingTable.Rows.Count);    }    [Test]    public void RecordingTitle()    {       DataRow recording = recordingTable.Rows[0];       string title = (string)recording["title"];       Assertion.AssertEquals("Up", title.Trim());    }    [Test]    public void RecordingTrackRelationship()    {       Assertion.AssertEquals(10, trackRows.Length);    }    [Test]    public void TrackContent()    {       DataRow track = trackRows[0];       string title = (string)track["title"];       Assertion.AssertEquals("Darkness", title.Trim());    }    [Test]    public void InvalidRecording()    {       DataSet ds = RecordingGateway.GetRecording(-1);       Assertion.AssertEquals(0, ds.Tables["recording"].Rows.Count);       Assertion.AssertEquals(0, ds.Tables["track"].Rows.Count);    } }

RecordingAssemblerFixture

第二个配件通过测试 DataSet 到 Recording 和 Track 对象的转换来测试 RecordingAssembler 类。

using NUnit.Framework; using System.Data; using System.IO; using System.Xml; [TestFixture] public class RecordingAssemblerFixture {    private static readonly long testId = 1234;    private Recording recording;    [SetUp]    public void Init()    {       DataSet ds = RecordingGateway.GetRecording(1234);       recording = RecordingAssembler.Assemble(ds);    }    [Test]    public void Id()    {       Assertion.AssertEquals(testId, recording.id);     }    [Test]    public void Title()    {       Assertion.AssertEquals("Up", recording.title);    }    [Test]    public void Artist()    {       Assertion.AssertEquals("Peter Gabriel", recording.artist);    }    [Test]    public void TrackCount()    {       Assertion.AssertEquals(10, recording.Track.Length);    }    [Test]    public void TrackTitle()    {       Track track = recording.Track[0];       Assertion.AssertEquals("Darkness", track.title);    }    [Test]    public void TrackDuration()    {       Track track = recording.Track[0];       Assertion.AssertEquals("6:51", track.duration);    }    [Test]    public void InvalidRecording()    {       DataSet ds = RecordingGateway.GetRecording(-1);       Recording recording = RecordingAssembler.Assemble(ds);       Assertion.AssertNull(recording);    } }

运行这些测试后,就可以判断是否能够正确地从数据库中检索信息,并将数据库输出转换为数据传输对象。但是,上述测试并未测试端到端功 能,也未测试所有服务接口代码。下面的示例则用于测试完整的功能。由于它验证整个接口是否按照所期望的方式工作,因此将它称为功能测试或验收测试。下面描 述的测试将从 RecordingGateway 中检索 DataSet。然后,它使用 Web Service 发出调用,以便检索完全相同的 Recording。收到结果后,它直接将两个结果进行比较。如果二者相等,则说明 Service Interface 运行正常。

注意:下面显示的仅仅是可能的验收测试的示例。您还应注意的是,还存在执行此类测试的其他方法。这仅仅是执行测试的一种方法。

AcceptanceTest.cs

下面是服务接口的一些验收测试示例:

using System; using System.Data; using NUnit.Framework; using ServiceInterface.TestCatalog; [TestFixture] public class AcceptanceTest {    private static readonly long id = 1234;     private DataSet localData;    private DataTable recordingTable;     private RecordingCatalog catalog = new RecordingCatalog();    private ServiceInterface.TestCatalog.Recording recording;    [SetUp]    public void Init()    {       // get the recording from the database       localData = RecordingGateway.GetRecording(id);       recordingTable = localData.Tables["recording"];       // get the same recording from the web service       recording = catalog.Get(id);    }    [Test]    public void Title()    {       DataRow recordingRow = recordingTable.Rows[0];       string title = (string)recordingRow["title"];       Assertion.AssertEquals(title.Trim(), recording.title);    }    [Test]    public void Artist()    {       DataRow recordingRow = recordingTable.Rows[0];       string title = (string)recordingRow["artist"];       Assertion.AssertEquals(title.Trim(), recording.artist);    }    // continued }

结果上下文

下面是将 ASP.NET Web Service 用作 Service Interface 实现的优缺点:

优点

分隔任务。将服务接口与应用逻辑分隔开来很重要,因为它们有可能独立变化。将接口部分作为 ASP.NET Web Service 实现有助于进行这种分隔。互操作性。由于接口基于 Internet 标准(如 XML 和 SOAP),这就使得客户端使用无论哪一种操作系统都可以访问 Web Service。ASP.NET Web services 和 Microsoft Visual Studio.NET。 此环境使得 Web Services 的使用非常简单。本示例中演示的 xsd.exe 工具提供了一种将 XML 架构转换为 C# 或 Microsoft Visual Basic? .NET 类的工具。为了创建 Web Service,本示例使用了 Microsoft Visual Studio? .NET 开发系统中的预定义模板并生成了 RecordingCatalog.asmx.cs 文件的大部分。

缺点

数据转换。在许多情况下,必须进行将应用逻辑表示法转换为服务接口当前所使用的表示法的数据转换。由于使用一个依赖于这两种表示法的类将导致依赖性,就使得这种转换始终会有问题。在本例中,RecordingAssembler 类依赖于由 RecordingGateway 返回的 DataSet 以及所生成的 Recording 和 Track 类。

同步。架构和所生成的代码无法自动更新。因此,对架构进行任何更改后,都需要重新运行 xsd.exe 工具来重新生成 Recording.cs 类。

转载于:https://www.cnblogs.com/ljf10223063/p/4673279.html

最新回复(0)