WCF版的PetShop之一:PetShop简介[提供源代码下载]

在《WCF技术剖析(卷1)》的最后一章,我写了一个简单基于WCF的Web应用程序,该程序模拟一个最简单的网上订购的场景,所以我将其命名为PetShop。PetShop的目在于让读者体会到在真正的项目开发中,如何正确地、有效地使用WCF。在这个应用中,还会将个人对设计的一些总结融入其中,希望能够对读者有所启发。Source Code从这里下载。

一、PetShop功能简介

PetShop前端是一个单纯的基于ASP.NET应用的Web站点,整个站点由以下三个Web页面构成:

登录页面:和一般的基于Internet的Web站点一样,采用基于用户名/密码的认证方式。在图1所示的登录页面中,实际上仅仅使用了一个Login控件。熟悉ASP.NET的读者应该很清楚,该控件和ASP.NET的成员资格(Membership)模块进行了有效的集成,通过该模块可进行用户验证。

clip_image002

图1 PetShop登录页面 

默认页面:PetShop的默认页面为一个宠物的列表,列表项包含宠物的编号、名称、类别、价格、数量和相关介绍。登录的用户可以通过点击“加入购物车”链接进行选购。默认页面的界面如图2所示。

clip_image004

图2 PetShop默认页面

购物车页面:在用户点击默认页面的“加入购物车”链接后,会跳转到购物车页面。如图3所示,该页面列出了当前登录用户购物车中选购的所有宠物列表。用户可以将选购的宠物从购物车中移除,也可以更新选购的数量。

clip_image006

图3 PetShop购物车页面

严格来说,PetShop并不是一个功能完成的在线购物的Web应用,我们甚至没有提供结帐的功能,功能的完整性并不是本案例关注的重点。接下来我们先讨论一下整个PetShop的结构。

二、 PetShop的物理结构

PetShop采用典型的基于分布式的Web应用部署,从物理结构上讲,大体上分为4个层次:客户端(浏览器)、Web服务器(IIS)、应用服务器(IIS)和数据库服务器。应用的前端展现,采用ASP.NET,整个ASP.NET Web站点部署于Web服务器的IIS中。ASP.NET Web应用本身并不承担对主要业务逻辑的实现,也不直接与数据库交互。PetShop将业务逻辑的实现定义在一个个WCF服务之中。WCF服务采用基于IIS的寄宿方式,部署于应用服务器。ASP.NET Web前端应用采用HTTP协议进行服务调用,如果两者在同一个局域网内,可以采用TCP通信协议以获得最好的性能,以及TCP协议本身提供的对可靠传输的支持。对数据库的访问发生在应用服务器与数据库服务器之间。整个物理(部署)结构如图4所示。

clip_image008

图4 PetShop物理(部署)结构

三、PetShop的模块划分

模块是应用最基本的组成单元,而模块化是实现高内聚、松耦合的重要途径。模块本身应该是自治的,它独立地承担着某项功能的实现。模块划分应该是基于功能的,一个模块可以看成是服务于某项功能的所有资源的集合,模块的元素可以包括可视的UI、后台代码和SQL(或者存储过程),以及存储数据等。

1、模块化设计

在进行团队开发时,模块之间的独立性确保基于各个模块的开发团队可以独立进行开发,对于大规模的应用开发,模块化是保证软件质量的重要途径。模块化对于测试也具有积极作用,因为模块化赋予了每一个模块“插件”的特质,单个模块可以以“插件”的形式动态地插入现有系统,从而保证测试的及时交付。除了开发和测试,模块化对于应用的部署及产品交付同样重要。在时间就是金钱的今天,大多软件的开发都是分阶段进行的,每一个阶段完成不同的模块,阶段性的成果需要及时向用户交付。每次交付时,整个应用应该保持稳定的状态。只有高度的模块化,才能保证动态交付的模块不会对现有的模块造成影响。

模块化以及由它带来的好处,大部分人都能够理解,但却有很少人能够正确地将其应用到实际的设计之中。很多人甚至没有意识到,一些我们习以为常的设计违背了反模块化的原则。举一个很常见的例子,菜单对于大部分应用都是必须的,我们通常的做法是将整个应用的菜单内容统一维护,将它们保存到数据库或XML中,当应用启动的时候,整个菜单被加载显示。对于应用的使用者来说,可视化的菜单结构反映应用当前能够提供的可用功能的集合,如果基于某个模块的菜单项能够显示出来,就应该保证相应模块功能的完整性。但是,由于整个菜单的维护是独立的,与模块本身无关的,所以在测试的时候就会出现这样的情形:整个菜单能够很完整地显示出来,但是随便点击某个菜单项,整个应用程序就崩溃。和开发人员联系,得到的答案是相应的模块尚未完成。这样的设计对于部署也是不可取的,因为交付一个模块,就需要对维护的菜单数据作一次修正。

如果按照模块化的原则,整个设计应该是这样:菜单的管理下放到具体的模块中,当模块加载的时候,模块自行负责加载属于自己的菜单,并添加到整个菜单树相应的位置上。对于熟悉微软软件工厂(Software Factory)的读者,应该知道微软的-客户端软件工厂,无论是Web客户端软件工厂(WCSF:Web Client Software Factory)还是智能客户端软件工厂(SCSF:Smart Client Software Factory)对于菜单,都是采用这样的设计模式。

模块的自治特性并不意味着模块之间不存在依赖,依赖在软件设计中无所不在,设计的目标往往不是在于剔除依赖,而在于降低或者转移依赖。一个模块需要使用到另一个模块提供的功能,依赖便产生了。依赖又可以分为运行时依赖设计时(或者编译时)依赖,我们关心的是如何降低设计时依赖,或者如何将设计时依赖转移到运行时依赖

对于模块依赖来说,依赖方关心的是被依赖方能否提供它所需要的功能,而不关心被依赖方采用怎样的手段去实现这些被依赖的功能。在面向对象的世界里,接口定义了一系列抽象的操作,从而制定了一份“契约”,实现了接口就相当于履行了这份契约,承诺实现接口定义的操作。所以,接口的本质就是对功能提供能力的描述,在设计时降低模块依赖的最有效的途径就是仅仅保留对接口的依赖

对于模块化的设计,如果一个模块需要为别的模块提供某种功能,我们需要为这些功能定义相应的接口。模块自身提供对接口的实现,其他的模块通过接口间接地消费被依赖模块提供的功能

2、业务模块和基础模块

说到模块,很多人首先想到的是对单一业务功能的实现,实际上这里所说的模块仅仅是模块的一种类型:业务模块(Business Module)。除了实现某种业务功能外,还有一个模块提供一些非业务功能的实现,比如异常处理(Exception Handling)、日志(Instrumentation)、审核(Auditing)、缓存(Caching)、事务处理(Transaction)等,我们可以把这些类型的模块称为基础模块(Foundation Module或Infrastructure Module)。基础模块为业务模块提供一些公用的底层功能实现。

虽然模块具有业务模块和基础模块之分,在我看来,两者并没有本质的区别。虽然基础模块的主要任务就是为其他的模块提供某种功能,注定处于被依赖一方,但是上层模块调用基础模块的方式与调用其他业务模块的方式并没有本质的不同:都应该采用基于接口的调用方式

3、PetShop的模块划分

虽然PetShop模拟的场景很简单,但是为了演示模块化的设计,特意将“简单的问题复杂化”,将整个应用刻意地划分列为以下两个业务模块:

  • 产品模块(Products:提供产品列表的获取,以及向订单模块提供基于产品信息和库存量的查询。Products的接口定义在Products.Interface中;
  • 订单模块(Orders):提供产品的订购,由于该模块在本例并不对其他模块提供服务,所以并未为之定义接口。

除了以上两个业务模块之外,我将所有的基础服务定义在Infrastructures项目中。在这里定义了两个简单的基础服务:

  • 导航服务:用于页面之间的导航和参数传递的基础服务;
  • 查询字符串解析服务:用于解析查询字符串(QueryString)的基础服务。

在这里,我多次提到“服务”二字,这与前面所介绍的WCF服务没有关系。这里的服务为广义的服务,指的是一个模块为另一个模块提供的功能,我们把模块之间的调用也称为服务调用。

图5演示了整个PetShop解决方案的模块划分。基础模块定义在Infrastructures目录下,上述的两个业务模块定义在Modules目录的两个子目录Orders和Products下。DataBase目录的包含一个Database项目,用于维护所有SQL脚本和存储过程。Hosting对应一个IIS下的虚拟目录,所有WCF服务项目编译后的程序集都会生成到该目录下的/Bin子目录下,Hosting中还包括基于WCF服务的.svc文件。Common项目用于定义一些公用的类型。

clip_image010

图5 从解决方案的结构看PetShop的模块化设计

下面的代码表示导航基础服务的接口和实现,服务接口INavigatorService和NavigatorService分别定义在Infrastructures.Interface和Infrastructures项目下面。

   1: using System.Collections.Generic;
   2: namespace Artech.PetShop.Infrastructures.Interface
   3: {
   4:     public interface INavigatorService
   5:     {
   6:         void Navigate(string targetUrl, IDictionary<string, object> prameters);
   7:         void Navigate(string targetUrl);
   8:     }
   9: }
   1: using System;
   2: using System.Collections.Generic;
   3: using System.Web;
   4: using Artech.PetShop.Infrastructures.Interface;
   5: namespace Artech.PetShop.Infrastructures
   6: {
   7:     public class NavigatorService : INavigatorService
   8:     {
   9:         public void Navigate(string targetUrl, IDictionary<string, object> parameters)
  10:         {
  11:             if (string.IsNullOrEmpty(targetUrl))
  12:             {
  13:                 throw new ArgumentNullException("targetUrl");
  14:             }
  15:             if (parameters == null)
  16:             {
  17:                 throw new ArgumentNullException("prameters");
  18:             }
  19:  
  20:             if (parameters.Count == 0)
  21:             {
  22:                 this.Navigate(targetUrl);
  23:                 return;
  24:             }
  25:  
  26:             string queryString = string.Empty;
  27:             foreach (var parameter in parameters)
  28:             { 
  29:                 queryString += string.Format("{0}={1}&",parameter.Key, HttpUtility.UrlEncode(parameter.Value.ToString()));
  30:             }
  31:  
  32:             queryString = queryString.TrimEnd("&".ToCharArray());
  33:             HttpContext.Current.Response.Redirect(targetUrl + "?" + queryString);            
  34:         }
  35:  
  36:         public void Navigate(string targetUrl)
  37:         {
  38:             if (string.IsNullOrEmpty(targetUrl))
  39:             {
  40:                 throw new ArgumentNullException("targetUrl");
  41:             }
  42:             HttpContext.Current.Response.Redirect(targetUrl);
  43:         }
  44:     }
  45: }

需要使用到基础服务的模块采用基于接口的服务调用方式,所以不须要引用到Infrastructures,仅仅须要引用Infrastructures.Interface,这无形之中降低了上层模块与基础模块的依赖性。但是,基于基础服务调用的编程又是如何定义的呢?基础服务最终的实现定义在Infrastructures中,在运行时又是如何激活相应的基础服务的呢?这就需要使用到我定义的另一个重要的静态类型:ServiceLoader。ServiceLoader的实现采用了微软P&P团队开发的一个重要的应用程序块Unity。Unity为我们提供了一个轻量级的、可扩展的依赖注入容器,关于Unity,在后面还会进行相应的介绍。ServiceLoader定义如下:

   1: namespace Artech.PetShop.Common
   2: {
   3:     public static class ServiceLoader
   4:     {
   5:         public static T LoadService<T>()
   6:         {
   7:             //省略实现
   8:         }
   9:  
  10:         public static T LoadService<T>(string serviceName)
  11:         {
  12:             //省略实现
  13:         }
  14:     }
  15: }

借助ServiceLoader,我们就可以完全通过接口的方式对其他模块的服务进行调用了,下面是通过INavigatorService调用导航服务的例子:

   1: Dictionary<string, object> parameters = new Dictionary<string, object>();
   2: parameters.Add("productid",001);
   3: ServiceLoader.LoadService<INavigatorService>().Navigate("~/ShoppingCart.aspx", parameters);

对于需要向其他模块提供服务的业务模块来说,其定义方式和服务调用方式也和基础模块完全一样。以Products模块为例,它需要向Orders模块提供基于产品的详细信息,为此定义了ProductService和相应的接口IProduct(为了与后面定义的WCF服务契约IProductService相区别,在这里没有加Service后缀)。IProduct定义在Products.Interface中,而ProductService定义在Products中。对ProductService的调用依然通过ServiceLoader采用基于接口的调用。下面的代码提供了IProduct和ProductService的定义,以及借助ServiceLoader对该服务的调用。

   1: using System;
   2: using Artech.PetShop.Orders.BusinessEntity;
   3: namespace Artech.PetShop.Products.Interface
   4: {
   5:    public interface IProduct
   6:    {
   7:        Product GetProduct(Guid productID);
   8:    }
   9: }
   1: using System;
   2: using Artech.PetShop.Common;
   3: using Artech.PetShop.Orders.BusinessEntity;
   4: using Artech.PetShop.Products.Interface;
   5: using Artech.PetShop.Products.Service.Interface;
   6: using Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers;
   7: namespace Artech.PetShop.Products
   8: {
   9:     [CachingCallHandler(0,30,0)]
  10:     public class ProductService : IProduct
  11:     {
  12:         private IProductService _proxy = ServiceProxyFactory.Create<IProductService>("productservice");
  13:  
  14:         #region IProduct Members
  15:         public Product GetProduct(Guid productID)
  16:         {
  17:             Product product = this._proxy.GetProductByID(productID);
  18:             if (product == null)
  19:             { 
  20:                 throw new BusinessException(string.Format("The product whose ID is /"{0}/" does not exist.", productID));
  21:             }
  22:  
  23:             return product;
  24:         }
  25:  
  26:         #endregion
  27:     }
  28: }

调用方式:

   1: Product product = ServiceLoader.LoadService<IProduct>().GetProduct(productID);

从上面的代码可以看到,ProductService的实现需要调用WCF服务,并根据产品ID获取产品信息。如果频繁调用,必然对性能有很大的影响,产品信息是相对稳定的信息,所以可以通过缓存的机制改善应用程序的性能。在PetShop中,我们通过AOP的方式提供对缓存的实现。在此,使用到了微软P&P团队开发的另一个开源AOP框架:Policy Injection Application Block(PIAB)。通过PIAB,仅仅需要在目标类型或目标方法上应用CachingCallHandlerAttribute特性就可以了。CachingCallHandlerAttribute采用基于参数的缓存机制,它的实现原理是这样的:当执行一个应用了CachingCallHandlerAttribute方法的时候,PIAB以传入方法的参数列表为Key,判断缓存中是否有相应的结果,如果有则直接返回而无须执行方法体;如果没有执行方法体,将执行结果进行缓存。通过CachingCallHandlerAttribute还可以设置过期时间,在上面的例子中,将过期时间设为30分钟([CachingCallHandler(0,30,0)])。关于PIAB,在后面还将进行简单的介绍。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
一、Model主要功能: 1、 将每一个“业务实体”抽象成“(瘦数据)类”,可以很好地“划分”各个“对象”,操作更加清晰 2、 用于在应用程序各层之间传递数据,被用做传输数据的“容器” 3、 这就是所谓的“建模”过程! 4、 Model各个类(Model文件夹中的各个文件)划分或者说编制的原则,更趋向于模拟整个系统中的业务实体 二、实现细节: 1、 PetShop中Model的规划与数据库表的关系: (1) AccountInfo类——Account表 (2) AddressInfo类——无直接对应关系(对应Account表中一部分字段) (3) CartItemInfo类——无直接对应关系 (4) CreditCardInfo类——无直接对应关系 (5) ItemInfo类——Item表 (6) LineItemInfo类——无直接对应关系 (7) OrderInfo类——Orders表 (8) ProductInfo类——Product表 2、 为每一个Model中的类都标记了[Serializable],说明这些类可以被传行化,但是不能被继承! 3、 AccountInfo.cs文件:用户在网站注册的信息,及喜好选择情况 4、 使用构造函数可以初始化私有字段;使用属性可以读取私有字段(但使用属性不能设置私有字段的值) 5、 其中包含一个AddressInfo类的私有变量,和一个AddressInfo类的属性 6、 命名空间为PetShop.Model 7、 AddressInfo.cs文件:用户真实的个人姓名、住址和电话号码等信息 8、 与AccountInfo不同的是,AddressInfo类允许使用属性设置私有变量的值 9、 CartItemInfo.cs文件:描述购物车中每一种所选商品的信息的类 10、 该“类”对象的某些信息(如这里的Subtotal属性)可能并不是此类的“自然信息”,而需要经过简单计算而得到!这些简单但必要的信息也要在类的设计中体现出来! 11、 CreditCardInfo.cs文件:表示特定一张信用卡的信息 12、 ItemInfo.cs文件:一个Item指的是category"product"item,如猫"波斯猫"成年男波斯猫(或成年女波斯猫)。这个文件表示一个Item的所有信息 13、 productDesc字段的作用? 14、 LineItemInfo.cs文件:注意与CartItemInfo类的区别!LineItemInfo是用来描述用户最终确认的订单当中的某一种类的产品的信息的类 15、 同样包括了Subtotal属性 16、 OrderInfo.cs文件:用于显示用户某一个订单具体信息的类,在此一个订单当中,可能包括多个商品种类,即包括多个LineItemInfo对象(实际上在OrderInfo类中也确实存在LineItemInfo类型对象的一个数组!) 17、 ProductInfo.cs文件:包括一个特定Product的信息,如波斯猫 三、启发: 1、 来自Directory项目结束后的启发,以后做设计的时候,要将每个实体抽象为一个类,在整个系统中进行操作。 2、 在任意一个类当中,可能不只包括此实体类的自然信息,也可以包括一些对其他地方数据调用有用的属性信息,如根据数量和单价计算出来的总价属性,或者标志此实体的直属上级实体的属性 四、问题: 1、 忽然发现在MSDN上有文章,关于数据实体的:浏览 2、 抽象这些业务实体模型为瘦数据类的原则是什么?什么样的业务实体可以被抽象,或者说进行抽象后更有意义? 3、 ItemInfo类中,productDesc字段的作用? 一、IDAL主要功能: 1、 这完全是“工厂模式”的一部分实现而已 2、 这是一组接口类,其中包括了每个要公开的数据访问方法。为每个数据库产品单独编写的DAL(数据访问层)都要实现这组接口所定义的方法 3、 也就是规定了在DAL中编写的“对用户账号进行操作的类”所必须执行的方法! 4、 IDAL要达到的目的是:实现业务逻辑(BLL)与数据库访问(DAL)的完全分离!!! 5、 IDAL各个类(IDAL文件夹中的各个文件)划分或者说编制的原则,更趋向于“将对数据库的不同操作进行归类”,考虑的主要方面是数据库操作!!!例如,有对用户账号进行的一系列数据库操作,则将这一系列操作统一放置于IAccount接口(将来实现后的Account类)文件当中 二、实现细节: 1、 IAccount.cs文件:为针对不同数据库产品编写的“操作用户账号的类”所必须实现的一系列方法定义契约 2、 IInventory.cs文件:定义“操作库存量的类”所必须实现的一系列方法;或者说将操作库存量的一系列(所有)方法做一个汇总 3、 IItem.cs文件:定义“操作某一Item的类”所必须实现的一系列方法。(Item在Model中定义了,是指具体某一类别的Product,如男猫或女猫) 4、 对于其中的GetItemsByProduct()方法,返回的是一个ArrayList的接口类型(IList)的对象(见问题部分!) 5、 IOrder.cs文件:定义了一组DAL层中“操作用户订单的类”必须执行的方法。其中包括“添加一张新订单”的方法Insert()和根据一个已有的订单号取得此订单详细信息的方法GetOrder(),此方法返回的是一个OrderInfo对象。(Model中的OrderInfo类模型定义了用户的某一张Order中相关的信息,如发货地点,总价,信用卡号码等等) 6、 IProduct.cs文件:定义类一组在DAL层中编写的“对Product进行操作的类” 7、 IProfile.cs文件:定义一组在DAL层编写的“对用户Profile进行操作的类” 三、启发: 1、 这样就可以让在BLL层只针对IDAL层定义的接口进行编程(使用IDAL接口定义的这些方法)就可以了!!!无论在底层使用了什么厂家的数据库产品,有区别的只是针对此数据库产品编写的DAL层(相同的方法,如SignIn()方法,对于不同的数据库产品,可能有不同的实现方式!),而不会影响到上层的BLL层已经编写好的内容! 2、 从这里可以看到软件架构师和程序员工作上的区别!架构师要负责的是搭建系统的层次结构,定义接口;而程序员要负责的是针对接口的具体代码实现过程! 3、 这个IDAL接口的使用,主要是为了保证在底层数据库实现,甚至数据库产品发生变化的时候,不需要对上层BLL层的业务逻辑进行大量的修改!BLL层针对IDAL接口编程即可!!! 4、 IDAL文件夹中定义的所有的接口中的所有的方法,包括了整个程序要对数据库进行操作的所有方法 5、 由于PetShop只是一个演示程序,所以若对数据库操作的某一类别(如对Account进行操作)中定义的“操作类”不够用(如除了接口中定义的几个方法外还需要其他Account操作),还可以在接口中追加其他的方法,用以约束DAL层的实现类(如Account类)必须执行这些新增加的方法! 四、问题: 1、 定义这组接口后,如何保证为每个单独的数据库产品编写的DAL都执行这组接口所定义的方法?(答案:编写DAL的时候必须刻意保证,否则就失去了定义IDAL层分割BLL和DAL层的意义!!!) 2、 既然在实现GetItemsByProduct()方法的时候,也是要把返回的所有Item对象添加到一个ArrayList当中去,但为什么GetItemsByProduct()方法返回的不是一个ArrayList对象,而是一个IList接口的对象呢?仅仅是基类引用的方法吗?这里的基类引用有什么用呢?是否是由于为了不限制使用的数组形式?执行IList则可以使用ArrayList,也可以使用其他形式的数组??? 3、 IProduct.cs文件中为何要将查询条件参数定义为一个string类型的数组?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值