——适合有一定设计模式基础和.NET基础的人阅读。

写在前面

“设计模式”我一向是敬而远之的态度,不会去写这方面的文章,原因有二:第一,要想写好设计模式的文章太难,需要笔者丰富的经验;第二,没有深厚的功底写出的设计模式文章容易误导他人。自认没有深厚的功底,但我不会为了设计模式而设计模式。我想大部分人对设计模式的理解是不够深刻的,不然应用自如,特别是初学者!所有研究高质量的源码或框架是我们学习实践设计模式的好途径之一。

而我之所以写这篇文章,主要是因为它从.NET Framework入手介绍已经使用的设计模式,作为一个.NET开发人员应该再熟悉不过了,能够有比较深刻的认识和理解。本文从.NET Framework中入手,发掘在.NET Framework中如何使用设计模式的。从中我们知道我们平时使用.NET时,我们使用了那些模式及学习使用设计模式。本文意译自Discover the Design Patterns You're Already Using in the .NET Framework及加入了相关设计模式的UML表示和主要介绍。

主要内容如下:

  1. .NET Framework中使用的观察者模式(Observer Pattern)
  2. .NET Framework中使用的迭代器模式(Iterator Pattern)
  3. .NET Framework中使用的装饰模式(Decorator Pattern)
  4. .NET Framework中使用的适配器模式(Adapter Pattern)
  5. .NET Framework中使用的工厂模式(Factory Pattern)
  6. .NET Framework中使用的策略模式(Strategy Pattern)
  7. ASP.NET中的组合模式(Composite Pattern)
  8. .NET Framework中使用的模板方法模式(Template Method Pattern)
  9. ASP.NET管道中的模式(Patterns in the ASP.NET Pipeline)
    1. 截取过滤器模式(Intercepting Filter Pattern)
    2. 页面控制器模式(Page Controller Pattern)
    3. ASP.NET中的其它web表示模式(Other Web Presentation Patterns in ASP.NET)
  10. 总结

1、观察者模式(Observer Pattern)

观察者模式:在此种模式中,一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常通过调用各观察者所提供的方法来实现。它的UML表示如下:

.NET Framework中已使用的设计模式

图1、观察者模式的UML表示 (来源:维基百科

好的面向对象设计都强调封装(encapsulation)和松耦合(loose coupling)。换句话说,类应该保持内部细节私有并且最小化类之间严格的依赖关系。大部分应用程序,类并不是独立工作的,而是与其他类交互的。类交互的一个通常例子是:一个类应该(观察者,Observer)被通知,当被观察者(Subject)的某些东西改变了。例如,当单击一个按钮后可能某些Windows Forms的控件需要更新他们的显示。一个简单的解决方案是,当状态改变时让被观察者调用观察者特定的方法。但是,这回引入一连串的问题。因为被观察者需要知道调用哪个方法,这样就与特定观察者产生了紧耦合(tight coupling)。而且,如果当需要添加多个观察者时,不得不继续添加每个观察者方法调用的代码。如果观察者的数量动态地改变,这将变得更复杂。这将很难维护!

应用观察者模式能有效地解决这个问题。可以从观察者解耦被观察者,因此在设计时和运行时观察者可以容易地添加和移除。被观察者维护者一个对它感兴趣的观察者列表,每次被观察者的状态改变时,它对每个观察者调用Notify方法。下面这段代码展示了一个实现示例:

  1. public abstract class CanonicalSubjectBase   
  2. {  
  3.     private ArrayList _observers = new ArrayList();   
  4.     public void Add(ICanonicalObserver o)   
  5.     {  
  6.         _observers.Add(o);   
  7.     }   
  8.     public void Remove(ICanonicalObserver o)   
  9.     {   
  10.         _observers.Remove(o);  
  11.     }   
  12.     public void Notify()   
  13.     {   
  14.         foreach(ICanonicalObserver o in _observers)  
  15.         {   
  16.             o.Notify();   
  17.         }   
  18.     }   
  19. }   
  20.  
  21. public interface ICanonicalObserver  
  22. {  
  23.     void Notify();  

所有的观察者类实现ICanonicalObserver接口,所有的被观察者必须继承自CanonicalSubjectBase。如果一个新的观察者想监视被观察者,Add方法可以轻松的处理而不必改变被观察者类的代码。注意:每个被观察者仅仅直接依赖于ICanonicalObserver接口,而不是特定的观察者。

然而使用GOF的观察者模式解决这些问题仍有一些障碍,因为被观察者必须继承一个特定的基类且观察者必须实现一个特定接口。考虑回Windows Forms按钮的例子,.NET Framework引入了委托事件来解决这些问题。如果你已经编写过ASP.NET或Windows Forms程序,你可能就是有了事件和事件处理器。事件作为被观察者,然而委托作为观察者。下面代码展示了使用事件的观察者模式:

  1. public delegate void Event1Hander();   
  2. public delegate void Event2Handler(int a);  
  3.  
  4. public class Subject   
  5. {  
  6.     public Subject(){}   
  7.     public Event1Hander Event1;  
  8.     public Event2Handler Event2;  
  9.     public void RaiseEvent1()   
  10.     {   
  11.         Event1Handler ev = Event1;  
  12.         if (ev != null) ev();  
  13.      }   
  14.  
  15.     public void RaiseEvent2()   
  16.     {   
  17.         Event2Handler ev = Event2;   
  18.         if (ev != null) ev(6);   
  19.     }   
  20. }   
  21.  
  22. public class Observer1  
  23. {   
  24.     public Observer1(Subject s)  
  25.     {  
  26.         s.Event1 += new Event1Hander(HandleEvent1);  
  27.         s.Event2 += new Event2Handler(HandleEvent2);  
  28.     }   
  29.     public void HandleEvent1()  
  30.     {   
  31.         Console.WriteLine("Observer 1 - Event 1");  
  32.     }  
  33.     public void HandleEvent2(int a)   
  34.     {   
  35.         Console.WriteLine("Observer 1 - Event 2");   
  36.     }   

Windows Forms Button控件公开一个Click事件,当button被点击时产生。任何设计为响应这个事件的类仅需要用这个事件注册一个委托。Button类不依赖与任何潜在的观察者,并且每个观察者仅需要知道这个事件的委托的正确类型(这里是EventHandler)。因为EventHandler是一个委托类型而不是一个接口,每个观察者不需要实现一个额外的接口。假定它已经包含一个与签名兼容的方法,只需要用被观察者的事件注册方法。通过使用委托和事件,观察者模式使被观察者与观察者们之间解耦了。

2、迭代器模式(Iterator Pattern)

迭代器模式:它可以让使用者通过特定的接口巡访容器中的每一个元素而不用了解底层的实作。它的UML表示如下:

.NET Framework中已使用的设计模式

图2、迭代器模式的UML表示(来源:TerryLee的.NET 设计模式(18):迭代器模式(Iterator Pattern)

许多编程任务包括操作对象的集合。不管这些集合是简单的列表还是更复杂的,如二叉树,经常需要访问集合中的每个对象。事实上,根据集合可能有几种不同的访问每个对象的方法,诸如从前向后、从后向前、前序或后序。为了保持集合简单,遍历代码通常放在自己单独的类中。

存储一个对象列表的常用方法之一就是用数组。数组类型在Visual Basic.NET和C#中都是内置类型,他们都有一个循环结构用于在数组上迭代:foreach(C#)和For Each(Visual Basic.NET)。下面是一个在数组上进行迭代的简单例子:

  1. int[] values = new int[] {1, 2, 3, 4, 5};  
  2.  
  3. foreach(int i in values)  
  4. {  
  5.     Console.Write(i.ToString() + " ");  

这些语句在后台对数组使用了迭代器。我们需要知道的就是它保证了循环保证了对数组中的每个元素进行一次遍历。

为了使这些语句起作用,foreach表达式中涉及的对象必须实现了IEnumerable接口。任何实现了IEnumerable接口的对象集合都可以被遍历(枚举)。这个接口仅有一个方法GetEnumerator(),它返回一个实现了IEnumerable的对象。IEnumerator接口包含遍历迭代集合所需要的代码,它有一个属性Current标识当前对象、方法MoveNext()移到下一个对象、方法Reset()重新开始。System.Collections命名空间中所有的集合类,及数组,都实现了IEnumerable接口,因此能被迭代。

如果你测试了由C#编译器生成foreach的MSIL代码,你可以看到大部分情况它仅使用IEnumerator去做迭代(特定类型,如数组和字符串,由编译器特别处理)。下面代码展示用IEnumerator方法实现上例功能的代码:

  1. int[] values = new int[] {1, 2, 3, 4, 5};  
  2. IEnumerator e = ((IEnumerable)values).GetEnumerator();  
  3. while(e.MoveNext())  
  4. {  
  5.     Console.Write(e.Current.ToString() + " ");  

.NET Framework使用IEnumerableIEnumerator接口实现了迭代器模式。迭代器模式使我们能够轻松地遍历一个集合而不用了解集合内部的工作机制。一个迭代器类,实现了IEnumerator接口,是一个独立与集合的类,实现了IEnumerable接口。迭代器类维护遍历的状态(包括当前元素是哪个和是否有更多的元素要遍历)。这个遍历的算法也包含在迭代器类中。这种方法可以同时有几个迭代器,每个以不同的方式遍历同一个集合,而不会对集合类增加任何复杂。

3、装饰模式(Decorator Pattern)

装饰模式:一种动态地往一个类中添加新的行为的设计模式,通过使用修饰模式,可以在运行时扩充一个类的功能。原理是:增加一个修饰类包裹原来的类,包裹的方式一般是通过在将原来的对象作为修饰类的构造函数的参数。装饰类实现新的功能,但是,在不需要用到新功能的地方,它可以直接调用原来的类中的方法。修饰类必须和原来的类有相同的接口。UML表示如下:

.NET Framework中已使用的设计模式

图3、装饰模式UML表示(来源:TerryLee的.NET 设计模式(10):装饰模式(Decorator Pattern)

任何有用的可执行程序包括读取输入或写输出,或者都有。尽管数据源被读或写,能够把它们抽象地看成字节序列。.NET使用System.IO.Stream类去表示这个抽象。不管这些数据是包含在文本文件中字符,还是TCP/IP网络流的数据,或任何其他实体中,你将通过一个Stream访问它们。因为用于文件数据的类(FileStream)和用于网络流的类(NetWorkStream)都继承自Stream,你可以简单地编写独立于数据源的代码处理数据。下面的代码展示从一个Stream中打印字节到控制台:

  1. public static void PrintBytes(Stream s)  
  2. {  
  3.     int b;  
  4.     while((b = fs.ReadByte()) >= 0)  
  5.     {  
  6.         Console.Write(b + " ");  
  7.     }  

每次读取单个字节通常不是最高效的访问流的方法。例如,硬件驱动器有能力(且优化了)从磁盘的一大块中读取连续的数据块。如果你知道你将读取几个字符,最好一次从磁盘中读取一个块然后从内存中逐字节地使用。框架包括BufferedStream类就是做这个的。BufferedStream的构造函数以流的类型为参数,设定你想缓存访问的类型。BufferedStream重写了Stream的主要方法,诸如ReadWrite,提供更多的功能。因为它仍然是Stream的子类,你可以想其他Stream一样使用它(Note:FileStream包括他自己的缓存能力)。类似地,你可以使用System.Security.Cryptography.CryptoStream加密和解密流(Streams),除了它是一个流应用程序不需要知道任何其他的东西。下面展示了几种使用不同的Streams调用打印方法:

  1. MemoryStream ms = new MemoryStream(new byte[] {1, 2, 3, 4, 5, 6, 7, 8});   
  2. PrintBytes(ms);  
  3. BufferedStream buff = new BufferedStream(ms);  
  4. PrintBytes(buff);  
  5. buff.Close();  
  6. FileStream fs = new FileStream("http://www.cnblogs.com/decorator.txt", FileMode.Open);  
  7. PrintBytes(fs);  
  8. fs.Close(); 

.NET Framework中已使用的设计模式

图4、使用装饰模式

使用组合动态透明地附加新功能到对象的能力是装饰模式的例子,如上图所示。给定任何Stream的的实例,你可以通过包装在一个BufferedStream中添加缓冲访问的能力,而不用改变接口数据。因为你仅仅是组合了对象,这可以在运行时做,而不像继承是编译时决定的。核心功能通过一个接口或者抽象类(如Stream)定义,所有装饰都派生自它。这些装饰自己实现(或重写)接口(或抽象类)中的方法以提供额外的功能。例如BufferedStream重写Read方法从缓冲中读取流,而不是直接从流中读取。如上面的代码所示,任何装饰的组合,不管多复杂,可以像只有基类一样使用。

4、适配器模式(Adapter Pattern)

适配器模式:将一个类别的接口转接成用户所期待的。一个适配使得因接口不兼容而不能在一起工作的类工作在一起,做法是将类别自己的接口包裹在一个已存在的类中。有两类适配器模式:

  1. 对象适配器模式——在这种适配器模式中,适配器容纳一个它我包裹的类的实例。在这种情况下,适配器调用被包裹对象的物理实体。
  2. 类适配器模式——这种适配器模式下,适配器继承自已实现的类(一般多重继承)。

他们的UML表示分别如下:

.NET Framework中已使用的设计模式

图5、对象适配器模式

.NET Framework中已使用的设计模式

图6、类适配器模式(来源:TerryLee的.NET 设计模式(8):适配器模式(Adapter Pattern)

.NET Framework的优势之一就是向后兼容性。从基于.NET的代码可以轻松的访问旧的COM对象,反之亦然。为了在项目中使用COM组件,必须在Visual Studio中通过添加引用对话框添加引用。在后台Visual Studio .NET调用tlbimp.exe工具创建一个包含在interop程序集中的运行时可调用包装(Runtime Callable Wrapper,RCW)类。一旦添加了引用(且interop程序集已经生产了),COM组件就可以像其它托管代码类一样使用。如果你在看别人写的代码没有看到引用列表(且没有坚持类关联的元数据或他们的实现),你将不知道哪些类是用.NET的语言写的,哪些是COM组件。

使这种神奇发生的是RCM。COM组件有不同的错误处理机制及使用不同的数据类型。例如.NET Framework中的字符串使用System.String类,而COM可能使用BSTR。当在.NET代码中用一个字符串调用COM组件时,你可以像其它托管代码方法一样传递一个System.String。在RCW内部,在COM调用之前,这个System.String转换成COM组件期望的格式,就像BSTR。类似地,COM组件的一个方法调用典型地返回HRESULT表示成功或失败。当一个COM方法调用返回表示失败的HRESULT时,RCW转化成一个异常(默认情况下),因此它能像其他托管代码错误一样处理。

尽管它们的接口不一样,但还是允许托管类和COM组件交互,RCWs是适配器模式的例子。适配器模式使一个接口适合另一个,COM并不理解System.String类,因此RCW适配器使其适应为可以理解的。即使你不能改变旧的组件工作机制,你仍可以与它交互。这种情况经常使用适配器。

适配器类本身包含了一个适配(Adaptee),将来自客户端的调用转化为合适的和顺序的调用。虽然听起来像装饰模式,它们之间有几个关键的不同。

  1. 装饰模式中,组合的对象接口是相同的;适配器模式中,你可以改变整个接口。
  2. 适配器模式中,给他们定义了一个明确的序列,适配必须包含在适配器中;装饰模式中,装饰类不必知道它包装了1个或500个其它类,因为接口是相同的。

因此使用装饰模式对应用程序时透明的,然而使用适配器模式不是。

5、工厂模式(Factory Pattern)

工厂模式:通过调用不同的方法返回需要的类,而不是去实例化具体的类。 对实例创建进行了包装。 工厂方法是一组方法, 他们针对不同条件返回不同的类实例,这些类一般有共同的父类。它的UML表示如下:

.NET Framework中已使用的设计模式

图7、工厂模式的UML表示(来源:TerryLee的.NET 设计模式(5):工厂方法模式(Factory Method)

许多情况下,在框架(Framework)中你可以自己不调用一个struct或class的构造器而获取一个新的实例。System.Convert类包含了一系列的静态方法向这样工作。例如,将一个整数转换为布尔类型,你可以调用Convert.ToBollean并传递一个整数。如果整数是一个非零值则这个方法的返回值是一个新的Boolean并设为“true”,否则是“false”。Convert类为我们创建Boolean实例,并设为正确的值,其它的类型转换也是类似。Int32和Double上的Parse方法返回这些对象的新实例并根据给定的字符串设置合适的值。

这种创建新对象实例的策略称为工厂模式。不用调用对象的构造器,你可以要求对象工厂为你创建一个实例。如此一来可以隐藏创建对象的复杂性(就像如何从一个字符串解析为一个Double值,Double.Parse)。如果你想改变创建对象的细节,你仅需要改变工厂它本身,你不必修改每个调用构造器的地方。

这些类型转换方法是工厂模式的变种,因为在问题中你不必使用这个工厂创建对象。一个更纯的工厂模式的例子是System.Net.WebRequest类,用来发出请求(request)和从Internet上的资源接受响应(response)。FTP、HTTP和文件系统请求默认页支持。为了创建一个请求,调用Create方法和传递一个URI。Create方法决定合适的请求协议,且返回合适的WebRequest的子类:HttpWebRequestFtpWebRequest(.NET Framework 2.0新增的)、FileWebRequest。调用者不需要知道每个特定的协议,仅需知道如何调用工厂和使用WebRequest获取返回值。如果URI从一个HTTP地址改变为FTP地址,代码根本不用改变。这是工厂模式的另一种常用用法。父类作为一个工厂,且根据客户端传递的参数返回特定派生类。就像WebRequest例子,它隐藏了选择合适派生类的复杂性。

6、策略模式(Strategy Pattern)

策略模式:指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中国交个人所得税”就有不同的算税方法。它的UML表示如下:

.NET Framework中已使用的设计模式

 图8、策略模式的UML表示(来源:维基百科)

数组(Array)和数组列表(ArrayList)都提供通过Sort方法对集合中的对象排序的功能。实际上,ArrayList.Sort仅对隐含的数组调用Sort。这些方法使用的是快速排序(QuickSor)算法。默认,Sort方法使用IComparable实现每个元素之间的比较。有时,使用不同的方法对同一列表排序非常有用。例如,字符串数组的排序可能是对大小写敏感的或不区分大小写的。要做到这点,Sort函数存在重载并以一个IComparer作为参数;然后IComparer.Compare用于进行比较。重载允许类的用户任何内置的ICompares或自定义的,而不用做修改甚至不用知道Array、ArrayList、或QuickSort算法的实现细节。

将比较算法的选择留给类的用户,像这种情况就是策略模式。使用策略模式允许有许多不同的可替换的算法。QuickSort本身只需要一个方法来相互比较对象。通过提供一个接口调用比较,由调用方自由选择可替换的适合特定需要的比较算法。QuickSort的代码可以保持不改变。

.NET Framework中已使用的设计模式

 图9、策略行为

.NET Framework 2.0的一个新的泛型集合类型——List<T>,还利用大量使用的策略模式,如上图所示。除了更新了排序方法,find-related方法,BinarySearch,其它以根据调用者需求改变算法的参数。在FindAll<T>方法中使用一个Predicate<T>委托使调用者使用任何方法作为List<T>的过滤器,只要它接受的对象类型并返回一个布尔值。组合匿名方法,客户可以容易地基于属性和列表中对象的方法过滤列表,而不用引入依赖于List<T>类本身。使用策略模式使复杂的处理过程,诸如排序,容易地修改以适合特定的目的,这意味着你可以编写和维护较少的代码。

7、ASP.NET中的组合模式(Composite Pattern)

组合模式:将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和使用具有一致性。它的UML表示为:

.NET Framework中已使用的设计模式

图10、组合模式的UML表示(来源:TerryLee的.NET 设计模式(11):组合模式(Composite Pattern)

ASP.NET的request/response管道(pipeline)是一个复杂的系统。模式用于设计管道本身和控件体系结构有效地平衡它的可扩展性能和简化编程。在深入挖掘管道之前,我们检查编程模型本身使用的模式。

当处理对象集合时,通常适合单个对象和整个集合。考虑一个ASP.NET控件,一个控件可能是一个简单的单个元素如Literal,或可能是一个有子控件的复杂的集合如DataGrid。不管怎样,任一这些控件上调用Render方法都将执行相同的显示功能。

当集合的每个元素可能自己包含其它对象的集合时,使用组合模式是合适的。组合模式是一个简单的方法来表示树形集合,而不用对父节点和叶子节点区别对象。

组合模式的权威例子是依赖于一个抽象基类,Component,包含添加和删除孩子、孩子和父亲之间的通用操作方法。ASP.NET正是对System.Web.UI.Control使用这种模式。Control表示Component基类,它有一些操作,诸如处理子控件(如子空间属性)、标准操作、属性如Render和Visible(译注:定义由所有 ASP.NET 服务器控件共享的属性、方法和事件)。每个对象,不管是原始的对象(如Literal)或组合对象(如DataGrid),读继承自这个基类。

因为控件是多样的,有一些中间继承类如WebControlBaseDataList,是其它控件的基类。虽然这些类公开一些额外的属性和方法,但他们仍然保留孩子的管理功能和从Control继承的核心操作。事实上,使用组合模式有助于隐藏他们的复杂性,如果需要的话。不管是一个Literal控件还是一个DataGrid控件,使用组合模式意味着你仅需要调用Render,事情会自行解决。

8、模板方法模式(Template Method Pattern)

模板方法模式:定义了一个算法的步骤,并允许次类别为一个或多个步骤提供其实践方式。让次类别在不改变算法架构的情况下,重新定义算法中的某些步骤。模板方法多用在:

  1. 某些类别的算法中,实做了相同的方法,造成程式码的重复。
  2. 控制次类别必须遵守的一些事项。

它的UML表示如下:

.NET Framework中已使用的设计模式

图11、模板方法的UML表示(来源:维基百科

当ASP.NET控件的标准库满足不了你的需求,你有几种选择如何创建自己的。对简单的控件仅用在一个项目中,用户控件是最好的选择。当控件用在集合Web应用程序中或要求更多的功能,一个自定义服务器控件也许是最好的选择。

当处理自定义控件,有两个一般的类型:控件组合已存在的控件的功能(称为组合模式)、控件有一个唯一的视觉表示。处理创建这些类型的控件的过程类似。对应组合控件,创建一个新的类继承自一个控件基类(像Control或WebControl),然后重写CreateChildControls方法(译注:该方法由 ASP.NET 页面框架调用,以通知使用基于合成的实现的服务器控件创建它们包含的任何子控件,以便为回发或呈现做准备)。对于其它自定义控件,你需要重写的Render且使用HtmlTextWriter参数用于为你的控件直接输出HTML。

不管你选择的自定义控件的样式,你不必写任何处理所有控件通用的功能,如在适当的时间加载和保存ViewState,允许PostBack事件去处理,且确保控件的生命周期事件以正确的顺序发生。控件怎么样加载、呈现、卸载的主要算法包含在控件基类中

你的控件的特定细节在控件算法的特定地方实现(CreateChildControlsRender方法)。这是模板方法模式的一个例子。主要算法框架定义在一个基类中,且子类可以插入他们自己的细节而不影响算法本身,如图12所示。一个组合控件和一个自定义控件都共享同样的一般生命周期,但是他们以完全不同的视觉表示。

.NET Framework中已使用的设计模式

图12、模板方法模式

这个模式类似于策略模式,他们在范围和方法上不同。

  1. 策略模式用于允许调用者改变整个算法,例如如何比较两个对象,然而模板方法模式用于改变算法的步骤。正是因为这点,策略模式是更粗粒度的,他们的不同客户端实现可以有巨大的不同,然而模板方法模式保持框架相同。
  2. 另一个主要不同点是,策略模式使用委托,然而模板方法模式使用继承。上面排序例子的策略模式,比较算法委托于IComparer参数;但是自定义控件中,你子类的基类和重写方法去做改变。然而,他们都使你容易地修改处理过程以适合你的特定需求。

9、ASP.NET管道中的模式(Patterns in the ASP.NET Pipeline)

当客户端请求一个ASPX网页,在最终以HTML显示到客户端的浏览器之前,请求穿过许多步骤。首先,请求被IIS处理和路由到合适的ISAPI扩展。ASP.NET的ISAPI扩展(aspnet_isapi.dll)路由请求到ASP.NET工作进程。

.NET Framework中已使用的设计模式

 图13、ASP.NET请求管道

在这一点,请求开始与处理请求的类进行交互。请求被传递到一个HttpApplication。一般地,这个类在Global.asax的后置代码文件中创建。HttpApplication然后传递请求给一些HTTP模块。这些类实现了IHttpModule接口且在传递给下一模块之前可以修改请求(甚至可以停止请求)。ASP.NET提供了一些标准的模块提供常用的功能,包括FormsAuthenticationModule、PassportAuthenticationModule、WindowsAuthentication、SessionStateModule等等。

最终,请求结束在一个IHttpHandler,最常用的是System.Web.UI.Page的IHttpHandler.Proce***equest方法。Page产生合适的事件(例如Init、Load、Render),处理ViewState,提供ASP.NET的编程模型。图13展示了一个整理轮廓。

在这个处理过程中采用了几个模式,更深入的可以参考Martin Fowler's Patterns of Enterprise Application Architecture(Addison-Wesley, 2002)。下面介绍其中用到的几个模式。

9.1、截取过滤器模式(Intercepting Filter Pattern)

一旦一个请求到达HttpApplication,它将传递到一些IHttpModules。每个模块是独立的且仅有数量有限的控制加在调用顺序上。HttpApplication类公开一系列的事件,在请求的处理过程中产生这些事件包括BeginRequest、AuthenticateRequest、AuthorizeRequest、EndRequest。当HttpApplication加载一个模块,它调用IHttpModule接口的Init方法,允许模块注册关心它的一些事件。作为一个给定的请求被处理,事件以合适的顺序产生且所有注册的模块可以与请求交互。因此,模块可以控制在它被调用的阶段,但不是这一阶段的确切顺序。

这些模块是截取过滤模式的例子,这个模式表示一个过滤器链,每个过滤器依次有机会修改请求(或消息)传递他们。图14展示了这个过程的一个简单的流程图。这个模式的关键思想是过滤器是独立的,过滤器可以修改传递到它的请求。

.NET Framework中已使用的设计模式 

图14、请求流程

有几种不同的截取过滤器模式变种的实现,一种是ASP.NET的基于事件的模型。一个简单的变种包括维护一个过滤器列表并对它迭代,依次对它们每个调用一个方法。这是Web Service Enhancements(WSE)为ASP.ENT Service如何使用这个模式。每个过滤器扩展自SoapInputFilter(为请求消息)或SoapOutputFilter(为响应),重写ProcessMessage方法执行过滤器的工作。

另外一种选择是通过装饰模式实现截取过滤器。每个过滤器将包装它的后继,执行预处理,调用它的后继,然后执行后处理。通过递归组合构建链,从后往前。这个模式用于实现.NET Remoting管道接收器。

无论其实现,结果是独立过滤器的动态可配置链。因为他们是独立的,这些过滤器可以容易地在其他应用程序中重排序和重用。通用任务如身份鉴定或日志可以封装在一个过滤器中且反复使用。这些任务可以在请求到达HttpHandler之前被过滤器链处理,保持处理代码整洁。

9.2、页面控制器模式(Page Controller Pattern)

System.Web.UI.Page实现ASP.NET编程模型的核心部分。每当你要添加一个逻辑页面到一个Web应用程序,你可以创建一个Web Form(通过一个ASPX文件和它的后置代码文件表示)。然后你可以编写代码处理新页面的具体需求,无论是通过处理页面级事件,显示一个组控件,或加载和操作数据。应用程序中的每个逻辑页面有一个相应的Web Form控制它的行为和调整它的显示。

每个逻辑页有一个控制器是页面控制器模式的一个例子,这种想法是ASP.NET的基础。当一个逻辑页面通过一个URI被请求时,ASP.NET运行时解析地址到相应的页面子类且使用该类去处理请求。所有的关于页面是什么样子的细节、哪些用户输入可以处理和如何响应输入都包含在一个地方。当应用程序中的逻辑页要求改变时,其它页面不受影响。这是一种非常普遍的抽象,以致我们甚至不考虑它。

这样的缺点之一是常与纯粹的页面控制器实现关联,共同的代码必须在每个页面重复。ASP.NET通过包括管道实现的其它模式来避免这个缺点,并提供System.Web.UI.Page作为所有页面控制器的共同基类。交叉剪切(Cross-cutting)关心诸如身份认证和会话状态由HttpModule截取过滤器处理,且产生页面生命周期事件,及其他由基类处理的活动。

9.3、ASP.NET中的其它web显示模式(Other Web Presentation Patterns in ASP.NET)

除了截取过滤器和页面控制器,ASP.NET使用了其它几种Web显示模式的变种。当ASP.NET决定哪个HttpHandler去传递请求,它使用类似于前端控制器(Front Controller)的模式。前端控制器的特点是有一个处理器处理所有的请求(如System.Web.UI.Page)。然而一旦请求达到页面类,页面控制器模式结束。

在页面控制器的ASP.NET实现中,有一些Model View Controller模式的元素。Model View Controller从view(显示信息)分离了model(业务对象,数据和流程)。控制器响应用户输入和更新model和view。大约来说,一个ASPX页面便是View,然而它的后置代码文件表示Model-Controller的混合。如果你从后置代码文件拉取出所有的业务和数据相关且使它仅仅是事件处理代码,这将后置代码变成一个纯粹的控制器,而其它的包含业务逻辑的类将是Model。

因此那些从经典模式中分离的,不在这里讨论了,Martin Fowler的书和Microsoft Patterns网站中查看更多的讨论。

10、总结

现在我们已经研究了在.NET Framework和BCL中使用的通常模式,可以容易地识别每天在编写代码时使用了相同的模式。希望通过突出显示通用类和功能隐含使用的设计模式,使我们更好地理解设计模式和它们提供的好处。试想如果UI编程而不用观察者模式或集合不使用迭代器模式,表明他们是framework不可或缺的。"Once you understand what each does, it becomes another valuable tool to add to your toolbox."

译者推荐

由于篇幅有限文中有许多知识点没有深入介绍,感兴趣可以深入阅读下列文章:

  1. 在ASP.NET 中使用HTTP 模块实现Intercepting Filter
  2. 在ASP.NET 中使用 HTTPHandler 实现 Front Controller
  3. .NET 设计模式系列文章
  4. .NET (C#) Internals: ASP.NET 应用程序与页面生命周期(意译)
  5. DotText 源码学习——ASP.NET的工作机制
  6. 领悟Web设计模式
  7. 表示层设计模式:Front Controller(前端控制器)
  8. ……

最后感谢TerryLee,写此文时为了节省时间,我从他的文章中借了各种设计模式的UML表示。