译序: C#进阶文章。译者对Samir提供的C#例子进行了简单整理(作者提供的某些代码在译者的环境中无法通过编译),并编写了对应的C++示例,一并置于译注中,以便读者比对。译文中所有C#、C++程序调试环境均为Microsoft Visual Studio.NET 7.0 Beta2。
概要: 通过提供一个框架,设计模式可以解决应用开发中的许多问题。模式使得设计过程更加清晰高效,它特别适用于C#程序开发,因为C#是面向对象的语言。【译注:因为设计模式的由来和出发点就是描述面向对象的(可复用的)软件设计】现有的设计模式为你自己的类的设计提供了优秀的模板,使用模式可以缩短软件开发周期。本文将描述几个流行的设计模式,包括singleton、strategy、decorator、composite和state,你可以在你自己的应用中使用它们,藉此提高应用的扩展性,并使类更易于重用。
正如任何一个老练的面向对象的软件开发者所了解的那样,缺乏对设计模式最起码的了解而来讨论软件设计架构是不可思议的。如果不是全部那也有大多数的软件应用、工具和系统至少使用了一种甚至更多种设计模式。设计模式是一种对一套相互作用的类的描述,这些类为解决特定上下文环境中的一般性问题提供了框架。换句话说,模式为面向对象软件开发中的特定问题提供了解决方案。此外,模式一般也重视限制其适应解决方案的相关约束和其它因素。类和类之间的连接和通信以及上下文细节共同定义了一个模式,它为任何一个面向对象软件设计中在特性和必要条件方面与之匹配的问题提供了解决方案。
我必须承认我是设计模式的一个热心的支持者。自从我阅读了Gamma、Helm、Johnson和Vlissides合著的那本创造性的著作《设计模式》以来,我就很少不用任何模式而设计软件了。实际上,我在软件设计的早期阶段花了相当可观的时间来定夺可和将来架构自然吻合的模式。毕竟,模式是经过时间和应用领域考验过的对一些问题的解决方案,那些问题已经被经验丰富的设计师、开发者和语言专家所解决。对任何一个正在进行软件设计的人员来说,善用可加以利用的知识和专家经验是明智的。而采用一个已被反复证明是成功的解决方案而不是从头发明一个新的往往是个好主意。
几乎没有开发人员能够享受只写小程序的奢侈了。现代的应用软件和系统是复杂的,它们往往由成千上万行代码组成,并且在这些基础代码之上的代码甚至更为庞大。仅仅对工具和语言的简单掌握是不足以胜任程序设计要求的,公司软件开发一般都要求在设计和架构上具有极大的弹性,以适应在产品开发的不同阶段客户的不断变化的需求,甚至在产品发布后也常常如此。这种动态性要求软件设计必须强健。它应该能够适应变化并且不会带来任何不必要的连锁反应—不应该要求重写潜在的、不相干的(子)系统。向不具备扩展能力的模块添加特性和组件是令人沮丧的和难以达到预期目标的。封闭的、无弹性的设计迟早会被变化的压力所压垮。设计模式有助于弹性架构的基础铺设,而这,是每一个优秀的面向对象设计的共同特点。
设计模式已经被编目归类以用于解决从细小问题乃至大规模架构级问题。本文将介绍几个流行的设计模式,在我自己的项目里,我发现它们很有用。尽管熟悉面向对象设计的概念有助于理解本文,但我并不假定你具备任何设计模式的预备知识。尽管任何适宜于面向对象开发的程序语言都可以用来阐明模式,但我将只用C#来编写例子【译注:但我不是J 我给出了和C#对应的C++的完整示例,以便于熟悉C++的读者进行比对J】,也借此来展示这门语言的威力。我不会讨论任何微软.NET类库细节,相反,我将集中于使用C#语言作为设计面向对象软件的工具。
C#和设计模式
C#是一个现代的程序语言,它通过提供直接映射面向对象设计概念的句法结构和语义支持来促进面向对象软件开发。这和C++大不相同,C++同时支持面向过程、面向对象和泛型编程。虽然如此,如果你是一名C++程序员,跟进C#是非常容易的。对于C++程序员来说,这个学习曲线是相当平坦的。即使你以前从未看过任何C#代码,理解本文示例代码也不应该有任何问题。事实上,如果你发现C#对设计模式的实现更为清晰,我也不会有任何惊讶,特别是如果你以前使用设计模式编写过代码的话。一般讨论设计模式的书籍和文章都会详细地描述模式所要解决的问题和上下文细节,并随后提供一个规范的解决方案的描述。本文不会那么严谨,我只关注模式本质,并辅以适当的C#示例来加以说明。
让我们从最简单的设计模式开始:singleton。
singleton
任何编写过MFC应用的开发人员(不管编写的应用是如何的小)都知道什么是singleton。singleton是类的唯一实例。使用MFC时,从CWinApp派生的应用类的全局实例就是singleton。当然,在MFC应用中,尽管规定不允许创建应用类的第二个实例,但是并没有什么可以阻止你那么做。【译注:实际上,不管是VC6.0还是VC7.0Beta2,它们的编译器都可以一定程度地限制你创建第二个实例。之所以说一定程度上,是因为诸如这种情况编译器并不帮你检查—试图在窗体的某个按钮事件里创建应用类的第二个实例】在这种情况下,当你需要某个特定的类表现出singleton行为时,一个更好的替代方案是让这个类自己负责确保只会被创建一个并且只有一个实例。再回到MFC,我们知道保证应用类实例的唯一性的责任被留给了开发应用的程序员,他(她)们必须小心不要创建应用类的第二个实例。
现在来看看表1所示的类。singleton的访问被局限于必须通过静态方法Instance。多数情况下,singleton应该具有全局可见性,这可通过将其创建方法public来实现。和用全局变量模拟singleton不同,这种模式可以防止创建出多余的实例,同时兼具全局可见性。注意,该类的构造器被置为private,这就意味着没有任何办法可以绕过静态方法Instance来直接创建类的实例。
表1
class Singleton { private static Singleton singleton = null; public static Singleton Instance() { if (null == singleton) singleton = new Singleton(); return singleton; } private Singleton() { } }
|
singleton模式的作用还不止于此,尤其是可以将其扩展,以创建类的可变数量的实例。假定有一个应用,当需要执行特定任务时就需要调度一个工作者线程。考虑到节约系统资源,我们使用singleton来实现这个线程类。不久,需要singleton线程处理的任务变得密集起来,如果我们决定扩展这个应用,我们可以很方便地增加工作者线程的数量,因为线程的创建和对它们的访问授权的所有逻辑都被定义在一个类中。
singleton模式的另外一个优点是singleton的创建可以被延迟到真正需要的时候,正如表1所示。不管是否需要,全局变量一开始就被创建,但这个全局对象并不一定是一直都需要的。C#不支持全局变量,但还是有可能在某个方法的一开始就在堆上创建了一个对象并直到很久以后才使用它。果真如此的话,singleton模式为这种案例提供了一个优雅的解决方案。
另外,作为一个工具,在singleton模式的实现上,C#优于C++,尽管这个优点很微妙,但绝对重要。基于C++的实现需考虑singleton带来的一些和生命期管理有关的棘手问题,而在C#中则由运行时自动处理。这个优点是有意义的,在singleton模式的C#实现版本中,你只需保证在需要singleton的时候,你拥有一个活的引用即可。
译注:以下是singleton模式完整示例
C#示例:
using System; class Singleton { private static Singleton singleton = null; public static Singleton Instance() { if (null == singleton) singleton = new Singleton(); return singleton; } private Singleton() { } } class Application { public static void Main() { Singleton s1 = Singleton.Instance(); //Singleton s2 = new Singleton(); //错误:构造器不可访问 Singleton s2 = Singleton.Instance(); if (s1.Equals(s2)) // 引用相等 Console.WriteLine("Instances are identical"); } }
/*以下是程序输出结果: Instances are identical */
|
C++示例:【译注:译者在自己的程序实践中,几乎从未将类的声明和实现搅和在一起,此处示例代码之所以是如此写法,只是为了便于大家阅读和比对而已】
#include "stdafx.h"; #include class Singleton { public: static Singleton* Instance() { if (NULL == singleton) singleton = new Singleton(); return singleton; }; private: Singleton() { }; private: static Singleton* singleton; }; Singleton* Singleton::singleton = NULL; int _tmain(int argc, _TCHAR* argv[]) { Singleton* sgt1 = Singleton::Instance(); Singleton* sgt2 = Singleton::Instance(); if(sgt1 == sgt2) cout<<"Instances are identical\n"; delete sgt1;//【译注:这个简单的例子里,是不存在内存泄漏或棘手的生命期管理问题的J】 return 0; } /*以下是程序输出结果: Instances are identical */ 】
|
strategy
应用常因用户输入、运行平台和部署环境等的不同执行的任务亦不相同。以磁盘文件异步I/O举例,Windows NT/2000下的Win32 API对异步I/O提供了原生支持,但Windows 95/98并非如此。因此,依赖于异步文件I/O的应用就必须实现两套不同的算法,根据部署环境不同,一套可使用Win32 API,另一套可能要采用多线程技术从头写起。而对于客户来说一般不会意识到执行了不同算法,他(她)们所关心的只是得到同样的结果,他(她)们也只关心这个。
另一个例子是从互联网上的远程服务器下载文件。提供文件下载服务的应用接受一个URL和一个协议(如FTP或HTTP),然后创建一个使用该协议和远程服务器通讯的对象。根据用户不同输入,使用不同的算法(协议)。但结果是一样的:下载了一个文件。
让我们看一个更具体的例子:素数测试。表2所示的代码声明了一个接口(C#中的一个概念),它只有一个方法:IsPrime。
表2
interface Strategy { bool IsPrime(int n); }
|
接口就象一个合约,它是所有派生类必须遵从的规范。更为特别的是,它定义方法的签名但并不实现它们,实现接口的具体类必须提供这些方法的实现。在这一点上,C#明显优于C++,因为C++缺乏对接口在语言上的原生的支持。C++程序员一般是通过定义只包含纯虚成员函数的的抽象类来创建接口。在C#中,所有接口成员都是public的,所有实现接口的类都必须实现接口中所有方法。
现在假定我们有三个不同的素数测试算法,每一种都有自己的性能和精度指标。其中之一属于计算密集型,对因数进行彻底测试,另外一种算法速度较快但对大数的测试结果未必准确。我的应用就是要询问用户期望何种执行性能,然后根据其选择调用相应的算法。我把算法封装到实现Strategy接口的若干个类中。参见表3示例代码。
表3
class Fermat : Strategy { public bool IsPrime(int n) { bool result = false; //使用Fermat法测试n是否为素数,果真,则更新result的值 Console.WriteLine("Using Fermat's test"); return result; } }
|