WCF版的PetShop之二:模块中的层次划分
系列文章导航:
WCF版的PetShop之三:实现分布式的Membership和上下文传递
四、数据访问层设计
数据访问层定义在{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。