WCF版的PetShop之二(2):模块中的层次划分

原文出处:http://kb.cnblogs.com/page/52615/2/
四、数据访问层设计

  数据访问层定义在{Module}.DataAccess中,它完成单纯的基于数据库操作。为了便于操作,我写了一个简单的帮助类:DbHelper。DbHelper通过ADO.NET完成一些简单的操作,ExecuteReader、ExecuteNonQuery和ExecuteScalar对应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(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 products = new List();
  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();
  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。

你可能感兴趣的:(WCF版的PetShop之二(2):模块中的层次划分)