WCF版的PetShop之二:模块中的层次划分[提供源代码下载]

上一篇文章主要讨论的是PetShop的模块划分,在这一篇文章中我们来讨论在一个模块中如何进行层次划分。模块划分应该是基于功能的,一个模块可以看成是服务于某项功能的所有资源的集合;层次划分侧重于关注点分离(SoC:Separation of Concern ),让某一层专注于某项单一的操作,以实现重用性、可维护性、可测试性等相应的目的。Source Code从这里下载。

一、基本的层次结构

我们接下来将目光聚焦到模块内部,看看每一个模块具体又有怎样的层次划分。我们将Infrastructures、Products和Orders目标展开,将会呈现出如图1所示的层次结构。

WCF版的PetShop之二:模块中的层次划分[提供源代码下载]

图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所示。

WCF版的PetShop之二:模块中的层次划分[提供源代码下载]

图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="http://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 = "http://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 = "http://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="http://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 = "http://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
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
上一篇:组策略管理——软件限制策略(4)


下一篇:Python 命令行之旅:深入 argparse(一)