程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> RIA服務-使用WCF RIA服務的企業模式

RIA服務-使用WCF RIA服務的企業模式

編輯:關於.NET

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)。

圖 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 設計器,可以根據數據庫表通過五個步驟創建一個簡單的“基於數據的窗體設計”編輯器:

創建新的 Silverlight 業務應用程序。

向創建的 Web 應用程序中添加一個新的實體數據模型 (EDM)(使用向導導入數據庫)。

向引用該數據模型的 Web 應用程序中添加域服務(務必先編譯該應用程序,使其正確發現 EDM)。

使用數據源面板將一個由 RIA 服務公開的實體拖放到 Silverlight 應用程序中的頁面或用戶控件的表面上(務必再次編譯該應用程序,使其能看到新的域服務)。

添加按鈕和隱藏代碼,使用如下簡單代碼行將窗體上的更改保存到數據庫:

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 推薦了兩種方法:

使用命令來封裝操作並傳遞這些命令

使用一個類來承載事務腳本集合

我在這裡選用了第二種方法,但完全也可以使用命令來實現。不將目錄條目公開到 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 所示。

圖 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)。

下載代碼示例:http://kharapos.codeplex.com/

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved