WCF for the Real World, Not Hello World
WCF 用于真正的 RAD(
引言
想必你已经创建了几个商业 WCF 项目。如果没有,建议你先创建几个 Hello World
项目,以便在 WCF 中获得实践经验。网上有大量关于创建 Hello World
WCF 项目的教程,例如 Implementing a Hello World WCF Project,以及 Implementing a WCF Service in the Real World。本文旨在演示如何在企业环境中通过团队合作高效有效地管理 WCF 服务开发,以快速开发和交付体面的 Web 服务。
Hello World
你知道 WCF 有多好,使用“WCF 服务应用程序”模板构建 Web 服务有多方便。只需点击几下鼠标,VS IDE 就会为你创建一套骨架代码,让你能够立即着手编写业务功能代码,正如你在熟悉的以下截图中看到的。
对于小型项目,例如 WCF 模块中的代码行数少于 2000 行,该模板提供了不错的 RAD 支持。在我早期编写 WCF 代码时也使用了这些模板。然而,模板生成的代码在整个 SDLC 的生产力方面存在一些漏洞。
- 将
抽象接口
和具体实现放在同一个包(程序集)中是不好的做法。 - 服务协定和数据协定没有定义命名空间。
漏洞 1 破坏了抽象和关注点分离,而这是管理复杂性的关键。
如果不显式定义命名空间,将使用默认命名空间,即操作协定的 http://tempuri.org/
,数据协定的 http://schemas.datacontract.org/
,第二个 URL 由 Microsoft 拥有。虽然这听起来微不足道,但这对于你的公司来说不是一个好宣传,毕竟你的公司显然不属于 Microsoft。此外,当你需要处理大量数据协定时,早晚会遇到瓶颈。例如,如果你有超过一百个数据协定都存在于同一个默认命名空间 http://schemas.datacontract.org/
中,程序员很有可能会创建两个名为“CompositeType
”或类似的协定,那么这两个数据协定可能很难存在于同一个 AppDomain
中,因为同一架构命名空间中的数据类型很可能会被转换为同一 CLR namespace
中的类。如果 BizTalk 或其他服务总线实现涉及系统集成,而这是一个企业架构的信息中心,情况尤其如此。不显式定义命名空间就是用未来的麻烦,尤其是高昂的维护成本来换取便利。
现实世界
在企业环境中,WCF 是一个强大而方便的框架,用于封装新的业务组件和遗留应用程序。整个开发工作的复杂性很快就需要高度关注点分离和职责分离到以下领域:
- 接口设计
- 实现
- 测试
- 配置
- 客户端编程
WCF 为这些分离提供了固有的支持。出于某些原因,VS 附带的那些模板隐藏并限制了 WCF 的这种强大功能,尽管它们足以让你尽快交付你的第一个 WCF 解决方案,并赢得老板的掌声。然而,如果你想高效地交付优雅的复杂问题解决方案,最好遵循 SOLID OOD 原则,并尽可能地分离关注点。否则,当项目需要演进并解决更复杂的问题时,你将不得不更辛苦地工作,而不是更聪明地工作。
在典型的企业应用程序中,具有不同生命周期的类最好保留在不同的包、不同的 Visual Studio 解决方案,甚至不同的版本控制存储库中。为这种安排进行规划对于提高可维护性和灵活性至关重要,以降低成本并提高生产力和质量。此外,还可以缩短构建时间。
下面的截图说明了一个简单的实际项目基本结构。
创建仅包含契约的项目 RealWorldService
namespace Fonlow.Demo.RealWorldService
{
[ServiceContract(Namespace = "http://www.fonllow.com/demo/RealWorldService/2012/08")]
public interface IService1
{
[OperationContract]
[FaultContract(typeof(Evil666Error))]
string GetData(int value);
[OperationContract]
CompositeType GetDataUsingDataContract(CompositeType composite);
}
[DataContract(Namespace = "http://www.fonllow.com/demo/RealWorldService/Data/2012/08")]
public class CompositeType
{
[DataMember]
public bool BoolValue
{
get;
set;
}
[DataMember]
public string StringValue
{
get;
set;
}
}
[DataContract(Namespace = "http://www.fonllow.com/demo/RealWorldService/Data/2012/08")]
public class Evil666Error
{
[DataMember]
public string Message { get; set; }
}
}
该项目还包含一个用于生成 WSDL 的批处理文件。
cd %~dp0
:: generate wsdl. Run this when there's a breaking change in the interface.
You still need to be careful about interface versioning.
"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\NETFX 4.0 Tools\
svcutil.exe" bin\Debug\Fonlow.RealWorldService.dll /directory:..\deployment
创建实现项目 RealWorldImp
namespace Fonlow.Demo.RealWorldService
{
public class Service1 : IService1
{
public string GetData(int value)
{
if (value == 666)
throw new FaultException<Evil666Error>(
new Evil666Error() { Message = "Hey, this is 666." });
return string.Format("You entered: {0}", value);
}
public CompositeType GetDataUsingDataContract(CompositeType composite)
{
if (composite == null)
{
throw new ArgumentNullException("composite");
}
if (composite.BoolValue)
{
composite.StringValue += "Suffix";
}
return composite;
}
}
}
创建单元测试项目 TestRealWorld
[TestMethod]
public void TestGetData()
{
IService1 service = new Service1();
Assert.IsTrue(service.GetData(1234).Contains("1234"));
}
将服务函数作为进程内函数进行单元测试会更方便。
使用 IIS 创建 RealWorld Web 服务
步骤
- 在 C:\inetpub\wwwroot\RealWorld 中创建一个 Web 站点 https://:8998,其应用程序池为 .NET 4。
- 将项目
RealWorldImp
的程序集复制到 Web 站点的 bin 目录
步骤 2 可以通过项目 RealWorldImp
的生成后事件来完成。
提示
如果你不喜欢像 8998 这样的端口号,而是想使用 http://MyService.localhost
,你可以修改你的本地 HostFile。如果你从未这样做过,请谷歌搜索“Windows HostFile”。
创建客户端 API 项目 RealWorldServiceClientApi
该类库项目应包含引用 System.ServiceModel
和 System.Runtime.Serialization
。
该项目以及客户端配置的代码由一个名为 CreateClientApi.bat 的批处理文件生成。
cd %~dp0
rem generate client proxy classes with the wsdl
"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\NETFX 4.0 Tools\svcutil.exe"
..\deployment\*.wsdl ..\deployment\*.xsd /language:C#
/n:http://www.fonllow.com/demo/RealWorldService/2012/08,
Fonlow.RealWorldService.Clients /n:http://www.fonllow.com/demo/RealWorldService/Data/2012/08,
Fonlow.RealWorldService.ClientData /o:RealWorldClientApiAuto.cs /config:appAuto.config
客户端 API 程序集和配置文件可以随后被你的客户端程序使用,包括集成测试套件。
提示
如果你的 Web 服务将由使用 .NET 进行软件开发的其他 IT 供应商使用,你可以向他们提供客户端 API,以便他们能够快速开发使用你的 Web 服务的应用程序。
请注意,我将那些生成的文件命名为 Auto 后缀,以便在运行某些静态代码分析工具(如 Source Monitor)时可以排除它们。
创建集成测试项目 TestRealWorldIntergration
该项目是客户端 API 的第一个使用者。
const string realWorldEndpoint = "DefaultBinding_RealWorld";
[TestMethod]
public void TestGetData()
{
using (Service1Client client = new Service1Client(realWorldEndpoint))
{
Assert.IsTrue(client.GetData(1234).Contains("1234"));
}
using (Service1Client client = new Service1Client(realWorldEndpoint))
{
try
{
Assert.IsTrue(client.GetData(666).Contains("1234"));
Assert.Fail("Expect fault");
}
catch (FaultException<Evil666Error> e)
{
Assert.IsTrue(e.Detail.Message.Contains("666"));
}
}
}
提示:Hello World 和 Real World 的源代码 都在这里。
这个 Real World 服务有多真实?
这个 Real World 服务的构建没有利用 IDE 向导提供的许多便捷功能,并且需要一些额外的手动步骤来设置项目结构。有什么好处呢?
请仔细考虑,在一个体面的 Web 服务开发项目中,大部分项目时间都花在分析、接口设计、实现和测试上。向导节省的时间在非琐碎的项目中非常微不足道,然而,“糟糕”的结构带来的成本将是长远的,并很快会困扰你。将来,我可能会展示一些负面的案例研究,说明使用 WCF 的糟糕软件工程实践如何导致低软件开发生产力和低服务质量。
在这里,我将尝试解释如何使用 WCF 建立一个积极的 Web 服务开发体验。
将接口和实现分离到两个程序集中
我相信你在软件开发人员的职业生涯中已经多次听说并研究过关注点分离。为什么要关注点分离?
关注点分离是一门计算机科学原理,它提倡将应用程序或用例的职责分散到多个组件中,每个组件都有一个独特的、小的职责集合。
在我看来,这是利用我们有限的大脑容量来管理复杂和易变的需求,以便我们能够设计出可行的、可维护的解决方案来应对复杂和动态的问题。并且相应的团队开发协作可以更有效率。相比之下,计算机并不关心关注点分离,软件开发中松耦合的组件最终都会在运行时粘合在一起。
将抽象和具体实现放在同一个包中是一个显著的设计上的瑕疵。为什么?
抽象和实现的生命周期往往大不相同。抽象通常是稳定的,一旦定义,很少或根本不应更改,例如,一旦发布了接口就不应更改。实现则会受到改进和错误修复的影响。
从定义契约的程序集中生成 WSDL
定义契约的程序集(C# 中的接口)包含足够的信息来生成 WSDL(XML 中的接口),并且配置为发布 WSDL 的 Web 服务会简单地从程序集中读取信息并实时生成 WSDL。因此,在获取 WSDL 之前,你不必先运行 Web 服务。
在一个大型分布式计算项目中,通常希望在服务实现完成之前就开始客户端编程。有了 WSDL,你可以使用 .NET SDK 来生成 Web 服务的模拟实现。客户端程序员随后可以使用模拟 Web 服务来构建和测试客户端程序。而你作为服务开发者可以使用模拟服务代码的副本开始实现 Web 服务。请注意,WCF 服务的客户端程序可能用 Java 或 PHP 编写。Java 程序员和 PHP 程序员随后可以导入 WSDL 来生成客户端代理类。
不使用 VS 的内置 Web 服务器
你可能听说过在尽可能接近生产环境的环境中进行测试。既然我们要在 IIS 中托管服务,为什么不在所有时间都用 IIS 进行测试呢?
你将清楚地了解你的服务是如何执行的。此外,请考虑以下场景。
你使用 VS IDE 开发和测试了一个 Web 服务,然后将包交给系统管理员,由他安装和测试另一个环境。这种情况并不少见:管理员回来告诉你“它不起作用”。你很困惑,“它在我电脑上能正常工作,你看,客户端和服务和谐地完美运行”。而管理员可能会回应:“我们在暂存环境中运行 Visual Studio 吗?”
如果你始终在开发机上使用 IIS 进行测试,你可能会说:“它在我的 IIS 实例上能工作,我们来看看你的 IIS 和我的 IIS 有什么区别?”你认为这听起来会比上面那个情况让系统管理员更舒服吗?
顺便说一句,在 VS 2012、2013 和 2015 中,内置的 IIS Express Web 服务器更像是一个 IIS 服务器。因此,使用 IIS Express 进行测试可能足以避免意外。总之,在开发机上使用 IIS Express 进行方便的集成测试,以及在 IIS 上进行实际场景测试都是不错的选择。
单元测试
还需要我多解释吗?
这里可能不是解释单元测试好处的好地方。我只想说没有单元测试有多糟糕。如果没有尽可能多地覆盖你的服务实现,你将花费更多的时间通过将代码附加到托管进程来调试。
集成测试
这里,我使用 MS Test 作为测试平台。你可以使用 NUnit、xUnit.NET 或其他测试平台框架来构建自动集成测试。即使你的单元测试已经尽可能多地覆盖了实现,Web 服务的一些特性仍然无法通过单元测试进行测试,例如,服务行为和故障协定等。集成测试本质上是 Web 服务的一个客户端程序,通常通过 HTTP 与服务进行通信。
根据你公司的系统配置,使用 NUnit 或 xUnit.NET 作为测试平台来开发集成测试可能很方便。因此,在系统管理员安装完服务后,管理员可以立即运行测试来验证部署,然后再将服务开放给业务使用。
客户端 API
你可以在 MSDN 或互联网上找到的典型 WCF 教程会在消费者项目中添加服务引用,指向正在运行的 Web 服务或静态 WSDL 文件的 WSDL,IDE 会在消费者项目内创建客户端 API 代码。
通过构建一个客户端 API 程序集,多个消费者项目可以共用同一个客户端 API 库,避免了在每个消费者项目中生成客户端 API 代码的麻烦,并防止了对 Web 服务引用不一致的尴尬。Amazon 和 Google 等公司通常会提供 .NET、Java 和 PHP 的客户端 API 库,以及发布 WSDL。因此,这是在著名软件开发公司中久经考验的常见做法。
除了减少生成的重复代码外,你还可以更好地控制客户端代理类的命名空间。这种做法在需要服务版本控制时尤为有益。
如果 Web 服务的互操作性是需求的一部分,你可以使用其他软件开发平台(如 NetBean)从 WSDL 创建客户端代码。事实上,我见过生产环境中用 WCF 编写的遗留 Web 服务无法被非 .NET 平台使用,然后支持新型客户端程序的成本非常高。
摘要
Visual Studio 的 WCF 模板和相应的 Hello World 教程对于以下场景已经足够了:
- Hello World 项目
- 原型
- 小型公司由小型消费者使用的小型项目
- 没有 QA 的合同工作
对于大型复杂项目,IDE 提供的那些快捷方式或便利措施很快就会成为一个负担,带来的麻烦会比好处多。我们最好“编程到 WCF 中”,而不是“在 Visual Studio WCF 中编程”。你听说过“编程到你的语言,而不是在语言中”吗?
请注意,本文不是关于每个 WCF 项目的模板。你真的需要记住创建实际 WCF 项目的所有这些步骤吗?
我不需要。然而,我已经基于这些设计原则或实践养成了习惯:
- 关注点分离。特别是,将抽象和实现分离。在代码层面,将它们放入不同的 VS 项目。
- 理解基础知识,尊重相关标准和约定
- 为快速变化的需求做好准备
- 尽早测试
许多人认为使用 WCF 是开发 Web 服务的一种 RAD 方法,因此跳过了对基础知识(不仅是 WCF 的基础,还有 Web 服务的基础)的理解。不幸的是,这种做法总是导致缓慢且充满 Bug 的应用程序开发:快速仅限于编码的开始阶段。人们可能已经快速开发了业务应用程序功能,但 RAD 常常变成了粗糙的应用程序交付,由于技术债务而付出高昂代价。
在现实世界中,业务/功能需求不断变化。问题或市场需求不断演变。随着时间的推移,我们对问题或需求的理解也在不断演进,即使面对相同的问题和需求。一个好的设计或好的实践是通过最简单的解决方案来解决复杂的事情,同时通过敏捷实践来应对易变的需求。
WCF 的好处在于它易于实践解耦,并通过配置在部署时实现集成。软件开发需要解耦以提高生产力、质量、灵活性和可维护性,而系统集成则会将组件绑定在一起,以根据多种环境因素提供服务。WCF 可以兼顾这两种情况,前提是你理解 Web 服务和 OOD 的基本知识。
本文旨在为您学习如何使用 WCF 管理复杂的分布式计算项目提供一个起点。