WCF 示例 – 第七章 – WCF 服务与 ViewModel 的解耦





5.00/5 (15投票s)
基础 WCF 分发层 - WCF 服务与 ViewModels 的解耦。
第六章 | 第八章 |
系列文章
WCF 示例是一系列文章,介绍了如何使用 WCF 进行通信和 NHibernate 进行持久化来设计和开发 WPF 客户端。该系列介绍描述了文章的范围,并从高层次讨论了架构解决方案。
章节概述
在上一章中,我们介绍了基本的 WPF 客户端。目前,视图 (View) 和模型 (Model) 几乎准备就绪,但 ViewModel 并没有提供调用服务方法的任何机制。我们之前讨论过将客户端与 WCF 分发层解耦的必要性。目标是提供一个支持快速应用程序开发 (RAD) 的开发环境,该环境能够利用业务探索,而无需部署完整生产基础设施的开销。我们希望提供一个更改成本低、部署简单的开发环境。我们不想在 IIS 中安装服务,也不想维护 SQL Server 数据库;我们正在寻找一个包含一堆文件的安装包,如果需要,甚至可以部署在内存条上。业务分析师、产品负责人、测试人员都应该能够以简单的方式执行此应用程序。
因此,我们需要抽象分发层,以便可以以透明的方式使用不同的实现。 在本章中,我们将描述契约定位器 (contract locator) 以及客户端如何使用此机制来建立一种管道模式 (pipeline pattern),从而提供我们正在寻找的所需功能。
原始实现虽然解决了客户端与服务之间的耦合问题,但需要为每种服务实现维护一组客户端类,这增加了开销。一个基于泛型类、泛型函数和匿名方法的 `ServiceAdapter` 提供了更好的实现,客户端无需实现任何额外的类来解耦服务调用。我们还在本文中引入了 `CommandDispatcher`,这在稍后将 WCF 服务引入客户端时是必需的,有关更多详细信息,请参见第 WCF 示例 - 第 XII 章 - WCF 实现 章。
本章的源代码可以在 CodePlex 更改集 93405 中找到。 eDirectory 解决方案的最新代码可以在 CodePlex 中找到。
客户端重构
对于熟悉本系列并可能对重构范围感兴趣的读者。以下屏幕截图显示了添加、删除和修改的文件
为此次重构采取了以下措施:
- 移除 WPF 项目中的 `IClientServices`
- 移除 `IContractLocator`
- ServerContractLocator 已移除 -- 需要重构测试:CustomerServiceTests
- 从 `ClientServiceLocator` 中移除 -- 需要对 `ICommandDispatcher` 进行引用
- 移除 `ClientContractLocator`
- 移除 `IContractLocator`
- 移除 WPF 项目中的 `CustomerServiceAdapter`
- 移除 WPF 项目中的 `ServiceAdapterBase` 类
- 添加 `eDirectory.Common.Message`, `ICommandDispatcher`
- 将 `CommandDispatcher` 添加到 `ClientServiceLocator`
- 创建服务器 `DirectCommandDispatcher` 类
- 创建泛型 `ServiceAdapter` 类 -- 此版本不包含异步内部调度器
- 重构测试
命令调度器 (Command Dispatcher)
`ICommandDispatcher` 接口提供了一种机制,允许客户端在调用给定服务时指示使用哪个方法和参数。客户端的唯一要求是知道契约接口,它不需要知道实现的细节。另一方面,`ICommandDispatcher` 的职责是处理服务实现并将方法结果传递给客户端。该接口是
public interface ICommandDispatcher
{
TResult ExecuteCommand<TService, TResult>(Func<;TService, TResult> command)
where TResult : IDtoResponseEnvelop
where TService : class, IContract;
}
在本章中,我们将仅讨论 `DirectCommandDispather` 实现,我们使用它将客户端直接连接到服务后端,而无需 WCF 和在 IIS 上部署应用程序。在此模式下,应用程序仅运行在单个应用程序域中。这对于测试和 RAD 非常有用。实现非常简单
public class DirectCommandDispatcher
: ICommandDispatcher
{
#region Implementation of ICommandDispatcher<TService>
public TResult ExecuteCommand<TService, TResult>(Func<TService, TResult> command)
where TResult : IDtoResponseEnvelop
where TService : class, IContract
{
01 var service = GetService<TService>();
return command.Invoke(service);
}
#endregion
02 private readonly IDictionary<Type, Type> _serviceMap = new Dictionary<Type, Type>
{
{typeof(ICustomerService), typeof(CustomerService)}
};
private TService GetService<TService>() where TService : class, IContract
{
var type = typeof(TService);
if (!_serviceMap.ContainsKey(type))
{
var msg = "Implementation for contract: {0} was " +
"not defined in the dispatcher service map";
msg = string.Format(msg, type.Name);
throw new NotImplementedException(msg);
}
03 return (TService)Activator.CreateInstance(_serviceMap[type]);
}
}
实现只是一个工厂类(第 01 行),它根据客户端传入的 `TService` 创建服务实例(第 03 行)。如果在服务器端添加了新契约,需要记住的是在第 02 行中添加服务到映射表中。
在第 XII 章 - WCF 实现 中,我们将讨论 `CommandDispatcher` 的 WCF 实现。这是生产实现,在某种程度上比我们上面讨论的更简单,因为它在向解决方案添加契约时不需要任何修改。
客户端服务定位器 (Client Service Locator)
对于某些客户端服务,在整个客户端应用程序的生命周期内使用单个实例就足够了,也是推荐的。我通常喜欢创建一个服务容器,代码可以轻松从中获取这类资源,而无需暴露 DI 容器。有几种方法可以提供此功能,并且人们有不同的意见。在这种情况下,我们使用定位器模式与 DI 结合,以在运行时动态解析服务实现。有关 DI 的更多信息,请参阅第 X 章 - 使用 Spring.Net 进行依赖注入。
`ClientServiceLocator` 的实现非常直接;目前,我们只公开 `ICommandDispacher` 服务,但在本系列结束时,这个定位器将公开另外两个服务。
public class ClientServiceLocator
{
static readonly Object LocatorLock = new object();
private static ClientServiceLocator InternalInstance;
private ClientServiceLocator() { }
public static ClientServiceLocator Instance()
{
if (InternalInstance == null)
{
lock (LocatorLock)
{
// in case of a race scenario ... check again
if (InternalInstance == null)
{
InternalInstance = new ClientServiceLocator();
}
}
}
return InternalInstance;
}
public ICommandDispatcher CommandDispatcher { get; set; }
}
服务适配器 (Service Adapter)
当 ViewModels 想要调用服务方法时,它们会使用 `ServiceAdapter`。这是一个泛型类,因此 ViewModel 在创建实例时也会确定 `ServiceAdapter` 将处理哪个服务契约,例如
01 var customerServiceAdapter = new ServiceAdapter<ICustomerService>();
var dto = new CustomerDto
{
FirstName = "Joe",
LastName = "Bloggs",
Telephone = "9999-8888"
};
02 var customerInstance =
customerServiceAdapter.Execute(s => s.CreateNewCustomer(dto));
在第 01 行,为 `ServiceAdapter` 创建了一个新实例,并传入了 `ICustomerService` 契约接口。此机制将有助于在第 02 行调用服务方法。要执行该方法,我们只需使用 `Execute` 方法,这要求我们传递一个函数表达式,该表达式接受服务实例(这是 `CommandDispatcher` 的职责)并执行其方法之一。返回值始终是 `IDtoResponseEnvelop` 的实例,它是服务方法的結果。
我们构建 `ServiceAdapter` 和 `CommandDispatcher` 的方式为开发人员提供了一种强类型机制,供 ViewModels 调用方法和传递输入参数。它类似于我们将在本系列稍后看到的 `RepositoryLocator`,但泛型 `ServiceAdapter` 提供了处理不同服务契约的附加功能。值得注意的是,`ServiceAdapter` 不需要服务实现,而且它不负责创建/维护服务实例,`CommandDispatcher` 负责这方面。该类的主要目的是以通用的方式管理业务警告/消息。实现如下:
public class ServiceAdapter<TService>
where TService:class, IContract
{
public TResponse Execute<TResponse>(Func<TService, TResponse> command)
where TResponse : IDtoResponseEnvelop
{
01 var dispatcher = ClientServiceLocator.Instance().CommandDispatcher;
02 var result = dispatcher.ExecuteCommand(command);
if (result.Response.HasWarning)
{
03 // TODO: Implement Business Warning processing -- See Chapter XIV
}
if (result.Response.HasException)
{
04 // TODO: Implement Business Exception processing -- See Chapter XIV
}
return result;
}
}
在第 01 行,我们使用新的 `ClientServiceLocator` 来获取 `CommandDispatcher`,然后在第 02 行,命令被传递给调度器;请注意 `ServiceAdapter` 完全不知道正在使用哪个服务实现。
这不是该类的最终版本;在第 XIV 章 - 验证和异常处理 中,我们将在此类中讨论业务警告和异常的实现,第 03 行和第 04 行。另外,在第 XII 章 - WCF 实现 中,介绍了调用服务的更好异步机制。
测试重构
我们已经有一套测试来从服务器端执行我们的服务。这些测试已被重构为使用 `ServiceAdapter`。这样,我们就可以测试新基础设施。我将代码留给读者自行检查,看看这些测试是如何实现的,它们提供了调用 `ServiceAdapter` 的全面示例。
章节总结
在本章中,我们为客户端端的分发层设置了基线。我们的设计提供了 ViewModel 类与我们在本章开头提到的服务之间所需的解耦。通过对服务测试进行一些小的更改,我们还演示了客户端如何轻松地调用服务器端服务。这种模型对于提供可插拔的架构至关重要,使我们能够在业务探索阶段部署我们的应用程序,而无需使用 WCF 来简化此时的部署过程。
我们尚未讨论 WCF 实现;这方面将在本系列的后续章节中介绍。然而,正如我们将看到的,一旦我们有了本章介绍的模式,WCF 实现相对简单。请参阅第 XII 章 - WCF 实现。
在 下一章 中,我们将看到命令如何在前端实现,以及 ViewModel 类如何使用本章提到的 `ServiceAdapter` 调用服务。我们离拥有一个全面的客户端和服务器端基础设施已经非常近了,可以开始向我们的客户部署解决方案的版本了。