WCF版的PetShop之二:模块中的层次划分
系列文章导航:
WCF版的PetShop之三:实现分布式的Membership和上下文传递
上一篇文章主要讨论的是PetShop的模块划分,在这一篇文章中我们来讨论在一个模块中如何进行层次划分。模块划分应该是基于功能的,一个模块可以看成是服务于某项功能的所有资源的集合;层次划分侧重于关注点分离(SoC:Separation of Concern ),让某一层专注于某项单一的操作,以实现重用性、可维护性、可测试性等相应的目的。Source Code从这里下载。
一、基本的层次结构
我们接下来将目光聚焦到模块内部,看看每一个模块具体又有怎样的层次划分。我们将Infrastructures、Products和Orders目标展开,将会呈现出如图1所示的层次结构。
图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所示。
图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();
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 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: }