.NET(C#) Internals: .NET Framework中已使用的设计模式

📅 2026/7/5 19:03:20
.NET(C#) Internals: .NET Framework中已使用的设计模式
观察者模式Observer Pattern观察者模式在此种模式中一个目标物件管理所有相依于它的观察者物件并且在它本身的状态改变时主动发出通知。这通常通过调用各观察者所提供的方法来实现。它的UML表示如下图1、观察者模式的UML表示 来源维基百科好的面向对象设计都强调封装encapsulation和松耦合loose coupling。换句话说类应该保持内部细节私有并且最小化类之间严格的依赖关系。大部分应用程序类并不是独立工作的而是与其他类交互的。类交互的一个通常例子是一个类应该观察者Observer被通知当被观察者Subject的某些东西改变了。例如当单击一个按钮后可能某些Windows Forms的控件需要更新他们的显示。一个简单的解决方案是当状态改变时让被观察者调用观察者特定的方法。但是这回引入一连串的问题。因为被观察者需要知道调用哪个方法这样就与特定观察者产生了紧耦合tight coupling。而且如果当需要添加多个观察者时不得不继续添加每个观察者方法调用的代码。如果观察者的数量动态地改变这将变得更复杂。这将很难维护应用观察者模式能有效地解决这个问题。可以从观察者解耦被观察者因此在设计时和运行时观察者可以容易地添加和移除。被观察者维护者一个对它感兴趣的观察者列表每次被观察者的状态改变时它对每个观察者调用Notify方法。下面这段代码展示了一个实现示例123456789101112131415161718192021222324publicabstractclassCanonicalSubjectBase{privateArrayList _observers newArrayList();publicvoidAdd(ICanonicalObserver o){_observers.Add(o);}publicvoidRemove(ICanonicalObserver o){_observers.Remove(o);}publicvoidNotify(){foreach(ICanonicalObserver oin_observers){o.Notify();}}}publicinterfaceICanonicalObserver{voidNotify();}所有的观察者类实现ICanonicalObserver接口所有的被观察者必须继承自CanonicalSubjectBase。如果一个新的观察者想监视被观察者Add方法可以轻松的处理而不必改变被观察者类的代码。注意每个被观察者仅仅直接依赖于ICanonicalObserver接口而不是特定的观察者。然而使用GOF的观察者模式解决这些问题仍有一些障碍因为被观察者必须继承一个特定的基类且观察者必须实现一个特定接口。考虑回Windows Forms按钮的例子.NET Framework引入了委托和事件来解决这些问题。如果你已经编写过ASP.NET或Windows Forms程序你可能就是有了事件和事件处理器。事件作为被观察者然而委托作为观察者。下面代码展示了使用事件的观察者模式12345678910111213141516171819202122232425262728293031323334353637publicdelegatevoidEvent1Hander();publicdelegatevoidEvent2Handler(inta);publicclassSubject{publicSubject(){}publicEvent1Hander Event1;publicEvent2Handler Event2;publicvoidRaiseEvent1(){Event1Handler ev Event1;if(ev !null) ev();}publicvoidRaiseEvent2(){Event2Handler ev Event2;if(ev !null) ev(6);}}publicclassObserver1{publicObserver1(Subject s){s.Event1 newEvent1Hander(HandleEvent1);s.Event2 newEvent2Handler(HandleEvent2);}publicvoidHandleEvent1(){Console.WriteLine(Observer 1 - Event 1);}publicvoidHandleEvent2(inta){Console.WriteLine(Observer 1 - Event 2);}}Windows Forms Button控件公开一个Click事件当button被点击时产生。任何设计为响应这个事件的类仅需要用这个事件注册一个委托。Button类不依赖与任何潜在的观察者并且每个观察者仅需要知道这个事件的委托的正确类型这里是EventHandler。因为EventHandler是一个委托类型而不是一个接口每个观察者不需要实现一个额外的接口。假定它已经包含一个与签名兼容的方法只需要用被观察者的事件注册方法。通过使用委托和事件观察者模式使被观察者与观察者们之间解耦了。2、迭代器模式Iterator Pattern迭代器模式它可以让使用者通过特定的接口巡访容器中的每一个元素而不用了解底层的实作。它的UML表示如下图2、迭代器模式的UML表示来源TerryLee的.NET 设计模式18迭代器模式Iterator Pattern许多编程任务包括操作对象的集合。不管这些集合是简单的列表还是更复杂的如二叉树经常需要访问集合中的每个对象。事实上根据集合可能有几种不同的访问每个对象的方法诸如从前向后、从后向前、前序或后序。为了保持集合简单遍历代码通常放在自己单独的类中。存储一个对象列表的常用方法之一就是用数组。数组类型在Visual Basic.NET和C#中都是内置类型他们都有一个循环结构用于在数组上迭代foreachC#和For EachVisual Basic.NET。下面是一个在数组上进行迭代的简单例子123456int[] values newint[] {1, 2, 3, 4, 5};foreach(intiinvalues){Console.Write(i.ToString() );}这些语句在后台对数组使用了迭代器。我们需要知道的就是它保证了循环保证了对数组中的每个元素进行一次遍历。为了使这些语句起作用foreach表达式中涉及的对象必须实现了IEnumerable接口。任何实现了IEnumerable接口的对象集合都可以被遍历枚举。这个接口仅有一个方法GetEnumerator()它返回一个实现了IEnumerable的对象。IEnumerator接口包含遍历迭代集合所需要的代码它有一个属性Current标识当前对象、方法MoveNext()移到下一个对象、方法Reset()重新开始。System.Collections命名空间中所有的集合类及数组都实现了IEnumerable接口因此能被迭代。如果你测试了由C#编译器生成foreach的MSIL代码你可以看到大部分情况它仅使用IEnumerator去做迭代特定类型如数组和字符串由编译器特别处理。下面代码展示用IEnumerator方法实现上例功能的代码123456int[] values newint[] {1, 2, 3, 4, 5};IEnumerator e ((IEnumerable)values).GetEnumerator();while(e.MoveNext()){Console.Write(e.Current.ToString() );}.NET Framework使用IEnumerable和IEnumerator接口实现了迭代器模式。迭代器模式使我们能够轻松地遍历一个集合而不用了解集合内部的工作机制。一个迭代器类实现了IEnumerator接口是一个独立与集合的类实现了IEnumerable接口。迭代器类维护遍历的状态包括当前元素是哪个和是否有更多的元素要遍历。这个遍历的算法也包含在迭代器类中。这种方法可以同时有几个迭代器每个以不同的方式遍历同一个集合而不会对集合类增加任何复杂。3、装饰模式Decorator Pattern装饰模式一种动态地往一个类中添加新的行为的设计模式通过使用修饰模式可以在运行时扩充一个类的功能。原理是增加一个修饰类包裹原来的类包裹的方式一般是通过在将原来的对象作为修饰类的构造函数的参数。装饰类实现新的功能但是在不需要用到新功能的地方它可以直接调用原来的类中的方法。修饰类必须和原来的类有相同的接口。UML表示如下图3、装饰模式UML表示来源TerryLee的.NET 设计模式10装饰模式Decorator Pattern任何有用的可执行程序包括读取输入或写输出或者都有。尽管数据源被读或写能够把它们抽象地看成字节序列。.NET使用System.IO.Stream类去表示这个抽象。不管这些数据是包含在文本文件中字符还是TCP/IP网络流的数据或任何其他实体中你将通过一个Stream访问它们。因为用于文件数据的类FileStream和用于网络流的类NetWorkStream都继承自Stream你可以简单地编写独立于数据源的代码处理数据。下面的代码展示从一个Stream中打印字节到控制台12345678publicstaticvoidPrintBytes(Stream s){intb;while((b fs.ReadByte()) 0){Console.Write(b );}}每次读取单个字节通常不是最高效的访问流的方法。例如硬件驱动器有能力且优化了从磁盘的一大块中读取连续的数据块。如果你知道你将读取几个字符最好一次从磁盘中读取一个块然后从内存中逐字节地使用。框架包括BufferedStream类就是做这个的。BufferedStream的构造函数以流的类型为参数设定你想缓存访问的类型。BufferedStream重写了Stream的主要方法诸如Read和Write提供更多的功能。因为它仍然是Stream的子类你可以想其他Stream一样使用它NoteFileStream包括他自己的缓存能力。类似地你可以使用System.Security.Cryptography.CryptoStream加密和解密流Streams除了它是一个流应用程序不需要知道任何其他的东西。下面展示了几种使用不同的Streams调用打印方法12345678MemoryStream ms newMemoryStream(newbyte[] {1, 2, 3, 4, 5, 6, 7, 8});PrintBytes(ms);BufferedStream buff newBufferedStream(ms);PrintBytes(buff);buff.Close();FileStream fs newFileStream(http://www.cnblogs.com/decorator.txt, FileMode.Open);PrintBytes(fs);fs.Close();图4、使用装饰模式