閱讀204 返回首頁    go 技術社區[雲棲]


WCF版的PetShop之二:模塊中的層次劃分[提供源代碼下載]

上一篇文章主要討論的是PetShop的模塊劃分,在這一篇文章中我們來討論在一個模塊中如何進行層次劃分。模塊劃分應該是基於功能的,一個模塊可以看成是服務於某項功能的所有資源的集合;層次劃分側重於關注點分離(SoC:Separation of Concern ),讓某一層專注於某項單一的操作,以實現重用性、可維護性、可測試性等相應的目的。Source Code從這裏下載。

一、基本的層次結構

我們接下來將目光聚焦到模塊內部,看看每一個模塊具體又有怎樣的層次劃分。我們將Infrastructures、Products和Orders目標展開,將會呈現出如圖1所示的層次結構。

clip_image002

圖1 從解決方案的結構看模塊的層次結構

以Products模塊為例,它由如下的項目組成:

  • Products對於整個應用來說,Products是最終基於該模塊功能的提供者;
  • Products.Interface: 模塊提供給其他模塊的服務接口,本項目被Products項目引用;
  • Products.Service.Interface模塊客戶端和服務端進行服務調用的WCF服務契約,Products項目最為WCF服務的客戶端通過該接口進行服務調用;
  • Products.Service實現了上述服務契約的WCF服務,引用了Products.Service.Interface項目;
  • Products.BusinessComponent也可以稱為業務邏輯層,實現了真正的業務邏輯;
  • Products.DataAccess數據訪問層,在這裏主要提供對數據庫的訪問;
  • Products.BusinessEntity提供的業務實體(BusinessEntity)類型的定義。一般來講,業務實體和數據契約(DataContract)是不同的,前者主要對本模塊,後者則對外,在這裏為了簡單起見,將兩者合二為一。

從部署的角度講,Products和Products.Interface部署與於Web服務器;Products.Service、Products.BusinessComponent和Products.DataAccess則部署於應用服務器;Products.Service.Interface和Products.BusinessEntity則同時被部署於Web服務器和應用服務器。整個層次結構大體上如圖2所示。

clip_image004

圖2 邏輯層次和物理部署

二、數據庫設計

整個應用主要涉及4個表,其中3個用於存儲業務數據(產品表、訂單表和訂單明細表),另一個用於存儲簡單的審核信息(審核表)。4個表的結構可以分別參考相應的SQL腳本。

產品表(T_PRODUCT)

   1: CREATE TABLE [T_PRODUCT] (
   2:   [PRODUCT_ID]         [VARCHAR](50)          NOT NULL,
   3:   [PRODUCT_CATEGORY]   [NVARCHAR](128)        NOT NULL,
   4:   [PRODUCT_NAME]       [NVARCHAR](256)        NOT NULL,
   5:   [PRODUCT_PIC]        [NVARCHAR](512),
   6:   [PRODUCT_DESC]       [NVARCHAR](800),
   7:   [PRODUCT_UNIT_PRICE] [DECIMAL](10,2)        NOT NULL,
   8:   [PRODUCT_INVENTORY]  [INT]                  NOT NULL,
   9:   
  10:   [VERSION_NO]         [TIMESTAMP]            NOT NULL,
  11:   [TRANSACTION_ID]     [VARCHAR](50)          NOT NULL,
  12:   [CREATED_BY]         [NVARCHAR](256)        NOT NULL,
  13:   [CREATED_TIME]       [DATETIME]             NOT NULL,
  14:   [LAST_UPDATED_BY]    [NVARCHAR](256)        NOT NULL,
  15:   [LAST_UPDATED_TIME]  [DATETIME]             NOT NULL
  16:   
  17:   CONSTRAINT [C_PRODUCT_PK]            PRIMARY KEY CLUSTERED    ( [PRODUCT_ID] ASC ) ON [PRIMARY]) ON [PRIMARY]

訂單表(T_ORDER)

   1: CREATE TABLE [T_ORDER] (
   2:   [ORDER_ID]          [VARCHAR](50)         NOT NULL,
   3:   [ORDER_DATE]        [DATETIME]            NOT NULL,
   4:   [ORDER_TOTAL_PRICE] [DECIMAL](38,2)       NOT NULL,
   5:   [ORDERED_BY]        [NVARCHAR](256)       NOT NULL,
   6:   
   7:   [VERSION_NO]        [TIMESTAMP]           NOT NULL ,
   8:   [TRANSACTION_ID]    [VARCHAR](50)         NOT NULL ,
   9:   [CREATED_BY]        [NVARCHAR](256)       NOT NULL ,
  10:   [CREATED_TIME]      [DATETIME]            NOT NULL ,
  11:   [LAST_UPDATED_BY]   [NVARCHAR](256)       NOT NULL ,
  12:   [LAST_UPDATED_TIME] [DATETIME]            NOT NULL 
  13:   
  14:   CONSTRAINT [C_ORDER_PK]                PRIMARY KEY CLUSTERED ( [ORDER_ID] ASC ) ON [PRIMARY]) ON [PRIMARY]

訂單明細表(T_ORDER_DETAIL)

   1: CREATE TABLE [T_ORDER_DETAIL] (
   2:   [ORDER_ID]          [VARCHAR](50)         NOT NULL,
   3:   [PRODUCT_ID]        [VARCHAR](50)         NOT NULL,
   4:   [QUANTITY]          [INT]                 NOT NULL,
   5:   
   6:   [VERSION_NO]        [TIMESTAMP]           NOT NULL ,
   7:   [TRANSACTION_ID]    [VARCHAR](50)         NOT NULL ,
   8:   [CREATED_BY]        [NVARCHAR](256)       NOT NULL ,
   9:   [CREATED_TIME]      [DATETIME]            NOT NULL ,
  10:   [LAST_UPDATED_BY]   [NVARCHAR](256)       NOT NULL ,
  11:   [LAST_UPDATED_TIME] [DATETIME]            NOT NULL 
  12:   
  13:   CONSTRAINT [C_ORDER_DETAIL_PK]        PRIMARY KEY CLUSTERED ( [PRODUCT_ID]  ASC,[ORDER_ID] ASC ) ON [PRIMARY]) ON [PRIMARY]

審核表(T_AUDIT)

   1: CREATE TABLE [T_AUDIT](
   2:     [TRANSACTION_ID] [varchar](50)    NOT NULL,
   3:     [OPERATION] [nvarchar](256)       NOT NULL,
   4:     [OPERATOR] [varchar](50)          NOT NULL,
   5:     [OPERATION_TIME] [datetime]       NOT NULL,
   6:     CONSTRAINT [C_AUDIT_PK]           PRIMARY KEY CLUSTERED ( [TRANSACTION_ID]  ASC) ON [PRIMARY])    ON [PRIMARY]

注:對於每一個業務表,我都添加了如下6個係統字段:VERSION_NO(TIMESTAMP)用於進行並發驗證;TRANSACTION_ID代表最後一次操作該紀錄的事務ID;CREATED_BY、CREATED_TIME、LAST_UPDATED_BY和LAST_UPDATED_TIME分別表示創建記錄的創建者和創建時間,以及最後一次操作的操作者和操作時間。

在PetShop中,我們將事務作為審核的基本單元,而每一個事務由上述的TRANSACTION_ID作為唯一標識。簡單起見,在這裏僅僅記錄一些數據最基本的信息:操作的名稱、操作者和操作時間。

介紹了表的定義,接下來簡單介紹相關存儲過程的定義。首先是用於篩選產品的兩個存儲過程:P_PRODUCT_GET_ALL和P_PRODUCT_GET_BY_ID,前者獲取所有的產品,後者根據ID獲取相應產品信息。

P_PRODUCT_GET_ALL

   1: CREATE Procedure P_PRODUCT_GET_ALL
   2: AS
   3: SELECT [PRODUCT_ID]
   4:       ,[PRODUCT_CATEGORY]
   5:       ,[PRODUCT_NAME]
   6:       ,[PRODUCT_PIC]
   7:       ,[PRODUCT_DESC]
   8:       ,[PRODUCT_UNIT_PRICE]
   9:       ,[PRODUCT_INVENTORY]
  10:       ,[VERSION_NO]
  11:       ,[TRANSACTION_ID]
  12:       ,[CREATED_BY]
  13:       ,[CREATED_TIME]
  14:       ,[LAST_UPDATED_BY]
  15:       ,[LAST_UPDATED_TIME]
  16:   FROM [dbo].[T_PRODUCT]
  17: GO
   1: CREATE Procedure P_PRODUCT_GET_BY_ID
   2: (
   3:     @p_product_id VARCHAR(50)
   4: )
   5: AS
   6:  
   7: SELECT [PRODUCT_ID]
   8:       ,[PRODUCT_CATEGORY]
   9:       ,[PRODUCT_NAME]
  10:       ,[PRODUCT_PIC]
  11:       ,[PRODUCT_DESC]
  12:       ,[PRODUCT_UNIT_PRICE]
  13:       ,[PRODUCT_INVENTORY]
  14:       ,[VERSION_NO]
  15:       ,[TRANSACTION_ID]
  16:       ,[CREATED_BY]
  17:       ,[CREATED_TIME]
  18:       ,[LAST_UPDATED_BY]
  19:       ,[LAST_UPDATED_TIME]
  20:   FROM [dbo].[T_PRODUCT]
  21:   WHERE [PRODUCT_ID] = @p_product_id
  22: GO

而下麵的兩個存儲過程P_ORDER_INSERT和P_ORDER_DETAIL_INSERT則用於添加訂單記錄。

P_ORDER_INSERT

   1: CREATE Procedure P_ORDER_INSERT
   2:     (
   3:         @p_order_id            VARCHAR(50),
   4:         @p_ordered_by          VARCHAR(50),
   5:         @p_total_price         DECIMAL,
   6:         @p_user_name           NVARCHAR(50),
   7:         @p_transacion_id       VARCHAR(50)
   8:     )
   9:  
  10: AS
  11: INSERT INTO [PetShop].[dbo].[T_ORDER]
  12:            ([ORDER_ID]
  13:            ,[ORDER_DATE]
  14:            ,[ORDER_TOTAL_PRICE]
  15:            ,[ORDERED_BY]
  16:            ,[TRANSACTION_ID]
  17:            ,[CREATED_BY]
  18:            ,[CREATED_TIME]
  19:            ,[LAST_UPDATED_BY]
  20:            ,[LAST_UPDATED_TIME])
  21:      VALUES
  22:            (@p_order_id
  23:            ,GETDATE()
  24:            ,@p_total_price
  25:            ,@P_ordered_by
  26:            ,@p_transacion_id
  27:            ,@p_user_name
  28:            ,GETDATE()
  29:            ,@p_user_name
  30:            ,GETDATE())
  31: GO

P_ORDER_DETAIL_INSERT

   1: CREATE Procedure P_ORDER_DETAIL_INSERT
   2:     (
   3:         @p_order_id        VARCHAR(50),
   4:         @p_product_id      VARCHAR(50),
   5:         @p_quantity        INT,
   6:         @p_user_name       NVARCHAR(50),
   7:         @p_transacion_id   VARCHAR(50)
   8:     )
   9: AS
  10: INSERT INTO [PetShop].[dbo].[T_ORDER_DETAIL]
  11:            ([ORDER_ID]
  12:            ,[PRODUCT_ID]
  13:            ,[QUANTITY]
  14:            ,[TRANSACTION_ID]
  15:            ,[CREATED_BY]
  16:            ,[CREATED_TIME]
  17:            ,[LAST_UPDATED_BY]
  18:            ,[LAST_UPDATED_TIME])
  19:      VALUES
  20:            (@p_order_id
  21:            ,@p_product_id
  22:            ,@p_quantity
  23:            ,@p_transacion_id
  24:            ,@p_user_name
  25:            ,GETDATE()
  26:            ,@p_user_name
  27:            ,GETDATE())
  28: GO

三、業務實體(數據契約)設計

我們將對內的業務實體(Business Entity)和對外的數據契約合二為一,定義成WCF的數據契約(Data Contract)。所有的業務實體類型定義在相應模塊的{Module}.BusinessEntity項目之中。在Products.BusinessEntity定義了Product數據契約表示,產品相關信息;在Orders.BusinessEntity中定義了Order和OrderDetail數據契約,表示提交的訂單和訂單明細。

注:如果采用領域模型(Domain Model)來設計業務邏輯層,整個模型通過以一個個麵向業務邏輯(而不是數據存儲)的對象構成。而這些對象是完全基於OO的對象,即數據(或者狀態)和行為(或者方法)的封裝。如果業務邏輯層對外提供服務,我們需要將數據封裝成為數據傳輸對象(DTO:Data Transfer Object)。在理想的情況下,我們需要一個額外的層次實現領域對象與數據傳輸對象之間的轉換,但是在實際項目開發中,這會帶來很多額外的成本。對於本例,我們大體上可以看成是將數據傳輸對象和領域對象的數據部分合二為一(PetShop並沒有完全按照領域模型來設計)。

Product

   1: using System;
   2: using System.Runtime.Serialization;
   3: namespace Artech.PetShop.Orders.BusinessEntity
   4: {
   5:     [DataContract(Namespace="https://www.artech.com/petshop/")]
   6:     public class Product
   7:     {
   8:         [DataMember]
   9:         public Guid ProductID
  10:         { get; set; }
  11:         [DataMember]
  12:         public string Category
  13:         { get; set; }
  14:         [DataMember]
  15:         public string ProductName
  16:         { get; set; }
  17:         [DataMember]
  18:         public string Description
  19:         { get; set; }
  20:         [DataMember]
  21:         public decimal UnitPrice
  22:         { get; set; }
  23:         [DataMember]
  24:         public string Picture
  25:         { get; set; }
  26:         [DataMember]
  27:         public int Inventory
  28:         { get; set; }
  29:     }
  30: }

OrderDetail

   1: using System;
   2: using System.Runtime.Serialization;
   3: namespace Artech.PetShop.Orders.BusinessEntity
   4: {
   5:     [DataContract(Namespace = "https://www.artech.com/petshop/")]
   6:     public class OrderDetail
   7:     {
   8:        [DataMember]
   9:        public Guid ProductID
  10:        { get; set; }
  11:        public string ProductName
  12:        { get; set; }
  13:        [DataMember]
  14:        public decimal UnitPrice
  15:        { get; set; }
  16:        [DataMember]
  17:         public int Quantity
  18:        { get; set; }
  19:     }
  20: }

Order

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Runtime.Serialization;
   5: namespace Artech.PetShop.Orders.BusinessEntity
   6: {
   7:     [DataContract(Namespace = "https://www.artech.com/petshop/")]
   8:     [KnownType(typeof(OrderDetail))]
   9:     public class Order
  10:     {
  11:         public Order()
  12:         {
  13:             this.Details = new List<OrderDetail>();
  14:         }
  15:         [DataMember]
  16:         public Guid OrderNo
  17:         { get; set; }
  18:         [DataMember]
  19:         public DateTime OrderDate
  20:         { get; set; }
  21:  
  22:         [DataMember]
  23:         public string OrderBy
  24:         { get; set; }
  25:  
  26:         [DataMember]
  27:         public IList<OrderDetail> Details
  28:         { get; set; }
  29:  
  30:         public decimal TotalPrice
  31:         {
  32:             get
  33:             {
  34:                 return (decimal)this.Details.Sum(detail => detail.Quantity * detail.UnitPrice);
  35:             }
  36:         }
  37:     }
  38: }

四、數據訪問層設計

數據訪問層定義在{Module}.DataAccess中,它完成單純的基於數據庫操作。為了便於操作,我寫了一個簡單的幫助類:DbHelper。DbHelper通過ADO.NET完成一些簡單的操作,ExecuteReader、ExecuteNonQuery和ExecuteScalar<T>對應DbCommand的同名方法。此外,該DbHelper與具體的數據庫無關,同時支持SQL Server和Oracle。

   1: using System.Collections.Generic;
   2: using System.Configuration;
   3: using System.Data;
   4: using System.Data.Common;
   5: using System.Data.OracleClient;
   6: using System.Data.SqlClient;
   7: namespace Artech.PetShop.Common
   8: {
   9:     public class DbHelper
  10:     {
  11:         private DbProviderFactory _dbProviderFactory;
  12:         private string _connectionString;
  13:         private DbConnection CreateConnection()
  14:         {
  15:             DbConnection connection = this._dbProviderFactory.CreateConnection();
  16:             connection.ConnectionString = this._connectionString;
  17:             return connection;
  18:         }
  19:  
  20:         private void DeriveParameters(DbCommand discoveryCommand)
  21:         {
  22:             if (discoveryCommand.CommandType != CommandType.StoredProcedure)
  23:             {
  24:                 return;
  25:             }
  26:  
  27:             if (this._dbProviderFactory is SqlClientFactory)
  28:             {
  29:                 SqlCommandBuilder.DeriveParameters
  30: ((SqlCommand)discoveryCommand);
  31:             }
  32:             
  33:             if(this._dbProviderFactory is OracleClientFactory)
  34:             {
  35:                 OracleCommandBuilder.DeriveParameters
  36: ((OracleCommand)discoveryCommand);
  37:             }
  38:         }
  39:  
  40:         private void AssignParameters(DbCommand command, IDictionary<string, object> parameters)
  41:         {
  42:             IDictionary<string, object> copiedParams = new Dictionary<string, object>();
  43:             foreach (var item in parameters)
  44:             {
  45:                 copiedParams.Add(item.Key.ToLowerInvariant(), item.Value);
  46:             }
  47:             foreach (DbParameter parameter in command.Parameters)
  48:             {
  49:                 if (!copiedParams.ContainsKey(parameter.ParameterName.
  50: TrimStart('@').ToLowerInvariant()))
  51:                 {
  52:                     continue;
  53:                 }
  54:  
  55:                 parameter.Value = copiedParams[parameter.ParameterName.
  56: TrimStart('@').ToLowerInvariant()];
  57:             }
  58:         }
  59:  
  60:         public DbHelper(string connectionStringName)
  61:         {
  62:             string providerName = ConfigurationManager.ConnectionStrings
  63: [connectionStringName].ProviderName;
  64:             this._connectionString = ConfigurationManager.ConnectionStrings
  65: [connectionStringName].ConnectionString;
  66:             this._dbProviderFactory = DbProviderFactories.GetFactory(providerName);
  67:         }
  68:  
  69:         public DbDataReader ExecuteReader(string procedureName,  IDictionary<string, object> parameters)
  70:         {           
  71:             DbConnection connection = this.CreateConnection();
  72:             using (DbCommand command = connection.CreateCommand())
  73:             {
  74:                 command.CommandText = procedureName;
  75:                 command.CommandType = CommandType.StoredProcedure;
  76:                 connection.Open();
  77:                 this.DeriveParameters(command);
  78:                 this.AssignParameters(command, parameters);
  79:                 return command.ExecuteReader(CommandBehavior.CloseConnection);
  80:             }     
  81:         }
  82:  
  83:         public int ExecuteNonQuery(string procedureName, IDictionary<string, object> parameters)
  84:         {
  85:             using (DbConnection connection = this.CreateConnection())
  86:             {
  87:                 using (DbCommand command = connection.CreateCommand())
  88:                 {
  89:                     command.CommandText = procedureName;
  90:                     command.CommandType =  CommandType.StoredProcedure;
  91:                     connection.Open();
  92:                     this.DeriveParameters(command);
  93:                     this.AssignParameters(command, parameters);
  94:                     return command.ExecuteNonQuery();
  95:                 }     
  96:             }
  97:         }
  98:  
  99:         public T ExecuteScalar<T>(string procedureName, IDictionary<string, object> parameters)
 100:         {
 101:             using (DbConnection connection = this.CreateConnection())
 102:             {
 103:                 using (DbCommand command = connection.CreateCommand())
 104:                 {
 105:                     command.CommandText = commandText;
 106:                     command.CommandType = CommandType.StoredProcedure;
 107:                     this.DeriveParameters(command);
 108:                     this.AssignParameters(command, parameters);
 109:                     connection.Open();
 110:                     return (T)command.ExecuteScalar();
 111:                 }
 112:             }
 113:         }
 114:     }
 115: }

注: 該DbHelper僅僅為演示之用,如果用於真正的開發中,應該進行一些優化,比如利用存儲過程的參數緩存提高性能等 。

為了促進重用和擴展,我為每一個層的類型都定義了一個基類,這在真正的項目開發中是比較常見的做法。所有的基類定義在Common項目中,對於數據訪問層,對應的基類是DataAccessBase。在DataAccessBase中,將上麵定義的DbHelper作為它的隻讀屬性,由於DbHelper是一個單純的工具(Utility)對象,故將其定義成單例模式。

   1: using System;
   2: namespace Artech.PetShop.Common
   3: {
   4:     public class DataAccessBase:MarshalByRefObject
   5:     {
   6:         private static readonly DbHelper helper = new DbHelper("PetShopDb");
   7:  
   8:         protected DbHelper Helper
   9:         {
  10:             get
  11:             {
  12:                 return helper;
  13:             }
  14:         }
  15:     }
  16: }

在Products.DataAccess和Orders.DataAccess中,分別定義了相應的DataAccessBase類型,用於進行產品的篩選和訂單的提交。

ProductDA

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Data.Common;
   4: using System.Linq;
   5: using Artech.PetShop.Common;
   6: using Artech.PetShop.Orders.BusinessEntity;
   7: namespace Artech.PetShop.Orders.DataAccess
   8: {
   9:     public class ProductDA: DataAccessBase
  10:     {       
  11:         public Product[] GetAllProducts()
  12:         {           
  13:             List<Product> products = new List<Product>();
  14:             using (DbDataReader reader = this.Helper.ExecuteReader("P_PRODUCT_GET_ALL", new Dictionary<string, object>()))
  15:             {
  16:                 while (reader.Read())
  17:                 {
  18:                     products.Add(new Product
  19:                     {
  20:                         ProductID   = new Guid((string)reader["PRODUCT_ID"]),
  21:                         ProductName = (string)reader["PRODUCT_NAME"],
  22:                         Description = (string)reader["PRODUCT_DESC"],
  23:                         Picture     = (string)reader["PRODUCT_PIC"],
  24:                         UnitPrice   = (decimal)reader["PRODUCT_UNIT_PRICE"],
  25:                         Category    = (string)reader["PRODUCT_CATEGORY"],
  26:                         Inventory   = (int)reader["PRODUCT_INVENTORY"]
  27:                     });
  28:                 }
  29:             }
  30:  
  31:             return products.ToArray<Product>();
  32:         }
  33:  
  34:         public Product GetProductByID(Guid productID)
  35:         {
  36:             Dictionary<string, object> parameters = new Dictionary<string, object>();
  37:             parameters.Add("p_product_id", productID.ToString());
  38:             using (DbDataReader reader = this.Helper.ExecuteReader("P_PRODUCT_GET_BY_ID", parameters))
  39:             {
  40:                 while (reader.Read())
  41:                 {
  42:                     return new Product
  43:                     {
  44:                         ProductID   = new Guid((string)reader["PRODUCT_ID"]),
  45:                         ProductName = (string)reader["PRODUCT_NAME"],
  46:                         Description = (string)reader["PRODUCT_DESC"],
  47:                         Picture     = (string)reader["PRODUCT_PIC"],
  48:                         UnitPrice   = (decimal)reader["PRODUCT_UNIT_PRICE"],
  49:                         Category    = (string)reader["PRODUCT_CATEGORY"],
  50:                         Inventory   = (int)reader["PRODUCT_INVENTORY"]
  51:                     };
  52:                 }
  53:             }
  54:  
  55:             return null;
  56:         }
  57:     }
  58: }

OrderDA

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Transactions;
   4: using Artech.PetShop.Common;
   5: using Artech.PetShop.Orders.BusinessEntity;
   6: namespace Artech.PetShop.Orders.DataAccess
   7: {
   8:   public  class OrderDA: DataAccessBase
   9:     {
  10:       public void Submit(Order order)
  11:       {
  12:           order.OrderNo = Guid.NewGuid();
  13:           string procedureName = "P_ORDER_INSERT";
  14:           Dictionary<string, object> parameters = new Dictionary<string, object>();
  15:           parameters.Add("p_order_id",      order.OrderNo.ToString());
  16:           parameters.Add("p_ordered_by",    ApplicationContext.Current.UserName);
  17:           parameters.Add("p_total_price",   order.TotalPrice);
  18:           parameters.Add("p_user_name",     ApplicationContext.Current.UserName);
  19:           parameters.Add("p_transacion_id", Transaction.Current.TransactionInformation.LocalIdentifier);
  20:           this.Helper.ExecuteNonQuery(procedureName, parameters);
  21:  
  22:           procedureName = "P_ORDER_DETAIL_INSERT";
  23:           foreach (OrderDetail detail in order.Details)
  24:           {
  25:               parameters.Clear();
  26:               parameters.Add("p_order_id",      order.OrderNo.ToString());
  27:               parameters.Add("p_product_id",    detail.ProductID.ToString());
  28:               parameters.Add("p_quantity",      detail.Quantity);
  29:               parameters.Add("p_user_name",     ApplicationContext.Current.UserName);
  30:               parameters.Add("p_transacion_id", Transaction.Current.TransactionInformation.LocalIdentifier);
  31:               this.Helper.ExecuteNonQuery(procedureName, parameters);
  32:           }
  33:       }
  34:     }
  35: } 

在PetShop中,對事務的控製放在服務層。事務在服務操作開始的時候被開啟,在事務被提交之前,我們通過當前事務(Transaction.Current)的TransactionInformation屬性得到事務ID(LocalIdentifier)。而CREATED_BY和LAST_UPDATED_BY代表當前登錄係統的用戶,對於采用分布式構架的PetShop來說,登錄用戶的獲取僅限於Web服務器,對於應用服務器是不可得的。不僅僅是用戶名,在基於分布式部署的情況下,可能會需要其他一些從客戶端向服務端傳遞的上下文信息。為此我定義了一個特殊的組件:ApplicationContext,用於保存基於當前線程或者會話的上下文信息。關於ApplicationContext的實現,你可以參考《 通過WCF Extension實現Context信息的傳遞》,在這裏隻需要知道可以通過它獲取登錄PetShop係統的用戶名。

五、業務邏輯層設計

業務邏輯層建立在數據訪問層之上,在PetShop中模塊業務邏輯層對應的項目為{Module}. BusinessComponent,所以業務對象類型也具有自己的基類:BusinessComponentBase。由於案例的邏輯相對簡單,並沒有太複雜的業務邏輯,所以主要集中在對數據訪問層的調用上麵。下麵是定義在Products.BusinessComponent和Orders.BusinessComponent中業務類型的定義:

ProductBC

   1: using System;
   2: using Artech.PetShop.Common;
   3: using Artech.PetShop.Orders.BusinessEntity;
   4: using Artech.PetShop.Orders.DataAccess;
   5: using Microsoft.Practices.Unity;
   6: namespace Artech.PetShop.Products.BusinessComponent
   7: {
   8:    public class ProductBC: BusinessComponentBase
   9:     {
  10:        [Dependency]
  11:        public ProductDA DataAccess
  12:        { get; set; }
  13:  
  14:         public Product[] GetAllProducts()
  15:         {
  16:             return this.DataAccess.GetAllProducts();
  17:         }
  18:  
  19:         public Product GetProductByID(Guid productID)
  20:         {
  21:             return this.DataAccess.GetProductByID(productID);
  22:         }
  23:  
  24:         public int GetInventory(Guid productID)
  25:         {
  26:             return this.DataAccess.GetProductByID(productID).Inventory;
  27:         }
  28:     }
  29: }

OrderBC

   1: using Artech.PetShop.Common;
   2: using Artech.PetShop.Orders.BusinessEntity;
   3: using Artech.PetShop.Orders.DataAccess;
   4: using Artech.PetShop.Products.Service.Interface;
   5: using Microsoft.Practices.Unity;
   6: namespace Artech.PetShop.Orders.BusinessComponent
   7: {
   8:     public class OrderBC:BusinessComponentBase
   9:     {
  10:         [Dependency]
  11:         public OrderDA DataAccess
  12:         { get; set; }
  13:  
  14:         [Dependency]
  15:         public IProductService ProductService
  16:         { get; set; }
  17:  
  18:         private void ValidateInventory(Order order)
  19:         {
  20:             foreach (var detail in order.Details)
  21:             {            
  22:  
  23:                 if(this.ProductService.GetInventory(detail.ProductID) < detail.Quantity)
  24:                 {
  25:                     throw new BusinessException("Lack of stock!");
  26:                 }
  27:             }
  28:         }
  29:  
  30:         public void Submit(Order order)
  31:         {
  32:             this.ValidateInventory(order);
  33:             this.DataAccess.Submit(order);
  34:         }
  35:     }
  36: }

PetShop采用典型的N層(N-Tier和N-Layer)應用架構和模塊化設計,我們通過依賴注入模式實現模塊之間,以及同一個模塊各個層次之間的鬆耦合。在實現上,充分利用了Unity這樣一個依賴注入容器。這兩點都可以從業務邏輯層的實現看出來:

  • 通過依賴注入容器創建底層對象:在業務邏輯層,對於數據訪問層對象的創建是通過屬性注入的方式實現的。比如,在ProductBC中,並沒有手工創建ProductDA對象,而是將其定義成屬性,並在上麵應用了DependencyAttribute特性。那麼當Unity創建ProductBC對象的時候,會初始化這個屬性。

注: 雖然ProductBC對ProductDA並沒有采用基於接口的調用(我們認為模塊是應用最基本的邏輯單元,接口是模塊對外的代理,模塊之間的調用才通過接口;無須為同一個模塊內各個層次之間的調用定義接口,當然,同一個模塊調用WCF服務又另當別論。如果硬要為被調用層的類型定義接口,我認為這是一種設計過度),談不上層次之間的鬆耦合,但是Unity是一種可擴展的依賴注入框架,我們可以同一些擴展去控製對象的創建行為,我認為這也是一種鬆耦合的表現。在PetShop中,正是因為采用這樣的設計,我們可以在每一個層上應用PIAB的CallHandler實現AOP,此是後話。

  • 通過依賴注入創建被依賴服務對象:一個模塊的業務邏輯需要調用另一個模塊的服務,需要采用基於接口的方式創建該服務。在OrderBC中,需要調用ProductService提供的服務獲取相關產品的庫存量。和上麵一樣,依然采用基於依賴屬性的實現方式,所不同的是,這裏屬性的類型為接口。

六、服務層與服務接口(服務契約)

業務場景的簡單性,決定了服務接口會很複雜。對於Products模塊來說,其業務功能主要集中於產品列表的獲取,以及基於某一個產品的相關信息和庫存的查詢;而Orders模塊,則主要體現在提交訂單上。下麵是分別定義在Products.Service.Interface和Orders.Service.Interface的服務契約。

IProductService

   1: using System;
   2: using System.ServiceModel;
   3: using Artech.PetShop.Common;
   4: using Artech.PetShop.Orders.BusinessEntity;
   5: namespace Artech.PetShop.Products.Service.Interface
   6: {
   7:     [ServiceContract(Namespace="https://www.artech.com/petshop/")]
   8:     public interface IProductService
   9:     {
  10:         [OperationContract]
  11:         [FaultContract(typeof(ServiceExceptionDetail))]
  12:         Product[] GetAllProducts();
  13:  
  14:         [OperationContract]
  15:         [FaultContract(typeof(ServiceExceptionDetail))]
  16:         Product GetProductByID(Guid productID);
  17:  
  18:         [OperationContract]
  19:         [FaultContract(typeof(ServiceExceptionDetail))]
  20:         int GetInventory(Guid productID);
  21:     }
  22: }

IOrderService

   1: using System.ServiceModel;
   2: using Artech.PetShop.Common;
   3: using Artech.PetShop.Orders.BusinessEntity;
   4: namespace Artech.PetShop.Orders.Service.Interface
   5: {
   6:     [ServiceContract(Namespace = "https://www.artech.com/petshop/")]
   7:     public interface IOrderService
   8:     {
   9:         [OperationContract]
  10:         [FaultContract(typeof(ServiceExceptionDetail))]
  11:        void Submit(Order order);
  12:     }
  13: }

在服務契約的每一個服務操作中,通過FaultContractAttribute定義了基於錯誤契約(Fault Contract),關於錯誤的契約,這是為了與EnterLib的Exception Handling Application Block集成的需要,具體的實現原理,可以參考《WCF與Exception Handling AppBlock集成[上篇][下篇]》。

服務接口定義完畢後,接下來的任務就是實現該接口,定義相應的服務。WCF服務定義在{Module}.Service項目中,服務操作通過調用對應的BusinessComonent實現。

ProductService

   1: using System;
   2: using Artech.PetShop.Common;
   3: using Artech.PetShop.Orders.BusinessComponent;
   4: using Artech.PetShop.Orders.BusinessEntity;
   5: using Artech.PetShop.Products.Service.Interface;
   6: using Microsoft.Practices.Unity;
   7: namespace Artech.PetShop.Products.Service
   8: {
   9:     public class ProductService : ServiceBase, IProductService
  10:     {
  11:         [Dependency]
  12:         public ProductBC BusinessComponent
  13:         { get; set; }
  14:  
  15:         #region IProductService Members
  16:  
  17:         public Product[] GetAllProducts()
  18:         {
  19:            return this.BusinessComponent.GetAllProducts();
  20:         }
  21:  
  22:         public Product GetProductByID(Guid productID)
  23:         {
  24:             return this.BusinessComponent.GetProductByID(productID);
  25:         }
  26:  
  27:         public int GetInventory(Guid productID)
  28:         {
  29:             return this.BusinessComponent.GetInventory(productID);
  30:         }
  31:  
  32:         #endregion
  33:     }
  34: }

OrderService:

   1: using System.ServiceModel;
   2: using Artech.PetShop.Common;
   3: using Artech.PetShop.Orders.BusinessComponent;
   4: using Artech.PetShop.Orders.BusinessEntity;
   5: using Artech.PetShop.Orders.Service.Interface;
   6: using Microsoft.Practices.Unity;
   7: namespace Artech.PetShop.Orders.Service
   8: {
   9:     public class OrderService :ServiceBase, IOrderService
  10:     {
  11:         [Dependency]
  12:         public OrderBC BusinessComponent
  13:         { get; set; }
  14:  
  15:         #region IOrderService Members
  16:  
  17:         [OperationBehavior(TransactionScopeRequired= true)]
  18:         [AuditCallHandler("提交訂單")]
  19:         public void Submit(Order order)
  20:         {
  21:             this.BusinessComponent.Submit(order);
  22:         }
  23:  
  24:         #endregion
  25:     }
  26: }

關於服務的定義,有以下3點值得注意:

  • 同BC(BusinssComponent)調用DA(DataAccess)一樣,Service同樣不需要通過new操作符創建BC對象,而是通過Unity提供的聲明式(應用DependencyAttribute特性)對象創建方式降低統一模塊中各個層級的依賴;
  • 對於涉及操作數據(添加、修改和刪除)的操作,需要將其納入事務中保證數據的完整性。PetShop中采用WCF自有的事務管理方式,我們隻需要在相應的操作中通過OperationBehavior設置TransactionScopeRequired屬性即可;
  • 由於在PetShop中,服務操作和事務具有相同的粒度,所以基於事務的審核也就是基於操作的審核。PetShop采用聲明式的審核方式,我們隻需要在相應的操作上添加AuditCallHandlerAttribute並設置操作審核名稱即可。這是一種AOP的編程方式,在這裏使用到的是微軟提供的一個開源的AOP框架:PIAB。

作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-30 11:04:31

  上一篇:go  WCF版的PetShop之一:PetShop簡介[提供源代碼下載]
  下一篇:go  擴展ToolBarManager、ListView和Grid控件以實現氣球式的ToolTip