现在的位置: 首页 > 综合 > 正文

petshop4.0 详解

2014年01月24日 ⁄ 综合 ⁄ 共 29588字 ⁄ 字号 评论关闭

前言:PetShop是一个范例,微软用它来展示.Net企业系统开发的能力。业界有许多.Net与J2EE之争,许多数据是从微软的PetShop和Sun的PetStore而来。这种争论不可避免带有浓厚的商业色彩,对于我们开发人员而言,没有必要过多关注。然而PetShop随着版本的不断更新,至现在基于.Net 2.0的PetShop4.0为止,整个设计逐渐变得成熟而优雅,却又很多可以借鉴之处。PetShop是一个小型的项目,系统架构与代码都比较简单,却也凸现了许多颇有价值的设计与开发理念。本系列试图对PetShop作一个全方位的解剖,依据的代码是PetShop4.0,可以从链接http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnbda/html/bdasamppet4.asp中获得。  

一、PetShop的系统架构设计

在软件体系架构设计中,分层式结构是最常见,也是最重要的一种结构。微软推荐的分层式结构一般分为三层,从下至上分别为:数据访问层、业务逻辑层(又或成为领域层)、表示层,如图所示:

ps01.gif
图一:三层的分层式结构

数据访问层:有时候也称为是持久层,其功能主要是负责数据库的访问。简单的说法就是实现对数据表的Select,Insert,Update,Delete的操作。如果要加入ORM的元素,那么就会包括对象和数据表之间的mapping,以及对象实体的持久化。在PetShop的数据访问层中,并没有使用ORM,从而导致了代码量的增加,可以看作是整个设计实现中的一大败笔。

业务逻辑层:是整个系统的核心,它与这个系统的业务(领域)有关。以PetShop为例,业务逻辑层的相关设计,均和网上宠物店特有的逻辑相关,例如查询宠物,下订单,添加宠物到购物车等等。如果涉及到数据库的访问,则调用数据访问层。

表示层:是系统的UI部分,负责使用者与整个系统的交互。在这一层中,理想的状态是不应包括系统的业务逻辑。表示层中的逻辑代码,仅与界面元素有关。在PetShop中,是利用ASP.Net来设计的,因此包含了许多Web控件和相关逻辑。

分层式结构究竟其优势何在?Martin Fowler在《Patterns of Enterprise Application Architecture》一书中给出了答案:
1、开发人员可以只关注整个结构中的其中某一层;
2、可以很容易的用新的实现来替换原有层次的实现;
3、可以降低层与层之间的依赖;
4、有利于标准化;
5、利于各层逻辑的复用。

概括来说,分层式设计可以达至如下目的:分散关注、松散耦合、逻辑复用、标准定义。

一个好的分层式结构,可以使得开发人员的分工更加明确。一旦定义好各层次之间的接口,负责不同逻辑设计的开发人员就可以分散关注,齐头并进。例如UI人员只需考虑用户界面的体验与操作,领域的设计人员可以仅关注业务逻辑的设计,而数据库设计人员也不必为繁琐的用户交互而头疼了。每个开发人员的任务得到了确认,开发进度就可以迅速的提高。

松散耦合的好处是显而易见的。如果一个系统没有分层,那么各自的逻辑都紧紧纠缠在一起,彼此间相互依赖,谁都是不可替换的。一旦发生改变,则牵一发而动全身,对项目的影响极为严重。降低层与层间的依赖性,既可以良好地保证未来的可扩展,在复用性上也是优势明显。每个功能模块一旦定义好统一的接口,就可以被各个模块所调用,而不用为相同的功能进行重复地开发。

进行好的分层式结构设计,标准也是必不可少的。只有在一定程度的标准化基础上,这个系统才是可扩展的,可替换的。而层与层之间的通信也必然保证了接口的标准化。

“金无足赤,人无完人”,分层式结构也不可避免具有一些缺陷:
1、降低了系统的性能。这是不言而喻的。如果不采用分层式结构,很多业务可以直接造访数据库,以此获取相应的数据,如今却必须通过中间层来完成。
2、有时会导致级联的修改。这种修改尤其体现在自上而下的方向。如果在表示层中需要增加一个功能,为保证其设计符合分层式结构,可能需要在相应的业务逻辑层和数据访问层中都增加相应的代码。

前面提到,PetShop的表示层是用ASP.Net设计的,也就是说,它应是一个BS系统。在.Net中,标准的BS分层式结构如下图所示:

ps02.gif
图二:.Net中标准的BS分层式结构

随着PetShop版本的更新,其分层式结构也在不断的完善,例如PetShop2.0,就没有采用标准的三层式结构,如图三:

ps03.gif
图三:PetShop 2.0的体系架构

从图中我们可以看到,并没有明显的数据访问层设计。这样的设计虽然提高了数据访问的性能,但也同时导致了业务逻辑层与数据访问的职责混乱。一旦要求支持的数据库发生变化,或者需要修改数据访问的逻辑,由于没有清晰的分层,会导致项目作大的修改。而随着硬件系统性能的提高,以及充分利用缓存、异步处理等机制,分层式结构所带来的性能影响几乎可以忽略不计。

PetShop3.0纠正了此前层次不明的问题,将数据访问逻辑作为单独的一层独立出来:

ps04.gif
图四:PetShop 3.0的体系架构

PetShop4.0基本上延续了3.0的结构,但在性能上作了一定的改进,引入了缓存和异步处理机制,同时又充分利用了ASP.Net 2.0的新功能MemberShip,因此PetShop4.0的系统架构图如下所示:

ps05.gif
图五:PetShop 4.0的体系架构

比较3.0和4.0的系统架构图,其核心的内容并没有发生变化。在数据访问层(DAL)中,仍然采用DAL Interface抽象出数据访问逻辑,并以DAL Factory作为数据访问层对象的工厂模块。对于DAL Interface而言,分别有支持MS-SQL的SQL Server DAL和支持Oracle的Oracle DAL具体实现。而Model模块则包含了数据实体对象。其详细的模块结构图如下所示:

ps06.gif
图六:数据访问层的模块结构图

可以看到,在数据访问层中,完全采用了“面向接口编程”思想。抽象出来的IDAL模块,脱离了与具体数据库的依赖,从而使得整个数据访问层利于数据库迁移。DALFactory模块专门管理DAL对象的创建,便于业务逻辑层访问。SQLServerDAL和OracleDAL模块均实现IDAL模块的接口,其中包含的逻辑就是对数据库的Select,Insert,Update和Delete操作。因为数据库类型的不同,对数据库的操作也有所不同,代码也会因此有所区别。

此外,抽象出来的IDAL模块,除了解除了向下的依赖之外,对于其上的业务逻辑层,同样仅存在弱依赖关系,如下图所示:

ps07.gif
图七:业务逻辑层的模块结构图

图七中BLL是业务逻辑层的核心模块,它包含了整个系统的核心业务。在业务逻辑层中,不能直接访问数据库,而必须通过数据访问层。注意图中对数据访问业务的调用,是通过接口模块IDAL来完成的。既然与具体的数据访问逻辑无关,则层与层之间的关系就是松散耦合的。如果此时需要修改数据访问层的具体实现,只要不涉及到IDAL的接口定义,那么业务逻辑层就不会受到任何影响。毕竟,具体实现的SQLServerDAL和OracalDAL根本就与业务逻辑层没有半点关系。

因为在PetShop 4.0中引入了异步处理机制。插入订单的策略可以分为同步和异步,两者的插入策略明显不同,但对于调用者而言,插入订单的接口是完全一样的,所以PetShop 4.0中设计了IBLLStrategy模块。虽然在IBLLStrategy模块中,仅仅是简单的IOrderStategy,但同时也给出了一个范例和信息,那就是在业务逻辑的处理中,如果存在业务操作的多样化,或者是今后可能的变化,均应利用抽象的原理。或者使用接口,或者使用抽象类,从而脱离对具体业务的依赖。不过在PetShop中,由于业务逻辑相对简单,这种思想体现得不够明显。也正因为此,PetShop将核心的业务逻辑都放到了一个模块BLL中,并没有将具体的实现和抽象严格的按照模块分开。所以表示层和业务逻辑层之间的调用关系,其耦合度相对较高:

ps08.gif
图八:表示层的模块结构图

在图五中,各个层次中还引入了辅助的模块,如数据访问层的Messaging模块,是为异步插入订单的功能提供,采用了MSMQ(Microsoft Messaging Queue)技术。而表示层的CacheDependency则提供缓存功能。这些特殊的模块,我会在此后的文章中详细介绍。 

在系列一中,我从整体上分析了PetShop的架构设计,并提及了分层的概念。从本部分开始,我将依次对各层进行代码级的分析,以求获得更加细致而深入的理解。在PetShop 4.0中,由于引入了ASP.Net 2.0的一些新特色,所以数据层的内容也更加的广泛和复杂,包括:数据库访问、Messaging、MemberShip、Profile四部分。在系列二中,我将介绍有关数据库访问的设计。

在PetShop中,系统需要处理的数据库对象分为两类:一是数据实体,对应数据库中相应的数据表。它们没有行为,仅用于表现对象的数据。这些实体类都被放到Model程序集中,例如数据表Order对应的实体类OrderInfo,其类图如下: 

ps09.gif

这些对象并不具有持久化的功能,简单地说,它们是作为数据的载体,便于业务逻辑针对相应数据表进行读/写操作。虽然这些类的属性分别映射了数据表的列,而每一个对象实例也恰恰对应于数据表的每一行,但这些实体类却并不具备对应的数据库访问能力。

由于数据访问层和业务逻辑层都将对这些数据实体进行操作,因此程序集Model会被这两层的模块所引用。

第二类数据库对象则是数据的业务逻辑对象。这里所指的业务逻辑,并非业务逻辑层意义上的领域(domain)业务逻辑(从这个意义上,我更倾向于将业务逻辑层称为“领域逻辑层”),一般意义上说,这些业务逻辑即为基本的数据库操作,包括Select,Insert,Update和Delete。由于这些业务逻辑对象,仅具有行为而与数据无关,因此它们均被抽象为一个单独的接口模块IDAL,例如数据表Order对应的接口IOrder: 

ps10.gif

将数据实体与相关的数据库操作分离出来,符合面向对象的精神。首先,它体现了“职责分离”的原则。将数据实体与其行为分开,使得两者之间依赖减弱,当数据行为发生改变时,并不影响Model模块中的数据实体对象,避免了因一个类职责过多、过大,从而导致该类的引用者发生“灾难性”的影响。其次,它体现了“抽象”的精神,或者说是“面向接口编程”的最佳体现。抽象的接口模块IDAL,与具体的数据库访问实现完全隔离。这种与实现无关的设计,保证了系统的可扩展性,同时也保证了数据库的可移植性。在PetShop中,可以支持SQL Server和Oracle,那么它们具体的实现就分别放在两个不同的模块SQLServerDAL、OracleDAL中。

以Order为例,在SQLServerDAL、OracleDAL两个模块中,有不同的实现,但它们同时又都实现了IOrder接口,如图: 

ps11.gif

从数据库的实现来看,PetShop体现出了没有ORM框架的臃肿与丑陋。由于要对数据表进行Insert和Select操作,以SQL Server为例,就使用了SqlCommand,SqlParameter,SqlDataReader等对象,以完成这些操作。尤其复杂的是Parameter的传递,在PetShop中,使用了大量的字符串常量来保存参数的名称。此外,PetShop还专门为SQL Server和Oracle提供了抽象的Helper类,包装了一些常用的操作,如ExecuteNonQuery、ExecuteReader等方法。

在没有ORM的情况下,使用Helper类是一个比较好的策略,利用它来完成数据库基本操作的封装,可以减少很多和数据库操作有关的代码,这体现了对象复用的原则。PetShop将这些Helper类统一放到DBUtility模块中,不同数据库的Helper类暴露的方法基本相同,只除了一些特殊的要求,例如Oracle中处理bool类型的方式就和SQL Server不同,从而专门提供了OraBit和OraBool方法。此外,Helper类中的方法均为static方法,以利于调用。OracleHelper的类图如下: 

ps12.gif

对于数据访问层来说,最头疼的是SQL语句的处理。在早期的CS结构中,由于未采用三层式架构设计,数据访问层和业务逻辑层是紧密糅合在一起的,因此,SQL语句遍布与系统的每一个角落。这给程序的维护带来极大的困难。此外,由于Oracle使用的是PL-SQL,而SQL Server和Sybase等使用的是T-SQL,两者虽然都遵循了标准SQL的语法,但在很多细节上仍有区别,如果将SQL语句大量的使用到程序中,无疑为可能的数据库移植也带来了困难。

最好的方法是采用存储过程。这种方法使得程序更加整洁,此外,由于存储过程可以以数据库脚本的形式存在,也便于移植和修改。但这种方式仍然有缺陷。一是存储过程的测试相对困难。虽然有相应的调试工具,但比起对代码的调试而言,仍然比较复杂且不方便。二是对系统的更新带来障碍。如果数据库访问是由程序完成,在.Net平台下,我们仅需要在修改程序后,将重新编译的程序集xcopy到部署的服务器上即可。如果使用了存储过程,出于安全的考虑,必须有专门的DBA重新运行存储过程的脚本,部署的方式受到了限制。

我曾经在一个项目中,利用一个专门的表来存放SQL语句。如要使用相关的SQL语句,就利用关键字搜索获得对应语句。这种做法近似于存储过程的调用,但却避免了部署上的问题。然而这种方式却在性能上无法得到保证。它仅适合于SQL语句较少的场景。不过,利用良好的设计,我们可以为各种业务提供不同的表来存放SQL语句。同样的道理,这些SQL语句也可以存放到XML文件中,更有利于系统的扩展或修改。不过前提是,我们需要为它提供专门的SQL语句管理工具。

SQL语句的使用无法避免,如何更好的应用SQL语句也无定论,但有一个原则值得我们遵守,就是“应该尽量让SQL语句尽存在于数据访问层的具体实现中”。

当然,如果应用ORM,那么一切就变得不同了。因为ORM框架已经为数据访问提供了基本的Select,Insert,Update和Delete操作了。例如在NHibernate中,我们可以直接调用ISession对象的Save方法,来Insert(或者说是Create)一个数据实体对象:
public void Insert(OrderInfo order)
{
    ISession s = Sessions.GetSession();
    ITransaction trans = null;
    try
    {
    trans = s.BeginTransaction();
      s.Save( order);
      trans.Commit();
    }
    finally
    {
      s.Close();
    }
}

没有SQL语句,也没有那些烦人的Parameters,甚至不需要专门去考虑事务。此外,这样的设计,也是与数据库无关的,NHibernate可以通过Dialect(方言)的机制支持不同的数据库。唯一要做的是,我们需要为OrderInfo定义hbm文件。

当然,ORM框架并非是万能的,面对纷繁复杂的业务逻辑,它并不能完全消灭SQL语句,以及替代复杂的数据库访问逻辑,但它却很好的体现了“80/20(或90/10)法则”(也被称为“帕累托法则”),也就是说:花比较少(10%-20%)的力气就可以解决大部分(80%-90%)的问题,而要解决剩下的少部分问题则需要多得多的努力。至少,那些在数据访问层中占据了绝大部分的CRUD操作,通过利用ORM框架,我们就仅需要付出极少数时间和精力来解决它们了。这无疑缩短了整个项目开发的周期。

还是回到对PetShop的讨论上来。现在我们已经有了数据实体,数据对象的抽象接口和实现,可以说有关数据库访问的主体就已经完成了。留待我们的还有两个问题需要解决:
1、数据对象创建的管理
2、利于数据库的移植

在PetShop中,要创建的数据对象包括Order,Product,Category,Inventory,Item。在前面的设计中,这些对象已经被抽象为对应的接口,而其实现则根据数据库的不同而有所不同。也就是说,创建的对象有多种类别,而每种类别又有不同的实现,这是典型的抽象工厂模式的应用场景。而上面所述的两个问题,也都可以通过抽象工厂模式来解决。标准的抽象工厂模式类图如下: 

ps13.gif

例如,创建SQL Server的Order对象如下:
PetShopFactory factory = new SQLServerFactory();
IOrder = factory.CreateOrder();

要考虑到数据库的可移植性,则factory必须作为一个全局变量,并在主程序运行时被实例化。但这样的设计虽然已经达到了“封装变化”的目的,但在创建PetShopFactory对象时,仍不可避免的出现了具体的类SQLServerFactory,也即是说,程序在这个层面上产生了与SQLServerFactory的强依赖。一旦整个系统要求支持Oracle,那么还需要修改这行代码为:
PetShopFactory factory = new oracleFactory();

修改代码的这种行为显然是不可接受的。解决的办法是“依赖注入”。“依赖注入”的功能通常是用专门的IoC容器提供的,在Java平台下,这样的容器包括Spring,PicoContainer等。而在.Net平台下,最常见的则是Spring.Net。不过,在PetShop系统中,并不需要专门的容器来实现“依赖注入”,简单的做法还是利用配置文件和反射功能来实现。也就是说,我们可以在web.config文件中,配置好具体的Factory对象的完整的类名。然而,当我们利用配置文件和反射功能时,具体工厂的创建就显得有些“画蛇添足”了,我们完全可以在配置文件中,直接指向具体的数据库对象实现类,例如PetShop.SQLServerDAL.IOrder。那么,抽象工厂模式中的相关工厂就可以简化为一个工厂类了,所以我将这种模式称之为“具有简单工厂特质的抽象工厂模式”,其类图如下: 

ps14.gif

DataAccess类完全取代了前面创建的工厂类体系,它是一个sealed类,其中创建各种数据对象的方法,均为静态方法。之所以能用这个类达到抽象工厂的目的,是因为配置文件和反射的运用,如下的代码片断所示:
public sealed class DataAccess
{
 // Look up the DAL implementation we should be using
    private static readonly string path = ConfigurationManager.AppSettings[”WebDAL”];
    private static readonly string orderPath = ConfigurationManager.AppSettings[”OrdersDAL”];

 public static PetShop.IDAL.IOrder CreateOrder()
 {
         string className = orderPath + “.Order”;
         return (PetShop.IDAL.IOrder)Assembly.Load(orderPath).CreateInstance(className);
    }
}

在PetShop中,这种依赖配置文件和反射创建对象的方式极其常见,包括IBLLStategy、CacheDependencyFactory等等。这些实现逻辑散布于整个PetShop系统中,在我看来,是可以在此基础上进行重构的。也就是说,我们可以为整个系统提供类似于“Service Locator”的实现:
public static class ServiceLocator
{
 private static readonly string dalPath = ConfigurationManager.AppSettings[”WebDAL”];
    private static readonly string orderPath = ConfigurationManager.AppSettings[”OrdersDAL”];
 //……
 private static readonly string orderStategyPath = ConfigurationManager.AppSettings[”OrderStrategyAssembly”];

 public static object LocateDALObject(string className)
 {
  string fullPath = dalPath + “.” + className;
  return Assembly.Load(dalPath).CreateInstance(fullPath);
 }
public static object LocateDALOrderObject(string className)
 {
  string fullPath = orderPath + “.” + className;
  return Assembly.Load(orderPath).CreateInstance(fullPath);
 }
public static object LocateOrderStrategyObject(string className)
 {
  string fullPath = orderStategyPath + “.” + className;
  return Assembly.Load(orderStategyPath).CreateInstance(fullPath);
 }
 //……
}

那么和所谓“依赖注入”相关的代码都可以利用ServiceLocator来完成。例如类DataAccess就可以简化为:
public sealed class DataAccess
{
 public static PetShop.IDAL.IOrder CreateOrder()
 {
         return (PetShop.IDAL.IOrder)ServiceLocator. LocateDALOrderObject(”Order”);
    }
}

通过ServiceLocator,将所有与配置文件相关的namespace值统一管理起来,这有利于各种动态创建对象的管理和未来的维护。

 在进行系统设计时,除了对安全、事务等问题给与足够的重视外,性能也是一个不可避免的问题所在,尤其是一个B/S结构的软件系统,必须充分地考虑访问量、数据流量、服务器负荷的问题。解决性能的瓶颈,除了对硬件系统进行升级外,软件设计的合理性尤为重要。
在前面我曾提到,分层式结构设计可能会在一定程度上影响数据访问的性能,然而与它给设计人员带来的好处相比,几乎可以忽略。要提供整个系统的性能,还可以从数据库的优化着手,例如连接池的使用、建立索引、优化查询策略等等,例如在PetShop中就利用了数据库的Cache,对于数据量较大的订单数据,则利用分库的方式为其单独建立了Order和Inventory数据库。而在软件设计上,比较有用的方式是利用多线程与异步处理方式。
在PetShop4.0中,使用了Microsoft Messaging Queue(MSMQ)技术来完成异步处理,利用消息队列临时存放要插入的数据,使得数据访问因为不需要访问数据库从而提供了访问性能,至于队列中的数据,则等待系统空闲的时候再进行处理,将其最终插入到数据库中。
PetShop4.0中的消息处理,主要分为如下几部分:消息接口IMessaging、消息工厂MessagingFactory、MSMQ实现MSMQMessaging以及数据后台处理应用程序OrderProcessor。
从模块化分上,PetShop自始自终地履行了“面向接口设计”的原则,将消息处理的接口与实现分开,并通过工厂模式封装消息实现对象的创建,以达到松散耦合的目的。
由于在PetShop中仅对订单的处理使用了异步处理方式,因此在消息接口IMessaging中,仅定义了一个IOrder接口,其类图如下:
 ps01.gif
在对消息接口的实现中,考虑到未来的扩展中会有其他的数据对象会使用MSMQ,因此定义了一个Queue的基类,实现消息Receive和Send的基本操作:
public virtual object Receive() 
{
      try 
{
          using (Message message = queue.Receive(timeout, transactionType))
             return message;
      }
      catch (MessageQueueException mqex) 
{
          if (mqex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout)
             throw new TimeoutException();
                throw;
      }
}
public virtual void Send(object msg) 
{
      queue.Send(msg, transactionType);
}
其中queue对象是System.Messaging.MessageQueue类型,作为存放数据的队列。MSMQ队列是一个可持久的队列,因此不必担心用户不间断地下订单会导致订单数据的丢失。在PetShopQueue设置了timeout值,OrderProcessor会根据timeout值定期扫描队列中的订单数据。
MSMQMessaging模块中,Order对象实现了IMessaging模块中定义的接口IOrder,同时它还继承了基类PetShopQueue,其定义如下:
public class order:PetShopQueue, PetShop.IMessaging.IOrder
方法的实现代码如下:
    public new orderInfo Receive() 
    {
        // This method involves in distributed transaction and need Automatic Transaction type
        base.transactionType = MessageQueueTransactionType.Automatic;
        return (OrderInfo)((Message)base.Receive()).Body;
    }

    public orderInfo Receive(int timeout) 
    {
        base.timeout = TimeSpan.FromSeconds(Convert.ToDouble(timeout));
        return Receive();
    }

    public void Send(OrderInfo orderMessage) 
    {
        // This method does not involve in distributed transaction and optimizes performance using Single type
        base.transactionType = MessageQueueTransactionType.Single;
        base.Send(orderMessage);
    }
所以,最后的类图应该如下:
 ps02.gif
注意在Order类的Receive()方法中,是用new关键字而不是override关键字来重写其父类PetShopQueue的Receive()虚方法。因此,如果是实例化如下的对象,将会调用PetShopQueue的Receive()方法,而不是子类Order的Receive()方法:
PetShopQueue queue = new order();
queue.Receive();
从设计上来看,由于PetShop采用“面向接口设计”的原则,如果我们要创建Order对象,应该采用如下的方式:
IOrder order = new order();
order.Receive();
考虑到IOrder的实现有可能的变化,PetShop仍然利用了工厂模式,将IOrder对象的创建用专门的工厂模块进行了封装:
 ps03.gif
在类QueueAccess中,通过CreateOrder()方法利用反射技术创建正确的IOrder类型对象:
    public static PetShop.IMessaging.IOrder CreateOrder() 
    {
        string className = path + ".Order";
        return PetShop.IMessaging.IOrder)Assembly.Load(path).CreateInstance(className);
    }
path的值通过配置文件获取:
private static readonly string path = ConfigurationManager.AppSettings["OrderMessaging"];
而配置文件中,OrderMessaging的值设置如下:
<add key="OrderMessaging" value="PetShop.MSMQMessaging"/>
之所以利用工厂模式来负责对象的创建,是便于在业务层中对其调用,例如在BLL模块中OrderAsynchronous类:
public class orderAsynchronous : IOrderStrategy
{        
    private static readonly PetShop.IMessaging.IOrder asynchOrder = PetShop.MessagingFactory.QueueAccess.CreateOrder();
    public void Insert(PetShop.Model.OrderInfo order) 
{
        asynchOrder.Send(order);
    }
}
一旦IOrder接口的实现发生变化,这种实现方式就可以使得客户仅需要修改配置文件,而不需要修改代码,如此就可以避免程序集的重新编译和部署,使得系统能够灵活应对需求的改变。例如定义一个实现IOrder接口的SpecialOrder,则可以新增一个模块,如PetShop.SpecialMSMQMessaging,而类名则仍然为Order,那么此时我们仅需要修改配置文件中OrderMessaging的值即可:
<add key="OrderMessaging" value="PetShop.SpecialMSMQMessaging"/>
OrderProcessor是一个控制台应用程序,不过可以根据需求将其设计为Windows Service。它的目的就是接收消息队列中的订单数据,然后将其插入到Order和Inventory数据库中。它利用了多线程技术,以达到提高系统性能的目的。
在OrderProcessor应用程序中,主函数Main用于控制线程,而核心的执行任务则由方法ProcessOrders()实现:
    private static void ProcessOrders() 
    {
        // the transaction timeout should be long enough to handle all of orders in the batch
        TimeSpan tsTimeout = TimeSpan.FromSeconds(Convert.ToDouble(transactionTimeout * batchSize));

        order order = new order();
        while (true) 
        {
            // queue timeout variables
            TimeSpan datetimeStarting = new TimeSpan(DateTime.Now.Ticks);
            double elapsedTime = 0;

            int processedItems = 0;

            ArrayList queueOrders = new ArrayList();

            using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Required, tsTimeout)) 
            {
                // Receive the orders from the queue
                for (int j = 0; j < batchSize; j++) 
                {
                    try 
                    {
                        //only receive more queued orders if there is enough time
                        if ((elapsedTime + queueTimeout + transactionTimeout) < tsTimeout.TotalSeconds)
                        {
                            queueOrders.Add(order.ReceiveFromQueue(queueTimeout));
                        }
                        else 
                        {
                            j = batchSize;   // exit loop
                        }

                        //update elapsed time
                        elapsedTime = new TimeSpan(DateTime.Now.Ticks).TotalSeconds - datetimeStarting.TotalSeconds;
                    }
                    catch (TimeoutException) 
                    {
                        //exit loop because no more messages are waiting
                        j = batchSize;
                    }
                }
                //process the queued orders
                for (int k = 0; k < queueOrders.Count; k++) 
                {
                    order.Insert((OrderInfo)queueOrders[k]);
                    processedItems++;
                    totalOrdersProcessed++;
                }

                //batch complete or MSMQ receive timed out
                ts.Complete();
            }

            Console.WriteLine("(Thread Id " + Thread.CurrentThread.ManagedThreadId + ") batch finished, " + processedItems + " items, in " + elapsedTime.ToString() + " seconds.");
        }
    }
首先,它会通过PetShop.BLL.Order类的公共方法ReceiveFromQueue()来获取消息队列中的订单数据,并将其放入到一个ArrayList对象中,然而再调用PetShop.BLL.Order类的Insert方法将其插入到Order和Inventory数据库中。
在PetShop.BLL.Order类中,并不是直接执行插入订单的操作,而是调用了IOrderStrategy接口的Insert()方法:
public void Insert(OrderInfo order) 
{
    // Call credit card procesor
    ProcessCreditCard(order);

    // Insert the order (a)synchrounously based on configuration
    orderInsertStrategy.Insert(order);
}
在这里,运用了一个策略模式,类图如下所示:
 ps05.gif
在PetShop.BLL.Order类中,仍然利用配置文件来动态创建IOrderStategy对象:
private static readonly PetShop.IBLLStrategy.IOrderStrategy orderInsertStrategy = LoadInsertStrategy();
private static PetShop.IBLLStrategy.IOrderStrategy LoadInsertStrategy() 
{
    // Look up which strategy to use from config file
    string path = ConfigurationManager.AppSettings["OrderStrategyAssembly"];
    string className = ConfigurationManager.AppSettings["OrderStrategyClass"];

    // Using the evidence given in the config file load the appropriate assembly and class
    return (PetShop.IBLLStrategy.IOrderStrategy)Assembly.Load(path).CreateInstance(className);
}
由于OrderProcessor是一个单独的应用程序,因此它使用的配置文件与PetShop不同,是存放在应用程序的App.config文件中,在该文件中,对IOrderStategy的配置为:
<add key="OrderStrategyAssembly" value="PetShop.BLL" />
<add key="OrderStrategyClass" value="PetShop.BLL.OrderSynchronous" />
因此,以异步方式插入订单的流程如下图所示:
 ps06.gif
Microsoft Messaging Queue(MSMQ)技术除用于异步处理以外,它主要还是一种分布式处理技术。分布式处理中,一个重要的技术要素就是有关消息的处理,而在System.Messaging命名空间中,已经提供了Message类,可以用于承载消息的传递,前提上消息的发送方与接收方在数据定义上应有统一的接口规范。
MSMQ在分布式处理的运用,在我参与的项目中已经有了实现。在为一个汽车制造商开发一个大型系统时,分销商Dealer作为.Net客户端,需要将数据传递到管理中心,并且该数据将被Oracle的EBS(E-Business System)使用。由于分销商管理系统(DMS)采用的是C/S结构,数据库为SQL Server,而汽车制造商管理中心的EBS数据库为Oracle。这里就涉及到两个系统之间数据的传递。
实现架构如下:
ps07.gif
     首先Dealer的数据通过MSMQ传递到MSMQ Server,此时可以将数据插入到SQL Server数据库中,同时利用FTP将数据传送到专门的文件服务器上。然后利用IBM的EAI技术(企业应用集成,Enterprise Application Itegration)定期将文件服务器中的文件,利用接口规范写入到EAI数据库服务器中,并最终写道EBS的Oracle数据库中。
上述架构是一个典型的分布式处理结构,而技术实现的核心就是MSMQ和EAI。由于我们已经定义了统一的接口规范,在通过消息队列形成文件后,此时的数据就已经与平台无关了,使得在.Net平台下的分销商管理系统能够与Oracle的EBS集成起来,完成数据的处理。 

如果对微型计算机硬件系统有足够的了解,那么我们对于Cache这个名词一定是耳熟能详的。在CPU以及主板的芯片中,都引入了这种名为高速缓冲存储器(Cache)的技术。因为Cache的存取速度比内存快,因而引入Cache能够有效的解决CPU与内存之间的速度不匹配问题。硬件系统可以利用Cache存储CPU访问概率高的那些数据,当CPU需要访问这些数据时,可以直接从Cache中读取,而不必访问存取速度相对较慢的内存,从而提高了CPU的工作效率。软件设计借鉴了硬件设计中引入缓存的机制以改善整个系统的性能,尤其是对于一个数据库驱动的Web应用程序而言,缓存的利用是不可或缺的,毕竟,数据库查询可能是整个Web站点中调用最频繁但同时又是执行最缓慢的操作之一,我们不能被它老迈的双腿拖缓我们前进的征程。缓存机制正是解决这一缺陷的加速器。

4.1  ASP.NET缓存概述

作为.Net框架下开发Web应用程序的主打产品,ASP.NET充分考虑了缓存机制。通过某种方法,将系统需要的数据对象、Web页面存储在内存中,使得Web站点在需要获取这些数据时,不需要经过繁琐的数据库连接、查询和复杂的逻辑运算,就可以“触手可及”,如“探囊取物”般容易而快速,从而提高整个Web系统的性能。

ASP.NET提供了两种基本的缓存机制来提供缓存功能。一种是应用程序缓存,它允许开发者将程序生成的数据或报表业务对象放入缓存中。另外一种缓存机制是页输出缓存,利用它,可以直接获取存放在缓存中的页面,而不需要经过繁杂的对该页面的再次处理。

应用程序缓存其实现原理说来平淡无奇,仅仅是通过ASP.NET管理内存中的缓存空间。放入缓存中的应用程序数据对象,以键/值对的方式存储,这便于用户在访问缓存中的数据项时,可以根据key值判断该项是否存在缓存中。

放入在缓存中的数据对象其生命周期是受到限制的,即使在整个应用程序的生命周期里,也不能保证该数据对象一直有效。ASP.NET可以对应用程序缓存进行管理,例如当数据项无效、过期或内存不足时移除它们。此外,调用者还可以通过CacheItemRemovedCallback委托,定义回调方法使得数据项被移除时能够通知用户。

在.Net Framework中,应用程序缓存通过System.Web.Caching.Cache类实现。它是一个密封类,不能被继承。对于每一个应用程序域,都要创建一个Cache类的实例,其生命周期与应用程序域的生命周期保持一致。我们可以利用Add或Insert方法,将数据项添加到应用程序缓存中,如下所示:
Cache["First"] = "First Item";
Cache.Insert("Second", "Second Item");

我们还可以为应用程序缓存添加依赖项,使得依赖项发生更改时,该数据项能够从缓存中移除:
string[] dependencies = {"Second"};
Cache.Insert("Third", "Third Item",
new System.Web.Caching.CacheDependency(null, dependencies));

与之对应的是缓存中数据项的移除。前面提到ASP.NET可以自动管理缓存中项的移除,但我们也可以通过代码编写的方式显式的移除相关的数据项:
Cache.Remove("First");

相对于应用程序缓存而言,页输出缓存的应用更为广泛。它可以通过内存将处理后的ASP.NET页面存储起来,当客户端再一次访问该页面时,可以省去页面处理的过程,从而提高页面访问的性能,以及Web服务器的吞吐量。例如,在一个电子商务网站里,用户需要经常查询商品信息,这个过程会涉及到数据库访问以及搜索条件的匹配,在数据量较大的情况下,如此的搜索过程是较为耗时的。此时,利用页输出缓存就可以将第一次搜索得到的查询结果页存储在缓存中。当用户第二次查询时,就可以省去数据查询的过程,减少页面的响应时间。

页输出缓存分为整页缓存和部分页缓存。我们可以通过@OutputCache指令完成对Web页面的输出缓存。它主要包含两个参数:Duration和VaryByParam。Duration参数用于设置页面或控件进行缓存的时间,其单位为秒。如下的设置表示缓存在60秒内有效:
<%@ OutputCache Duration=“60“ VaryByParam=“none“ %>

只要没有超过Duration设置的期限值,当用户访问相同的页面或控件时,就可以直接在缓存中获取。
使用VaryByParam参数可以根据设置的参数值建立不同的缓存。例如在一个输出天气预报结果的页面中,如果需要为一个ID为txtCity的TextBox控件建立缓存,其值将显示某城市的气温,那么我们可以进行如下的设置:
<%@ OutputCache Duration=”60” VaryByParam=”txtCity” %>

如此一来,ASP.NET会对txtCity控件的值进行判断,只有输入的值与缓存值相同,才从缓存中取出相应的值。这就有效地避免了因为值的不同而导致输出错误的数据。

利用缓存的机制对性能的提升非常明显。通过ACT(Application Center Test)的测试,可以发现设置缓存后执行的性能比未设置缓存时的性能足足提高三倍多。

引入缓存看来是提高性能的“完美”解决方案,然而“金无足赤,人无完人”,缓存机制也有缺点,那就是数据过期的问题。一旦应用程序数据或者页面结果值发生的改变,那么在缓存有效期范围内,你所获得的结果将是过期的、不准确的数据。我们可以想一想股票系统利用缓存所带来的灾难,当你利用错误过期的数据去分析股市的风云变幻时,你会发现获得的结果真可以说是“失之毫厘,谬以千里”,看似大好的局面就会像美丽的泡沫一样,用针一戳,转眼就消失得无影无踪。

那么我们是否应该为了追求高性能,而不顾所谓“数据过期”所带来的隐患呢?显然,在类似于股票系统这种数据更新频繁的特定场景下,数据过期的糟糕表现甚至比低效的性能更让人难以接受。故而,我们需要在性能与数据正确性间作出权衡。所幸的是,.Net Framework 2.0引入了一种新的缓存机制,它为我们的“鱼与熊掌兼得”带来了技术上的可行性。

.Net 2.0引入的自定义缓存依赖项,特别是基于MS-SQL Server的SqlCacheDependency特性,使得我们可以避免“数据过期”的问题,它能够根据数据库中相应数据的变化,通知缓存,并移除那些过期的数据。事实上,在PetShop 4.0中,就充分地利用了SqlCacheDependency特性。

4.2 SqlCacheDependency特性

SqlCacheDependency特性实际上是通过System.Web.Caching.SqlCacheDependency类来体现的。通过该类,可以在所有支持的SQL Server版本(7.0,2000,2005)上监视特定的SQL Server数据库表,并创建依赖于该表以及表中数据行的缓存项。当数据表或表中特定行的数据发生更改时,具有依赖项的数据项就会失效,并自动从Cache中删除该项,从而保证了缓存中不再保留过期的数据。
由于版本的原因,SQL Server 2005完全支持SqlCacheDependency特性,但对于SQL Server 7.0和SQL Server 2000而言,就没有如此幸运了。毕竟这些产品出现在.Net Framework 2.0之前,因此它并没有实现自动监视数据表数据变化,通知ASP.NET的功能。解决的办法就是利用轮询机制,通过ASP.NET进程内的一个线程以指定的时间间隔轮询SQL Server数据库,以跟踪数据的变化情况。

要使得7.0或者2000版本的SQL Server支持SqlCacheDependency特性,需要对数据库服务器执行相关的配置步骤。有两种方法配置SQL Server:使用aspnet_regsql命令行工具,或者使用SqlCacheDependencyAdmin类。

4.2.1  利用aspnet_regsql工具

aspnet_regsql工具位于Windows/Microsoft.NET/Framework/[版本]文件夹中。如果直接双击该工具的执行文件,会弹出一个向导对话框,提示我们完成相应的操作:

4-1.gif
图4-1 aspnet_regsql工具

如图4-1所示中的提示信息,说明该向导主要用于配置SQL Server数据库,如membership,profiles等信息,如果要配置SqlCacheDependency,则需要以命令行的方式执行。以PetShop 4.0为例,数据库名为MSPetShop4,则命令为:
aspnet_regsql -S localhost -E -d MSPetShop4 -ed

以下是该工具的命令参数说明:
-?  显示该工具的帮助功能;
-S  后接的参数为数据库服务器的名称或者IP地址;
-U  后接的参数为数据库的登陆用户名;
-P  后接的参数为数据库的登陆密码;
-E  当使用windows集成验证时,使用该功能;
-d  后接参数为对哪一个数据库采用SqlCacheDependency功能;
-t  后接参数为对哪一个表采用SqlCacheDependency功能;
-ed  允许对数据库使用SqlCacheDependency功能;
-dd  禁止对数据库采用SqlCacheDependency功能;
-et  允许对数据表采用SqlCacheDependency功能;
-dt  禁止对数据表采用SqlCacheDependency功能;
-lt  列出当前数据库中有哪些表已经采用sqlcachedependency功能。

以上面的命令为例,说明将对名为MSPetShop4的数据库采用SqlCacheDependency功能,且SQL Server采用了windows集成验证方式。我们还可以对相关的数据表执行aspnet_regsql命令,如:
aspnet_regsql -S localhost -E -d MSPetShop4 -t Item -et
aspnet_regsql -S localhost -E -d MSPetShop4 -t Product -et
aspnet_regsql -S localhost -E -d MSPetShop4 -t Category -et

当执行上述的四条命令后,aspnet_regsql工具会在MSPetShop4数据库中建立一个名为AspNet_SqlCacheTablesForChangeNotification的新数据库表。该数据表包含三个字段。字段tableName记录要追踪的数据表的名称,例如在PetShop 4.0中,要记录的数据表就包括Category、Item和Product。notificationCreated字段记录开始追踪的时间。changeId作为一个类型为int的字段,用于记录数据表数据发生变化的次数。如图4-2所示:

4-2.gif
图4-2 AspNet_SqlCacheTablesForChangeNotification数据表

除此之外,执行该命令还会为MSPetShop4数据库添加一组存储过程,为ASP.NET提供查询追踪的数据表的情况,同时还将为使用了SqlCacheDependency的表添加触发器,分别对应Insert、Update、Delete等与数据更改相关的操作。例如Product数据表的触发器:
Create TRIGGER dbo.[Product_AspNet_SqlCacheNotification_Trigger] ON [Product]
    FOR Insert, Update, Delete AS BEGIN
    SET NOCOUNT ON
    EXEC dbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure N'Product'
END

其中,AspNet_SqlCacheUpdateChangeIdStoredProcedure即是工具添加的一组存储过程中的一个。当对Product数据表执行Insert、Update或Delete等操作时,就会激活触发器,然后执行AspNet_SqlCacheUpdateChangeIdStoredProcedure存储过程。其执行的过程就是修改AspNet_SqlCacheTablesForChangeNotification数据表的changeId字段值:
Create PROCEDURE dbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure
             @tableName NVARCHAR(450)
         AS
         BEGIN
             Update dbo.AspNet_SqlCacheTablesForChangeNotification WITH (ROWLOCK) SET changeId = changeId + 1
             Where tableName = @tableName
         END  
GO

4.2.2  利用SqlCacheDependencyAdmin类

我们也可以利用编程的方式来来管理数据库对SqlCacheDependency特性的使用。该类包含了五个重要的方法:

DisableNotifications
为特定数据库禁用 SqlCacheDependency对象更改通知
DisableTableForNotifications
为数据库中的特定表禁用SqlCacheDependency对象更改通知
EnableNotifications
为特定数据库启用SqlCacheDependency对象更改通知
EnableTableForNotifications
为数据库中的特定表启用SqlCacheDependency对象更改通知
GetTablesEnabledForNotifications
返回启用了SqlCacheDependency对象更改通知的所有表的列表

表4-1 SqlCacheDependencyAdmin类的主要方法

假设我们定义了如下的数据库连接字符串:
const string connectionStr = "Server=localhost;Database=MSPetShop4";

那么为数据库MSPetShop4启用SqlCacheDependency对象更改通知的实现为:
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
   {
       SqlCacheDependencyAdmin.EnableNotifications(connectionStr);
   }
}

为数据表Product启用SqlCacheDependency对象更改通知的实现则为:
SqlCacheDependencyAdmin.EnableTableForNotifications(connectionStr, "Product");

如果要调用表4-1中所示的相关方法,需要注意的是访问SQL Server数据库的帐户必须具有创建表和存储过程的权限。如果要调用EnableTableForNotifications方法,还需要具有在该表上创建SQL Server触发器的权限。

虽然说编程方式赋予了程序员更大的灵活性,但aspnet_regsql工具却提供了更简单的方法实现对SqlCacheDependency的配置与管理。PetShop 4.0采用的正是aspnet_regsql工具的办法,它编写了一个文件名为InstallDatabases.cmd的批处理文件,其中包含了对aspnet_regsql工具的执行,并通过安装程序去调用该文件,实现对SQL Server的配置。

4.3 在PetShop 4.0中ASP.NET缓存的实现

PetShop作为一个B2C的宠物网上商店,需要充分考虑访客的用户体验,如果因为数据量大而导致Web服务器的响应不及时,页面和查询数据迟迟得不到结果,会因此而破坏客户访问网站的心情,在耗尽耐心的等待后,可能会失去这一部分客户。无疑,这是非常糟糕的结果。因而在对其进行体系架构设计时,整个系统的性能就显得殊为重要。然而,我们不能因噎废食,因为专注于性能而忽略数据的正确性。在PetShop 3.0版本以及之前的版本,因为ASP.NET缓存的局限性,这一问题并没有得到很好的解决。PetShop 4.0则引入了SqlCacheDependency特性,使得系统对缓存的处理较之以前大为改观。

4.3.1  CacheDependency接口

PetShop 4.0引入了SqlCacheDependency特性,对Category、Product和Item数据表对应的缓存实施了SQL Cache Invalidation技术。当对应的数据表数据发生更改后,该技术能够将相关项从缓存中移除。实现这一技术的核心是SqlCacheDependency类,它继承了CacheDependency类。然而为了保证整个架构的可扩展性,我们也允许设计者建立自定义的CacheDependency类,用以扩展缓存依赖。这就有必要为CacheDependency建立抽象接口,并在web.config文件中进行配置。

在PetShop 4.0的命名空间PetShop.ICacheDependency中,定义了名为IPetShopCacheDependency接口,它仅包含了一个接口方法:
public interface IPetShopCacheDependency
{      
    AggregateCacheDependency GetDependency();
}

AggregateCacheDependency是.Net Framework 2.0新增的一个类,它负责监视依赖项对象的集合。当这个集合中的任意一个依赖项对象发生改变时,该依赖项对象对应的缓存对象都将被自动移除。
AggregateCacheDependency类起到了组合CacheDependency对象的作用,它可以将多个CacheDependency对象甚至于不同类型的CacheDependency对象与缓存项建立关联。由于PetShop需要为Category、Product和Item数据表建立依赖项,因而IPetShopCacheDependency的接口方法GetDependency()其目的就是返回建立了这些依赖项的AggregateCacheDependency对象。

4.3.2  CacheDependency实现

CacheDependency的实现正是为Category、Product和Item数据表建立了对应的SqlCacheDependency类型的依赖项,如代码所示:
public abstract class TableDependency : IPetShopCacheDependency
{
    // This is the separator that's used in web.config
    protected char[] configurationSeparator = new char[] { ',' };

    protected AggregateCacheDependency dependency = new AggregateCacheDependency();
    protected TableDependency(string configKey)
    {
        string dbName = ConfigurationManager.AppSettings["CacheDatabaseName"];
        string tableConfig = ConfigurationManager.AppSettings[configKey];
        string[] tables = tableConfig.Split(configurationSeparator);

        foreach (string tableName in tables)
            dependency.Add(new SqlCacheDependency(dbName, tableName));
    }
    public AggregateCacheDependency GetDependency()
   {
        return dependency;
    }
}

需要建立依赖项的数据库与数据表都配置在web.config文件中,其设置如下:
<add key="CacheDatabaseName" value="MSPetShop4"/>
<add key="CategoryTableDependency" value="Category"/>
<add key="ProductTableDependency" value="Product,Category"/>
<add key="ItemTableDependency" value="Product,Category,Item"/>

根据各个数据表间的依赖关系,因而不同的数据表需要建立的依赖项也是不相同的,从配置文件中的value值可以看出。然而不管建立依赖项的多寡,其创建的行为逻辑都是相似的,因而在设计时,抽象了一个共同的类TableDependency,并通过建立带参数的构造函数,完成对依赖项的建立。由于接口方法GetDependency()的实现中,返回的对象dependency是在受保护的构造函数创建的,因此这里的实现方式也可以看作是Template Method模式的灵活运用。例如TableDependency的子类Product,就是利用父类的构造函数建立了Product、Category数据表的SqlCacheDependency依赖:
public class Product : TableDependency
{
    public Product() : base("ProductTableDependency") { }
}

如果需要自定义CacheDependency,那么创建依赖项的方式又有不同。然而不管是创建SqlCacheDependency对象,还是自定义的CacheDependency对象,都是将这些依赖项添加到AggregateCacheDependency类中,因而我们也可以为自定义CacheDependency建立专门的类,只要实现IPetShopCacheDependency接口即可。

4.3.3  CacheDependency工厂

继承了抽象类TableDependency的Product、Category和Item类均需要在调用时创建各自的对象。由于它们的父类TableDependency实现了接口IPetShopCacheDependency,因而它们也间接实现了IPetShopCacheDependency接口,这为实现工厂模式提供了前提。

在PetShop 4.0中,依然利用了配置文件和反射技术来实现工厂模式。命名空间PetShop.CacheDependencyFactory中,类DependencyAccess即为创建IPetShopCacheDependency对象的工厂类:
public static class DependencyAccess
{       
    public static IPetShopCacheDependency CreateCategoryDependency()
    {
        return LoadInstance("Category");
    }
    public static IPetShopCacheDependency CreateProductDependency()
    {
        return LoadInstance("Product");
    }
    public static IPetShopCacheDependency CreateItemDependency()
    {
        return LoadInstance("Item");
    }
    private static IPetShopCacheDependency LoadInstance(string className)
    {
        string path = ConfigurationManager.AppSettings["CacheDependencyAssembly"];
        string fullyQualifiedClass = path + "." + className;
        return (IPetShopCacheDependency)Assembly.Load(path).CreateInstance(fullyQualifiedClass);
    }
}
整个工厂模式的实现如图4-3所示:

4-3.gif
 图4-3 CacheDependency工厂

虽然DependencyAccess类创建了实现了IPetShopCacheDependency接口的类Category、Product、Item,然而我们之所以引入IPetShopCacheDependency接口,其目的就在于获得创建了依赖项的AggregateCacheDependency类型的对象。我们可以调用对象的接口方法GetDependency(),如下所示:
AggregateCacheDependency dependency = DependencyAccess.CreateCategoryDependency().GetDependency();

为了方便调用者,似乎我们可以对DependencyAccess类进行改进,将原有的CreateCategoryDependency()方法,修改为创建AggregateCacheDependency类型对象的方法。

然而这样的做法扰乱了作为工厂类的DependencyAccess的本身职责,且创建IPetShopCacheDependency接口对象的行为仍然有可能被调用者调用,所以保留原有的DependencyAccess类仍然是有必要的。

在PetShop 4.0的设计中,是通过引入Facade模式以方便调用者更加简单地获得AggregateCacheDependency类型对象。

4.3.4  引入Facade模式

利用Facade模式可以将一些复杂的逻辑进行包装,以方便调用者对这些复杂逻辑的调用。就好像提供一个统一的门面一般,将内部的子系统封装起来,统一为一个高层次的接口。一个典型的Facade模式示意图如下所示:

4-4.gif
图4-4 Facade模式

Facade模式的目的并非要引入一个新的功能,而是在现有功能的基础上提供一个更高层次的抽象,使得调用者可以直接调用,而不用关心内部的实现方式。以CacheDependency工厂为例,我们需要为调用者提供获得AggregateCacheDependency对象的简便方法,因而创建了DependencyFacade类:
public static class DependencyFacade
{
    private static readonly string path = ConfigurationManager.AppSettings["CacheDependencyAssembly"];
    public static AggregateCacheDependency GetCategoryDependency()
    {
        if (!string.IsNullOrEmpty(path))
            return DependencyAccess.CreateCategoryDependency().GetDependency();
        else
            return null;
    }
    public static AggregateCacheDependency GetProductDependency()
    {
        if (!string.IsNullOrEmpty(path))
            return DependencyAccess.CreateProductDependency().GetDependency();
        else
            return null;
        }
    public static AggregateCacheDependency GetItemDependency()
    {
        if (!string.IsNullOrEmpty(path))
            return DependencyAccess.CreateItemDependency().GetDependency();
        else
            return null;
    }
}

DependencyFacade类封装了获取AggregateCacheDependency类型对象的逻辑,如此一来,调用者可以调用相关方法获得创建相关依赖项的AggregateCacheDependency类型对象:
AggregateCacheDependency dependency = DependencyFacade.GetCategoryDependency();

比起直接调用DependencyAccess类的GetDependency()方法而言,除了方法更简单之外,同时它还对CacheDependencyAssembly配置节进行了判断,如果其值为空,则返回null对象。

在PetShop.Web的App_Code文件夹下,静态类WebUtility的GetCategoryName()和GetProductName()方法调用了DependencyFacade类。例如GetCategoryName()方法:
public static string GetCategoryName(string categoryId)
{
     Category category = new Category();
     if (!enableCaching)
            return category.GetCategory(categoryId).Name;

     string cacheKey = string.Format(CATEGORY_NAME_KEY, categoryId);

     // 检查缓存中是否存在该数据项;
     string data = (string)HttpRuntime.Cache[cacheKey];
     if (data == null)
     {
           // 通过web.config的配置获取duration值;
           int cacheDuration = int.Parse(ConfigurationManager.AppSettings["Category

抱歉!评论已关闭.