在《WCF技术剖析(卷1)》的最后一章,我写了一个简单基于WCF的Web应用程序,该程序模拟一个最简单的网上订购的场景,所以我将其命名为PetShop。PetShop的目在于让读者体会到在真正的项目开发中,如何正确地、有效地使用WCF。在这个应用中,还会将个人对设计的一些总结融入其中,希望能够对读者有所启发。Source Code从这里下载。
一、PetShop功能简介
PetShop前端是一个单纯的基于ASP.NET应用的Web站点,整个站点由以下三个Web页面构成:
登录页面:和一般的基于Internet的Web站点一样,采用基于用户名/密码的认证方式。在图1所示的登录页面中,实际上仅仅使用了一个Login控件。熟悉ASP.NET的读者应该很清楚,该控件和ASP.NET的成员资格(Membership)模块进行了有效的集成,通过该模块可进行用户验证。
图1 PetShop登录页面
默认页面:PetShop的默认页面为一个宠物的列表,列表项包含宠物的编号、名称、类别、价格、数量和相关介绍。登录的用户可以通过点击“加入购物车”链接进行选购。默认页面的界面如图2所示。
图2 PetShop默认页面
购物车页面:在用户点击默认页面的“加入购物车”链接后,会跳转到购物车页面。如图3所示,该页面列出了当前登录用户购物车中选购的所有宠物列表。用户可以将选购的宠物从购物车中移除,也可以更新选购的数量。
图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所示。
图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项目用于定义一些公用的类型。
图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,在后面还将进行简单的介绍。
WCF版的PetShop之二:模块中的层次划分[提供源代码下载]
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。