RIA 服务

使用 WCF RIA 服务的企业模式

Michael D. Brown

下载代码示例

PDC09 和 Mix10 上宣布了两条重大消息,分别是推出 Silverlight 4 Beta 和 RC。读到本文时,发布到网上的 Silverlight 4 完全版本已经可供下载。除广泛的打印支持外,它还支持权限升级、网络摄像头、麦克风、toast、剪贴板访问,等等。凭借其全新的功能集,Silverlight 4 作为一种多平台的丰富 UI 框架,可以从容应对与 Adobe AIR 之间的正面交锋。

尽管我对这一切确实感到兴奋,但我的主要角色是一名业务应用程序开发人员,我所关注的一点是如何使用一种简单的方法将我的业务数据和逻辑融入 Silverlight 应用程序。

对于 Silverlight 业务线应用程序,我关注的一个问题是如何连接到数据。在 Silverlight 3 中创建自己的 Windows Communication Foundation (WCF) 服务以及连接到该服务,是没有任何障碍的。但该版本还有很大的改进空间,尤其是从 ASP.NET 或桌面应用程序连接到数据时所能采取的诸多方式,有待改善。桌面应用程序和 Web 应用程序可以通过 Nhibernate、实体框架 (EF) 或原始 ADO.NET 结构直接连接到数据库,但 Silverlight 应用程序与我的数据之间却由“云”所阻隔。我将这种阻隔称为“数据鸿沟”。

跨越这道鸿沟乍一看可能非常简单。很明显,现在有很多数据丰富的 Silverlight 应用程序在某种程度上已经实现了这种跨越。但起初看似简单的任务随着您解决更多的问题后反而变得越来越复杂。如何通过网络跟踪更改?如何封装位于防火墙两端的实体内的业务逻辑?如何防止传输详细信息泄露您的业务敏感信息?

用来解决这些问题的第三方工具越来越多,但 Microsoft 也发现需要提供某种解决方案,因此推出了 WCF RIA 服务(以前称 .NET RIA 服务),简称 RIA 服务。2009 年 5 月版*《MSDN 杂志》*的“使用 Silverlight 3 构建数据驱动的开支应用程序”(msdn.microsoft.com/magazine/dd695920) 中完整介绍了 RIA 服务。从第一次受邀参与 Beta 程序开始,我便一直在关注 RIA 服务,向开发团队建言献策并学习如何在我自己的应用程序中应用该框架。

RIA 服务论坛中的一个常见问题是,RIA 服务如何与最佳实践体系结构相适应。我一直对 RIA 服务的“基于数据的窗体设计”基本功能印象深刻,但我也的确看到了改善我的应用程序体系结构的机会,因此框架问题没有影响到我的应用程序逻辑。

KharaPOS 简介

我开发了一个范例应用程序:KharaPOS,旨在针对我在本文陈述的概念提供一个相关实例。该实例是使用 RIA 服务、实体框架和 SQL Server 2008 在 Silverlight 4 中实现的销售点 (POS) 应用程序。最终目标是使该应用程序能承载于 Windows Azure 平台和 SQL Azure,但 Windows Azure 平台对 Microsoft .NET Framework 4 的支持有一点问题(甚至不支持)。 

在这个过渡期,KharaPOS 可作为一个使用 .NET Framework 4 创建实际应用程序的好例子。该项目通过 CodePlex 放在 KharaPOS.codeplex.com 中。您可以从该站点下载代码、查看文档以及加入关于开发此应用程序的讨论。

我要说明的是,对于 KharaPOS 应用程序的大部分设计与功能,我借鉴了由 Peter Coad、David North 和 Mark Mayfield 合著的《对象模型:策略、模式与应用,第 2 版》(Prentice Hall PTR,1996)。我将重点介绍该应用程序的一个子系统:目录管理系统(请参阅图 1)。

Figure 1 The Entity Data Model for Catalog Management
图 1 目录管理系统的实体数据模型

企业模式

有大量优秀著作都在讨论关于企业应用程序开发的设计模式。我常用作参考的一本书是 Martin Fowler 的《企业应用架构模式》(Addison-Wesley,2003)本书及其辅助网站 (martinfowler.com/eaaCatalog/) 精彩总结了用于开发企业业务应用程序的有用的软件模式。

Fowler 书中介绍的一部分软件模式是关于数据表示和数据操作的,有意思的是,它们与 RIA 服务同样占有一席之地。通过了解这些模式会更清楚为何能采用 RIA 服务来满足从最简单到最复杂的业务应用程序。我将讨论如下模式:

  • 窗体和控件
  • 事务脚本
  • 域模型
  • 应用服务层

我们先快速了解一下这些模式。前三者涉及到数据相关逻辑的不同处理方式。随着对它们的深入了解,开始时逻辑分散于整个应用程序并根据需要进行重复,随后逐渐转为集中和聚合。

窗体和控件

窗体和控件模式(也就是我前面提到的“基于数据的窗体设计”)将所有的逻辑放入 UI 内。乍一看,这似乎并不是好方法。但对于简单的数据输入和大纲/细节视图,这种模式是将数据从 UI 输入数据库的最简单且最直接的方法。很多框架都有对这种模式的内在支持(Ruby on Rails 基架、ASP.NET 动态数据和 SubSonic 是三个主要例子),因此这种被某些人称为“反模式”的模式确实有其存在的理由。虽然很多开发人员仅将这种“基于数据的窗体设计”方法归于初始原型,但它在最终应用程序中也有一定的用途。

无论您对其用途有何看法,“基于数据的窗体设计”的简单性和直接性是不可否认的。由于过于单调,因此这种模式不称为快速应用程序开发 (RAD)。但 WCF RIA 服务可为 Silverlight 提供 RAD。通过使用实体框架、RIA 服务和 Silverlight 设计器,可以根据数据库表通过五个步骤创建一个简单的“基于数据的窗体设计”编辑器:

  1. 创建新的 Silverlight 业务应用程序。
  2. 向创建的 Web 应用程序中添加一个新的实体数据模型 (EDM)(使用向导导入数据库)。
  3. 向引用该数据模型的 Web 应用程序中添加域服务(务必先编译该应用程序,使其正确发现 EDM)。
  4. 使用数据源面板将一个由 RIA 服务公开的实体拖放到 Silverlight 应用程序中的页面或用户控件的表面上(务必再次编译该应用程序,使其能看到新的域服务)。
  5. 添加按钮和隐藏代码,使用如下简单代码行将窗体上的更改保存到数据库:
this.categoryDomainDataSource.SubmitChanges();

现在您已经拥有一个简单的数据网格,可用于直接在表中编辑现有的行。通过再进行一些添加后,您便可以创建一个窗体来向表中添加新行。

虽然该模式屡经验证,为了展示 WCF RIA 服务的 RAD 优势,仍有必要在此介绍该模式,因为它针对使用框架进行开发的开发方式提供了一种基准。另外,如前文所述,这是一种基于 RIA 服务的应用程序内的有效模式。

建议 对于 ASP.NET 动态数据,“基于数据的窗体设计”模式应当用于简单的管理 UI(例如 KharaPOS 产品目录编辑器),这种 UI 中的逻辑简单而直接:在查找表内添加、删除和编辑行。但 Silverlight 和 RIA 服务应对的是复杂得多的应用程序,我们下面将看到这一点。

表数据网关 我刚才谈到的 RIA 服务应用程序的标准现成方法也可看作表数据网关模式的一种实现,如 Fowler 书中第 144–151 页所述。经由两层中间环节(一层是基于数据库的 EF 映射,接着的一层是基于 EF 的域服务映射),我创建了通向数据库表的一个简单网关,做法是使用基本的创建、读取、更新和删除 (CRUD) 操作返回强类型的数据传输对象 (DTO)。

从技术角度上讲,这并不够格成为纯粹的表数据网关,因为它有两个中间层。但如果以斜视方式查看,则其与表数据网关模式非常相似。老实说,讨论 RIA 服务与表数据网关模式之间的映射,需要在逻辑上有较大的跳跃,因为上面列出的所有其余模式都是数据接口模式,只有“基于数据的窗体设计”基本上是一种 UI 模式。但我感觉还是最好先介绍这种基本的情形,然后将焦点从 UI 移回到数据库。

Model-View-ViewModel (MVVM) 虽然使用“基于数据的窗体设计”来创建功能性窗体的过程比较简单,但是仍然存在某种冲突。图 2 用于类别管理的 XAML 说明了这一点。

图 2 用于类别管理的 XAML

<Controls:TabItem Header="Categories">
  <Controls:TabItem.Resources>
    <DataSource:DomainDataSource
      x:Key="LookupSource"
      AutoLoad="True"
      LoadedData="DomainDataSourceLoaded"
      QueryName="GetCategoriesQuery"
      Width="0">
      <DataSource:DomainDataSource.DomainContext>
        <my:CatalogContext />
      </DataSource:DomainDataSource.DomainContext>
    </DataSource:DomainDataSource>
    <DataSource:DomainDataSource
      x:Name="CategoryDomainDataSource"
      AutoLoad="True"
      LoadedData="DomainDataSourceLoaded"
      QueryName="GetCategoriesQuery"
      Width="0">
      <DataSource:DomainDataSource.DomainContext>
        <my:CatalogContext />
      </DataSource:DomainDataSource.DomainContext>
      <DataSource:DomainDataSource.FilterDescriptors>
        <DataSource:FilterDescriptor 
          PropertyPath="Id" 
          Operator="IsNotEqualTo" Value="3"/>
      </DataSource:DomainDataSource.FilterDescriptors>
    </DataSource:DomainDataSource>
  </Controls:TabItem.Resources>
  <Grid>
    <DataControls:DataGrid
      AutoGenerateColumns="False" 
      ItemsSource="{Binding Path=Data,
        Source={StaticResource CategoryDomainDataSource}}" 
      x:Name="CategoryDataGrid">
      <DataControls:DataGrid.Columns>
        <DataControls:DataGridTextColumn 
          Binding="{Binding Name}" Header="Name" Width="100" />
        <DataControls:DataGridTemplateColumn 
          Header="Parent Category" Width="125">
            <DataControls:DataGridTemplateColumn.CellEditingTemplate>
              <DataTemplate>
                <ComboBox 
                  IsSynchronizedWithCurrentItem="False" 
                  ItemsSource="{Binding Source=
                    {StaticResource LookupSource}, Path=Data}"  
                  SelectedValue="{Binding ParentId}" 
                  SelectedValuePath="Id" 
                  DisplayMemberPath="Name"/>
              </DataTemplate>
            </DataControls:DataGridTemplateColumn.CellEditingTemplate>
            <DataControls:DataGridTemplateColumn.CellTemplate>
              <DataTemplate>
                <TextBlock Text="{Binding Path=Parent.Name}"/>
              </DataTemplate>
            </DataControls:DataGridTemplateColumn.CellTemplate>
          </DataControls:DataGridTemplateColumn>
          <DataControls:DataGridTextColumn
            Binding="{Binding ShortDescription}"
            Header="Short Description" Width="150" />
          <DataControls:DataGridTextColumn 
            Binding="{Binding LongDescription}" 
            Header="Long Description" Width="*" />
        </DataControls:DataGrid.Columns>
    </DataControls:DataGrid>
  </Grid>
</Controls:TabItem>

数据网格中父类别对应的列是一个组合框,该组合框包含一组现有的类别,这样用户就可以按名称选择父类别,而不必记住类别的 ID。遗憾的是,当同一对象在虚拟树中加载两次时,Silverlight 不支持这种列。因此,我必须声明两个域数据源:一个用于网格,一个用于查找组合框。另外,用于管理类别的隐藏代码相当复杂(请参阅图 3)。

图 3 用于管理类别的隐藏代码

private void DomainDataSourceLoaded(object sender, LoadedDataEventArgs e)
{
  if (e.HasError)
  {
    MessageBox.Show(e.Error.ToString(), "Load Error", MessageBoxButton.OK);
    e.MarkErrorAsHandled();
  }
}

private void SaveButtonClick(object sender, RoutedEventArgs e)
{
  CategoryDomainDataSource.SubmitChanges();
}

private void CancelButtonClick(object sender, RoutedEventArgs e)
{
  CategoryDomainDataSource.Load();
}

void ReloadChanges(object sender, SubmittedChangesEventArgs e)
{
  CategoryDomainDataSource.Load();
}

我在这里不会完整介绍 MVVM,请参阅 2009 年 2 月那一期的“使用 Model-View-ViewModel 设计模式构建 WPF 应用程序”(msdn.microsoft.com/magazine/dd419663),其中就这一主题做了精彩论述。图 4 显示了一种在 RIA 服务应用程序中使用 MVVM 的方式。

图 4 通过视图模型进行类别管理

public CategoryManagementViewModel()
{
  _dataContext = new CatalogContext();
  LoadCategories();
}

private void LoadCategories()
{
  IsLoading = true;
  var loadOperation= _dataContext.Load(_dataContext.
    GetCategoriesQuery());
  loadOperation.Completed += FinishedLoading;
}

protected bool IsLoading
{
  get { return _IsLoading; }
  set
  {
    _IsLoading = value;
    NotifyPropertyChanged("IsLoading");
  }
}

private void NotifyPropertyChanged(string propertyName)
{
  if (PropertyChanged!=null)
    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}

void FinishedLoading(object sender, EventArgs e)
{
  IsLoading = false;
  AvailableCategories=
    new ObservableCollection<Category>(_dataContext.Categories);
}

public ObservableCollection<Category>AvailableCategories
{
  get
  {
    return _AvailableCategories;
  }
  set
  {
    _AvailableCategories = value;
    NotifyPropertyChanged("AvailableCategories");
  }
}

正如您所见,ViewModel 负责初始化域上下文并在发生加载时通知 UI,而且还处理来自 UI 的请求以创建新的类别、保存对现有类别的更改以及重新加载来自域服务的数据。这样便清楚地分开了 UI 与驱动 UI 的逻辑。MVVM 模式可能看似需要做更多的工作,但在您第一次需要更改“将数据引入 UI”的逻辑时,其优点就显露了出来。另外,通过将类别的加载流程转移到 ViewModel,可以显著净化视图(类似于 XAML 和隐藏代码)。

建议 为避免复杂的 UI 逻辑干扰 UI(甚至更糟的是,干扰到业务对象模型),请使用 MVVM。

事务脚本

随着您向应用程序中添加越来越多的逻辑,“基于数据的窗体设计”模式将变得愈加繁琐。由于与数据处理相关的逻辑被嵌入到 UI 中(或在 ViewModel 中,如果您执行了该步骤),因此该逻辑将分散于整个应用程序。逻辑分散的另一副作用是,开发人员可能不知道应用程序中已经存在该特定功能,因此可能导致重复。这在逻辑发生变化时会带来噩梦,因为所有位置都需要更新该逻辑(前提是已正确记录了所有实现该逻辑的位置)。

对此,事务脚本模式(请参考 Fowler 书中第 110–115 页)可以提供一定的缓解办法。使用这种模式可将管理数据的业务逻辑同 UI 分开。

根据 Fowler 的定义,事务脚本按步骤对业务逻辑进行组织,其中每个步骤只处理来自表示层的一个请求。事务脚本远远不止简单的 CRUD 操作。实际上,它们就像是坐在表数据网关前面处理 CRUD 操作。在走向极端的情况下,单独的事务脚本将处理针对数据库的每项检索和提交操作。但作为有逻辑思维的人,我们知道任何事物都有时间和空间属性。

当您的应用程序需要协调两个实体之间的交互时(例如当您创建属于不同实体类的两个实例之间的关联时),事务脚本就可以发挥其作用。例如,在目录管理系统中,我通过创建一个目录条目,表明库存中存在某个产品可供某个业务部门订购。该条目同时以内部与外部方式标识产品、业务部门、产品 SKU 以及可订购的持续时间。为了简化目录条目的创建工作,我在域服务上创建了一个方法(请参阅以下代码段),该方法提供了一个事务脚本来修改产品对于业务部门的可用性,而不必通过 UI 直接处理目录条目。

事实上,目录条目甚至不用通过域服务公开,如下所示:

public void CatalogProductForBusinessUnit(Product product, int businessUnitId)
{
  var entry = ObjectContext.CreateObject<CatalogEntry>();
  entry.BusinessUnitId = businessUnitId;
  entry.ProductId = product.Id;
  entry.DateAdded = DateTime.Now;
  ObjectContext.CatalogEntries.AddObject(entry);
  ObjectContext.SaveChanges();
}

RIA 服务在相关的实体(本例中为 Product)上生成一个函数,调用该函数即可将一条更改通知放置于对象上,而该通知将在服务器端上被解释为域服务上的方法调用;而不是采取在客户端域上下文中公开函数的做法。

对于实现事务脚本,Fowler 推荐了两种方法:

  1. 使用命令来封装操作并传递这些命令
  2. 使用一个类来承载事务脚本集合

我在这里选用了第二种方法,但完全也可以使用命令来实现。不将目录条目公开到 UI 层的好处在于,事务脚本成为创建目录条目的唯一方式。如果使用命令模式,规则将受制于约定。如果开发人员忘记了已存在某条命令,则将使您回到起始位置,并产生逻辑碎片和重复情况。

将事务脚本置于域服务上的另一个好处在于,逻辑将在服务器端执行(如前文所述)。如果您有自己独有的算法,或想确定用户未恶意操作您的数据,那么将事务脚本置于域服务上是一种不错的做法。

建议 当业务逻辑太复杂而不宜使用基于数据的窗体设计时,或希望在服务器端执行某项操作的逻辑时(或者兼具这两种情况),可使用事务脚本。

业务逻辑与 UI 逻辑 我提到几次 UI 逻辑与业务逻辑,尽管乍一看差异比较细微,但却很重要。UI 逻辑是与表示相关的逻辑,表示是指屏幕上的显示内容及显示方式(例如,用于填充组合框的项目)。而业务逻辑则是驱动应用程序本身的一种逻辑(例如,应用于在线购物的折扣)。两者都是应用程序的重要方面,当允许二者融合时将形成另一种模式,请参阅 Brian Foote 和 Joseph Yoder 的文章《Big Ball of Mud》(laputan.org/mud)。

将多个实体传递给域服务 默认情况下,只能将一个实体传递给自定义的域服务方法。例如,方法

public void CatalogProductForBusinessUnit(Product product, int businessUnitId)

在您尝试使用以下签名而非整数时将不起作用:

public void CatalogProductForBusinessUnit(Product product, BusinessUnit bu)

RIA 服务不会为该函数生成客户端代理,原因是……对了,规则就是这样。在自定义服务方法中,您只能有一个实体。这在大多数情况下都应该没有问题,因为如果您有实体,您就有它的键并可在后端再次检索它。

为了便于演示,换句话来说,就是检索实体(也许它在 Web 服务的另一端)是一项成本很高的操作。您可以告诉域服务,希望它保存指定实体的副本,如下所示:

public void StoreBusinessUnit(BusinessUnit bu)
{
  HttpContext.Current.Session[bu.GetType().FullName+bu.Id] = bu;
}

public void CatalogProductForBusinessUnit(Product product, int businessUnitId)
{
  var currentBu = (BusinessUnit)HttpContext.Current.
    Session[typeof(BusinessUnit).FullName + businessUnitId];
  // Use the retrieved BusinessUnit Here.
}

由于域服务运行在 ASP.NET 下,因此它有 ASP.NET 会话和缓存的完全访问权限,另外,也是便于您想在一段时间后从内存中自动删除对象。实际上,我正对一个项目使用此技术。在该项目中,我必须从多个远程 Web 服务检索客户关系管理 (CRM) 数据,并在统一的 UI 下显示给用户。我使用了一个显式方法,因为有些数据有缓存价值,而有些没有。

域模型

有时,业务逻辑会变得十分复杂,以至于事务脚本也无法正确管理它。通常,这种逻辑在一个或多个事务脚本中显示为复杂的分支逻辑,以说明逻辑的细微差别。应用程序不适用事务脚本的另一种情况是,该应用程序需要频繁更新以满足快速变化的业务需求。

如果您已经注意到了这类状况,那么现在可以考虑使用富域模型(请参考 Fowler 书中第 116–124 页)。到目前为止介绍的几种模式都有一个共同点:实体都只不过是 DTO — 它们不包含逻辑(某些人将这种情况视为反模式,也称为“贫血域模型”)。面向对象开发的主要优势之一是能够将数据及其关联的逻辑封装起来。富域模型通过将逻辑放回其所属的实体,充分利用了这一优势。

关于设计域模型的详细信息已超出本文的范畴。请参阅 Eric Evans 的图书《领域驱动设计:软件核心复杂性应对之道》(Addison-Wesley,2004),或前面提到的 Coad 关于对象模型的图书,了解有关此主题的详细信息。不过,我可以提供一种场景来帮助说明域模型如何在一定程度上控制此问题。

某些 KharaPOS 客户想要查看某些产品系列的历史销售情况,并按照各市场的具体情况决定是要根据给定的原因扩大产品系列(提供该系列的更多产品)、减少产品系列、全部退出市场还是要保持不变。

我已经在 KharaPOS 的另一个子系统中有了销售数据,而我所需的所有其他数据都在此处的目录系统中。我只会将只读的产品销售视图放入我们的实体数据模型中,如图 5 所示。

Figure 5 Entity Data Model Updated with Sales Data
图 5 加入销售数据后的实体数据模型

现在,我只需要将产品选择逻辑添加到域模型中。由于我是在为市场选择产品,因此我要将该逻辑添加到 BusinessUnit 类中(使用具有 shared.cs 或 shared.vb 扩展名的分部类来通知 RIA 服务您希望该函数与客户端通信)。图 6 显示了相应代码。

图 6 关于为业务部门选择产品的域逻辑

public partial class BusinessUnit
{
  public void SelectSeasonalProductsForBusinessUnit(
    DateTime seasonStart, DateTime seasonEnd)
  {
    // Get the total sales for the season
    var totalSales = (from sale in Sales
                     where sale.DateOfSale > seasonStart
                     && sale.DateOfSale < seasonEnd
                     select sale.LineItems.Sum(line => line.Cost)).
                     Sum(total=>total);
    // Get the manufacturers for the business unit
    var manufacturers =
      Catalogs.Select(c =>c.Product.ManuFacturer).
        Distinct(new Equality<ManuFacturer>(i => i.Id));
    // Group the sales by manufacturer
    var salesByManufacturer = 
      (from sale in Sales
      where sale.DateOfSale > seasonStart
      && sale.DateOfSale < seasonEnd
      from lineitem in sale.LineItems
      join manufacturer in manufacturers on
      lineitem.Product.ManufacturerId equals manuFacturer.Id
      select new
      {
        Manfacturer = manuFacturer,
          Amount = lineitem.Cost
      }).GroupBy(i => i.Manfacturer);
    foreach (var group in salesByManufacturer)
    {
      var manufacturer = group.Key;
      var pct = group.Sum(t => t.Amount)/totalSales;
      SelectCatalogItemsBasedOnPercentage(manufacturer, pct);
    }
  }

  private void SelectCatalogItemsBasedOnPercentage(
    ManuFacturer manufacturer, decimal pct)
  {
     // Rest of logic here.
  }
}

对产品执行自动选择来使产品延续一个季度的操作非常简单,只需在 BusinessUnit 上调用这个新函数,然后在 DomainContext 上调用 SubmitChanges 函数。在将来,如果发现逻辑存在错误或需要更新逻辑,我完全清楚应该在哪个位置进行查找。我不仅集中了逻辑,而且还使对象模型的意图更明确。Evans 的《领域驱动设计》书中第 246 页解释了这样做的好处:

如果开发人员必须为了使用某个组件而考虑该组件的实现,那么封装也就失去了意义。如果除原开发人员以外的其他人必须根据相应的实现来推断对象或操作的用途,那么这个新的开发人员基本上推断不出该对象或操作的用途。如果无法了解意图,那么代码可能目前有效,但设计的概念基础已经遭到破坏,并且两名开发人员之间无法协作。

为了便于理解,我根据函数用途对函数进行显式命名并对逻辑进行封装(并附带了一些注释来解释操作用途),因此下一位开发人员(甚至该开发人员可能就是五个月之后的我自己)可以轻松确定操作用途,而不必先进行实现。将该逻辑与其自然关联的数据封装到一起,其实是充分利用了面向对象语言的表达特性。

建议 如果逻辑复杂而曲折并且可能一次性涉及多个实体,则可使用域模型。建议将逻辑与其具有最密切关系的对象绑定到一起,并为操作特意指定一个有意义的名称。

域模型与 RIA 服务中的事务脚本的差异 您可能已经注意到,事务脚本和域模型都是在实体上直接进行调用。但请注意,这两种模式处于两个不同的位置。对于事务脚本,在实体上调用函数的目的在于,向域上下文/服务表明当下次调用提交更改时应该在域服务上调用相应的函数。而对于域模型,则是在客户端执行逻辑,然后在调用提交更改时提交。

存储库和查询对象 域服务在默认情况下采用存储库模式(请参阅 Fowler 书中第 322 页)。在 WCF RIA 服务代码库 (code.msdn.microsoft.com/RiaServices) 中,RIA 服务团队提供了一个精彩示例,说明了如何在 DomainContext 上创建该模式的显式实现。这样可以提高应用程序的可测试性,并且无需实际影响到服务层和数据库。另外,在我的博客 (azurecoding.net/blogs/brownie) 中,我提供了一个在存储库之上的查询对象模式(请参考 Fowler 书中第 316 页)的实现,这会使服务器端的查询执行操作推迟到出现实际枚举时。

应用服务层

快速回答问题:如果您想要利用富域模型,但不想将其逻辑公开到 UI 层,您会怎么做?这种情况就是应用服务层模式(请参考 Fowler 书中第 133 页)所适用的情况。在拥有域模型之后,该模型的实现很简单,只需将域逻辑从 shared.cs 中移出并移入到单独的分部类,然后将一个函数添加到域服务上,并由域服务在实体上调用该函数。

应用服务层作为域模型上层的简化外观,它会公开操作但不公开操作的详细信息。另一个优点是,域对象能够获取内部依赖关系,并且不要求服务层客户端也获取这样的内部依赖关系。在某些情况下(请参阅图 6 所示的季节性产品选择示例),域服务在域上执行一次简单调用。有时,域服务可能包含一些业务流程条目,但请注意,太多的业务流程就会变回到事务脚本,在域中封装逻辑的优势也就丧失。

建议 若要在域模型的上层提供简单外观,而又不必让 UI 层具有实体可能具有的依赖关系,则可使用应用服务层。

额外收获:界定的上下文

在 RIA 论坛中,参与者通常问:“如何将跨越多个域服务的大型数据库进行拆分,使其更易于管理?”接下来的一个问题是:“如何处理需要存在于多个域服务内的实体?”起初,我认为应该不需要做这样的事;域服务应该充当域模型上方的服务层,并且应该有单一的域服务充当整个域的外观。

但我在为本文做调研期间,我遇到了“界定的上下文”模式(请参考 Evans 书中第 336 页),我以前也在书上看到过这种模式,但我在回答这些问题时并没有想起它。该模式的基本前提是,在大项目上有多个子域同时起作用。以 KharaPOS 为例,我有一个对应于目录的域,还有一个对应于销售的单独域。

界定的上下文允许这些域和平共存,即使它们之间有共享的元素(例如 Sale、BusinessUnit、Product 和 LineItem,这些元素同时存在于销售域和目录域中)也不会冲突。这些实体适用不同的规则,而域将基于这些规则与实体交互(Sale 和 LineItem 在目录域中为只读)。一条最终规则是,操作绝不能跨越上下文。这其实是为了简化流程,因为 Silverlight 不支持位于多个域服务之上的事务。

建议 若要将大型系统拆分为多个逻辑子系统,则可使用界定的上下文。

成功的陷阱

在本文中,我们了解了 RIA 服务如何轻松支持大多数企业模式。很少有框架如此简单易行而且足够灵活,能够支持所有的应用程序(从最简单的电子表格数据输入应用程序到最复杂的业务应用程序),无需做太多的工作就能完成过渡。这就是 Brad Abrams 在他的同名博客文章中提到的成功的陷阱 (blogs.msdn.com/brada/archive/2003/10/02/50420.aspx)。                 

Mike Brown  是 KharaSoft Inc. (kharasoft.com) 的创始人兼总裁,该公司是专门从事培训、自定义软件开发和“软件即服务”的一家技术公司。他是一名拥有超过 14 年行业经验的极具成就的技术专家、蝉联 MVP 奖的得主、Indy Alt.NET 用户组 (indyalt.net) 的创始人之一、Bears 的忠实粉丝!

衷心感谢以下技术专家,感谢他们审阅了本文:Brad Abrams