• 【转载】 笔记设计模式精解c++-GoF 23 种设计模式解析


    0  引言
    0.1 设计模式解析(总序)
    0.2 设计模式解析后记
    0.3  与作者联系
    1  创建型模式
    1.1 Factory模式
     1)为了提高内聚(Cohesion)和松耦合(Coupling) ,我们经常会抽象出一些类的公共接口以形成抽象基类或者接口。 这样我们可以通过声明一个指向基类的指针来指向实际的子类实现,达到了多态的目的。这里很容易出现的一个问题 n 多的子类继承自抽象基类,我们不得不在每次要用到子类的地方就编写诸如 new  ×××;的代码。这里带来两个问题 1)客户程序员必须知道实际子类的名称(当系统复杂后,命名将是一个很不好处理的问题,为了处理可能的名字冲突,有的命名可能并不是具有很好的可读性和可记忆性,就姑且不论不同程序员千奇百怪的个人偏好了。 ) ,2)程序的扩展性和维护变得越来越困难。
     2)还有一种情况就是在父类中并不知道具体要实例化哪一个具体的子类。这里的意思为:假设我们在类 A 中要使用到类 B,B 是一个抽象父类,在 A 中并不知道具体要实例化那一个 B 的子类,但是在类 A的子类 D中是可以知道的。在 A中我们没有办法直接使用类似于 new  ×××的语句,因为根本就不知道×××是什么。   以上两个问题也就引出了 Factory模式的两个最重要的功能:  
     1)定义创建对象的接口,封装了对象的创建;
     2)使得具体化类的工作延迟到了子类中。
         Factory模式也带来至少以下两个问题:
          1)如果为每一个具体的 ConcreteProduct 类的实例化提供一个函数体,那么我们可能不得不在系统中添加了一个方法来处理这个新建的 ConcreteProduct,这样 Factory 的接口永远就不肯能封闭(Close) 。当然我们可以通过创建一个 Factory的子类来通过多态实现这一点,但是这也是以新建一个类作为代价的。
        2)在实现中我们可以通过参数化工厂方法,即给 FactoryMethod()传递一个参数用以决定是创建具体哪一个具体的 Product(实际上笔者在 VisualCMCS 中也正是这样做的) 。当然也可以通过模板化避免 1)中的子类创建子类,其方法就是将具体 Product 类作为模板参数,实现起来也很简单。
          可以看出,Factory 模式对于对象的创建给予开发人员提供了很好的实现策略,但是Factory 模式仅仅局限于一类类(就是说 Product 是一类,有一个共同的基类) ,如果我们要为不同类的类提供一个对象创建的接口,那就要用abstractFactory了。
     
    AbstractFactory模式就是用来解决这类问题的:要创建一组相关或者相互依赖的对象。
    1.2 AbstactFactory模式
     AbstractFactory 模式关键就是将这一组对象的创建封装到一个用于创建对象的类
    (ConcreteFactory) 中, 维护这样一个创建类总比维护 n 多相关对象的创建过程要简单的多。 
     AbstractFactory 模式和 Factory 模式的区别是初学(使用)设计模式时候的一个容易引起困惑的地方。实际上,AbstractFactory模式是为创建一组(有多类)相关或依赖的对象提供创建接口,而 Factory模式正如我在相应的文档中分析的是为一类对象提供创建接口或延迟对象的创建到子类中实现。并且可以看到,abstractFactory 模式通常都是使用 Factory 模式实现(ConcreteFactory1)。

    1.3 Singleton模式
      个人认为 Singleton 模式是设计模式中最为简单、最为常见、最容易实现,也是最应该熟悉和掌握的模式。且不说公司企业在招聘的时候为了考察员工对设计的了解和把握,考的最多的就是 Singleton 模式。
     Singleton 模式解决问题十分常见,我们怎样去创建一个唯一的变量(对象)?在基于对象的设计中我们可以通过创建一个全局变量(对象)来实现,在面向对象和面向过程结合的设计范式(如 C++中)中,我们也还是可以通过一个全局变量实现这一点。但是当我们遇到了纯粹的面向对象范式中,这一点可能就只能是通过 Singleton 模式来实现了,可能这也正是很多公司在招聘 Java 开发人员时候经常考察 Singleton 模式的缘故吧。  Singleton 模式在开发中非常有用,具体使用在讨论给出。
         在 Singleton 模式的结构图中可以看到,我们通过维护一个 static 的成员变量来记录这 个唯一的对象实例。通过提供一个 staitc 的接口instance 来获得这个唯一的实例。
     Singleton 模式的实现无须补充解释,需要说明的是,Singleton 不可以被实例化,因此我们将其构造函数声明为 protected或者直接声明为 private。
     Singleton 模式在开发中经常用到,且不说我们开发过程中一些变量必须是唯一的,比如说打印机的实例等等。
     Singleton 模式经常和 Factory(AbstractFactory)模式在一起使用,因为系统中工厂对象一般来说只要一个,笔者在开发 Visual CMCS 的时候,语义分析过程(以及其他过程)中都用到工厂模式来创建对象(对象实在是太多了) ,这里的工厂对象实现就是同时是一个Singleton 模式的实例,因为系统我们就只要一个工厂来创建对象就可以了。
    int main(int argc,char* argv[])
    {
      Singleton* sgn = Singleton::Instance(); 
     return 0;
    }

    1.4 Builder模式
      生活中有着很多的Builder的例子, 个人觉得大学生活就是一个Builder模式的最好体验:要完成大学教育,一般将大学教育过程分成 4 个学期进行,因此没有学习可以看作是构建完整大学教育的一个部分构建过程,每个人经过这 4 年的(4个阶段)构建过程得到的最后的结果不一样,因为可能在四个阶段的构建中引入了很多的参数(每个人的机会和际遇不完全相同) 。
     Builder 模式要解决的也正是这样的问题:当我们要创建的对象很复杂的时候(通常是由很多其他的对象组合而成) ,我们要要复杂对象的创建过程和这个对象的表示(展示)分离开来,这样做的好处就是通过一步步的进行复杂对象的构建,由于在每一步的构造过程中可以引入参数,使得经过相同的步骤创建最后得到的对象的展示不一样。
    Builder模式和AbstractFactory模式在功能上很相似,因为都是用来创建大的复杂的对象,它们的区别是:Builder模式强调的是一步步创建对象,并通过相同的创建过程可以获得不同的结果对象,一般来说Builder模式中对象不是直接返回的。而在AbstractFactory模式中对象是直接返回的,AbstractFactory模式强调的是为创建多个相互依赖的对象提供一个同一的接口。
    1.5 Prototype模式
    Prototype模式也正是提供了自我复制的功能,就是说新对象的创建可以通过已有对象进行创建。在C++中拷贝构造函数(Copy Constructor)曾经是很对程序员的噩梦,浅层拷贝和深层拷贝的魔魇也是很多程序员在面试时候的快餐和系统崩溃时候的根源之一。
    Prototype模式提供了一个通过已存在对象进行新对象创建的接口(Clone),Clone()实现和具体的实现语言相关,在C++中我们将通过拷贝构造函数实现之。

    Prototype模式的结构和实现都很简单,其关键就是(C++中)拷贝构造函数的实现方式,这也是C++实现技术层面上的事情。由于在示例代码中不涉及到深层拷贝(主要指有指针、复合对象的情况),因此我们通过编译器提供的默认的拷贝构造函数(按位拷贝)的方式进行实现。说明的是这一切只是为了实现简单起见,也因为本文档的重点不在拷贝构造函数的实现技术,而在Prototype模式本身的思想。
    Prototype模式通过复制原型(Prototype)而获得新对象创建的功能,这里Prototype本身就是“对象工厂”(因为能够生产对象),实际上Prototype模式和Builder模式、AbstractFactory模式都是通过一个类(对象实例)来专门负责对象的创建工作(工厂对象),它们之间的区别是:Builder模式重在复杂对象的一步步创建(并不直接返回对象),AbstractFactory模式重在产生多个相互依赖类的对象,而Prototype模式重在从自身复制自己创建新类。
    2  结构型模式
    2.1 Bridge模式
    总结面向对象实际上就两句话:一是松耦合(Coupling),二是高内聚(Cohesion)。面向对象系统追求的目标就是尽可能地提高系统模块内部的内聚(Cohesion)、尽可能降低模块间的耦合(Coupling)。然而这也是面向对象设计过程中最为难把握的部分,大家肯定在OO系统的开发过程中遇到这样的问题:
    1)客户给了你一个需求,于是使用一个类来实现(A);
    2)客户需求变化,有两个算法实现功能,于是改变设计,我们通过一个抽象的基类,再定义两个具体类实现两个不同的算法(A1和A2);
    3)客户又告诉我们说对于不同的操作系统,于是再抽象一个层次,作为一个抽象基类A0,在分别为每个操作系统派生具体类(A00和A01,其中A00表示原来的类A)实现不同操作系统上的客户需求,这样我们就有了一共4个类。
    4)可能用户的需求又有变化,比如说又有了一种新的算法……..
    5)我们陷入了一个需求变化的郁闷当中,也因此带来了类的迅速膨胀。
    Bridge模式则正是解决了这类问题。

    在Bridge模式的结构图中可以看到,系统被分为两个相对独立的部分,左边是抽象部分,右边是实现部分,这两个部分可以互相独立地进行修改:例如上面问题中的客户需求变化,当用户需求需要从Abstraction派生一个具体子类时候,并不需要像上面通过继承方式实现时候需要添加子类A1和A2了。另外当上面问题中由于算法添加也只用改变右边实现(添加一个具体化子类),而右边不用在变化,也不用添加具体子类了。
    Bridge是设计模式中比较复杂和难理解的模式之一,也是OO开发与设计中经常会用到的模式之一。使用组合(委托)的方式将抽象和实现彻底地解耦,这样的好处是抽象和实现可以分别独立地变化,系统的耦合性也得到了很好的降低。
    GoF在说明Bridge模式时,在意图中指出Bridge模式“将抽象部分与它的实现部分分离,使得它们可以独立地变化”。这句话很简单,但是也很复杂,连Bruce Eckel在他的大作《Thinking in Patterns》中说“Bridge模式是GoF所讲述得最不好(Poorly-described)的模式”,个人觉得也正是如此。原因就在于GoF的那句话中的“实现”该怎么去理解:“实现”特别是和“抽象”放在一起的时候我们“默认”的理解是“实现”就是“抽象”的具体子类的实现,但是这里GoF所谓的“实现”的含义不是指抽象基类的具体子类对抽象基类中虚函数(接口)的实现,是和继承结合在一起的。而这里的“实现”的含义指的是怎么去实现用户的需求,并且指的是通过组合(委托)的方式实现的,因此这里的实现不是指的继承基类、实现基类接口,而是指的是通过对象组合实现用户的需求。理解了这一点也就理解了Bridge模式,理解了Bridge模式,你的设计就会更加Elegant了。
    实际上上面使用Bridge模式和使用带来问题方式的解决方案的根本区别在于是通过继承还是通过组合的方式去实现一个功能需求。因此面向对象分析和设计中有一个原则就是:Favor Composition Over Inheritance。其原因也正在这里。
    2.2 Adapter模式
    实际上在软件系统设计和开发中,这种问题也会经常遇到:我们为了完成某项工作购买了一个第三方的库来加快开发。这就带来了一个问题:我们在应用程序中已经设计好了接口,与这个第三方提供的接口不一致,为了使得这些接口不兼容的类(不能在一起工作)可以在一起工作了,Adapter模式提供了将一个类(第三方库)的接口转化为客户(购买使用者)希望的接口。
    在上面生活中问题的解决方式也就正好对应了Adapter模式的两种类别:类模式和对象模式。
    在Adapter模式的结构图中可以看到,类模式的Adapter采用继承的方式复用Adaptee的接口,而在对象模式的Adapter中我们则采用组合的方式实现Adaptee的复用。有关这些具体的实现和分析将在代码说明和讨论中给出。
    Adapter模式实现上比较简单,要说明的是在类模式Adapter中,我们通过private继承Adaptee获得实现继承的效果,而通过public继承Target获得接口继承的效果(有关实现继承和接口继承参见讨论部分)。
    在Adapter模式的两种模式中,有一个很重要的概念就是接口继承和实现继承的区别和联系。接口继承和实现继承是面向对象领域的两个重要的概念,接口继承指的是通过继承,子类获得了父类的接口,而实现继承指的是通过继承子类获得了父类的实现(并不统共接口)。在C++中的public继承既是接口继承又是实现继承,因为子类在继承了父类后既可以对外提供父类中的接口操作,又可以获得父类的接口实现。当然我们可以通过一定的方式和技术模拟单独的接口继承和实现继承,例如我们可以通过private继承获得实现继承的效果(private继承后,父类中的接口都变为private,当然只能是实现继承了。),通过纯抽象基类模拟接口继承的效果,但是在C++中pure virtual function也可以提供默认实现,因此这是不纯正的接口继承,但是在Java中我们可以interface来获得真正的接口继承了.
    2.3 Decorator模式
    在OO设计和开发过程,可能会经常遇到以下的情况:我们需要为一个已经定义好的类添加新的职责(操作),通常的情况我们会给定义一个新类继承自定义好的类,这样会带来一个问题(将在本模式的讨论中给出)。通过继承的方式解决这样的情况还带来了系统的复杂性,因为继承的深度会变得很深。
    而Decorator提供了一种给类增加职责的方法,不是通过继承实现的,而是通过组合。
    在结构图中,ConcreteComponent和Decorator需要有同样的接口,因此ConcreteComponent和Decorator有着一个共同的父类。这里有人会问,让Decorator直接维护一个指向ConcreteComponent引用(指针)不就可以达到同样的效果,答案是肯定并且是否定的。肯定的是你可以通过这种方式实现,否定的是你不要用这种方式实现,因为通过这种方式你就只能为这个特定的ConcreteComponent提供修饰操作了,当有了一个新的ConcreteComponent你又要去新建一个Decorator来实现。但是通过结构图中的ConcreteComponent和Decorator有一个公共基类,就可以利用OO中多态的思想来实现只要是Component型别的对象都可以提供修饰操作的类,这种情况下你就算新建了100个Component型别的类ConcreteComponent,也都可以由Decorator一个类搞定。这也正是Decorator模式的关键和威力所在了。
    当然如果你只用给Component型别类添加一种修饰,则Decorator这个基类就不是很必要了。
    Decorator模式和Composite模式有相似的结构图,其区别在Composite模式已经详细讨论过了,请参看相应文档。另外GoF在《设计模式》中也讨论到Decorator和Proxy模式有很大程度上的相似,初学设计模式可能实在看不出这之间的一个联系和相似,并且它们在结构图上也很不相似。实际上,在本文档2.2节模式选择中分析到,让Decorator直接拥有一个ConcreteComponent的引用(指针)也可以达到修饰的功能,大家再把这种方式的结构图画出来,就和Proxy很相似了!
    Decorator模式和Proxy模式的相似的地方在于它们都拥有一个指向其他对象的引用(指针),即通过组合的方式来为对象提供更多操作(或者Decorator模式)间接性(Proxy模式)。
    但是他们的区别是,Proxy模式会提供使用其作为代理的对象一样接口,使用代理类将其操作都委托给Proxy直接进行。这里可以简单理解为组合和委托之间的微妙的区别了。
    Decorator模式除了采用组合的方式取得了比采用继承方式更好的效果,Decorator模式还给设计带来一种“即用即付”的方式来添加职责。在OO设计和分析经常有这样一种情况:为了多态,通过父类指针指向其具体子类,但是这就带来另外一个问题,当具体子类要添加新的职责,就必须向其父类添加一个这个职责的抽象接口,否则是通过父类指针是调用不到这个方法了。这样处于高层的父类就承载了太多的特征(方法),并且继承自这个父类的所有子类都不可避免继承了父类的这些接口,但是可能这并不是这个具体子类所需要的。而在Decorator模式提供了一种较好的解决方法,当需要添加一个操作的时候就可以通过Decorator模式来解决,你可以一步步添加新的职责。
    2.4 Composite模式
    在开发中,我们经常可能要递归构建树状的组合结构,Composite模式则提供了很好的解决方案。
    Composite模式在实现中有一个问题就是要提供对于子节点(Leaf)的管理策略,这里使用的是STL 中的vector,可以提供其他的实现方式,如数组、链表、Hash表等.
    Composite模式通过和Decorator模式有着类似的结构图,但是Composite模式旨在构造类,而Decorator模式重在不生成子类即可给对象添加职责。Decorator模式重在修饰,而Composite模式重在表示。
    2.5 Flyweight模式
    在面向对象系统的设计何实现中,创建对象是最为常见的操作。这里面就有一个问题:如果一个应用程序使用了太多的对象,就会造成很大的存储开销。特别是对于大量轻量级(细粒度)的对象,比如在文档编辑器的设计过程中,我们如果为没有字母创建一个对象的话,系统可能会因为大量的对象而造成存储开销的浪费。例如一个字母“a”在文档中出现了100000次,而实际上我们可以让这一万个字母“a”共享一个对象,当然因为在不同的位置可能字母“a”有不同的显示效果(例如字体和大小等设置不同),在这种情况我们可以为将对象的状态分为“外部状态”和“内部状态”,将可以被共享(不会变化)的状态作为内部状态存储在对象中,而外部对象(例如上面提到的字体、大小等)我们可以在适当的时候将外部对象最为参数传递给对象(例如在显示的时候,将字体、大小等信息传递给对象)。
    可以从图2-1中看出,Flyweight模式中有一个类似Factory模式的对象构造工厂FlyweightFactory,当客户程序员(Client)需要一个对象时候就会向FlyweightFactory发出请求对象的消息GetFlyweight()消息,FlyweightFactory拥有一个管理、存储对象的“仓库”(或者叫对象池,vector实现),GetFlyweight()消息会遍历对象池中的对象,如果已经存在则直接返回给Client,否则创建一个新的对象返回给Client。当然可能也有不想被共享的对象(例如结构图中的UnshareConcreteFlyweight),但不在本模式的讲解范围,故在实现中不给出。
    Flyweight模式在实现过程中主要是要为共享对象提供一个存放的“仓库”(对象池),这里是通过C++ STL中Vector容器,当然就牵涉到STL编程的一些问题(Iterator使用等)。另外应该注意的就是对对象“仓库”(对象池)的管理策略(查找、插入等),这里是通过直接的顺序遍历实现的,当然我们可以使用其他更加有效的索引策略,例如Hash表的管理策略,当时这些细节已经不是Flyweight模式本身要处理的了。
    我们在State模式和Strategy模式中会产生很多的对象,因此我们可以通过Flyweight模式来解决这个问题。
    2.6 Facade模式
    实际上在软件系统开发中也经常回会遇到这样的情况,可能你实现了一些接口(模块),而这些接口(模块)都分布在几个类中(比如A和B、C、D):A中实现了一些接口,B中实现一些接口(或者A代表一个独立模块,B、C、D代表另一些独立模块)。然后你的客户程序员(使用你设计的开发人员)只有很少的要知道你的不同接口到底是在那个类中实现的,绝大多数只是想简单的组合你的A-D的类的接口,他并不想知道这些接口在哪里实现的。
    这里的客户程序员就是上面生活中想办理手续的郁闷的人!在现实生活中我们可能可以很快想到找一个人代理所有的事情就可以解决你的问题(你只要维护和他的简单的一个接口而已了!),在软件系统设计开发中我们可以通过一个叫做Façade的模式来解决上面的问题。
    2.7 Proxy模式
    至少在以下集中情况下可以用Proxy模式解决问题:
    1)创建开销大的对象时候,比如显示一幅大的图片,我们将这个创建的过程交给代理去完成,GoF称之为虚代理(Virtual Proxy);
    2)为网络上的对象创建一个局部的本地代理,比如要操作一个网络上的一个对象(网络性能不好的时候,问题尤其突出),我们将这个操纵的过程交给一个代理去完成,GoF称之为远程代理(Remote Proxy);
    3)对对象进行控制访问的时候,比如在Jive论坛中不同权限的用户(如管理员、普通用户等)将获得不同层次的操作权限,我们将这个工作交给一个代理去完成,GoF称之为保护代理(Protection Proxy)。
    4)智能指针(Smart Pointer),关于这个方面的内容,建议参看Andrew Koenig的《C++沉思录》中的第5章。

    Proxy模式最大的好处就是实现了逻辑和实现的彻底解耦。
    3  行为模式
    3.1 Template模式
    在面向对象系统的分析与设计过程中经常会遇到这样一种情况:对于某一个业务逻辑(算法实现)在不同的对象中有不同的细节实现,但是逻辑(算法)的框架(或通用的应用算法)是相同的。Template提供了这种情况的一个实现框架。
    Template模式是采用继承的方式实现这一点:将逻辑(算法)框架放在抽象基类中,并定义好细节的接口,子类中实现细节。【注释1】
    【注释1】:Strategy模式解决的是和Template模式类似的问题,但是Strategy模式是将逻辑(算法)封装到一个类中,并采取组合(委托)的方式解决这个问题。
    解决2.1中问题可以采取两种模式来解决,一是Template模式,二是Strategy模式。本文当给出的是Template模式。一个通用的Template模式的结构图为:
    3.2 Strategy模式
    Template模式实际上就是利用面向对象中多态的概念实现算法实现细节和高层接口的松耦合。可以看到Template模式采取的是继承方式实现这一点的,由于继承是一种强约束性的条件,因此也给Template模式带来一些许多不方便的地方(有关这一点将在讨论中展开)。
    由于Template模式的实现代码很简单,因此解释是多余的。其关键是将通用算法(逻辑)封装起来,而将算法细节让子类实现(多态)。
    唯一注意的是我们将原语操作(细节算法)定义未保护(Protected)成员,只供模板方法调用(子类可以)。
    Template模式是很简单模式,但是也应用很广的模式。如上面的分析和实现中阐明的Template是采用继承的方式实现算法的异构,其关键点就是将通用算法封装在抽象基类中,并将不同的算法细节放到子类中实现。
    Template模式获得一种反向控制结构效果,这也是面向对象系统的分析和设计中一个原则DIP(依赖倒置:Dependency Inversion Principles)。其含义就是父类调用子类的操作(高层模块调用低层模块的操作),低层模块实现高层模块声明的接口。这样控制权在父类(高层模块),低层模块反而要依赖高层模块。
    继承的强制性约束关系也让Template模式有不足的地方,我们可以看到对于ConcreteClass类中的实现的原语方法Primitive1(),是不能被别的类复用。假设我们要创建一个AbstractClass的变体AnotherAbstractClass,并且两者只是通用算法不一样,其原语操作想复用AbstractClass的子类的实现。但是这是不可能实现的,因为ConcreteClass继承自AbstractClass,也就继承了AbstractClass的通用算法,AnotherAbstractClass是复用不了ConcreteClass的实现,因为后者不是继承自前者。
    Template模式暴露的问题也正是继承所固有的问题,Strategy模式则通过组合(委托)来达到和Template模式类似的效果,其代价就是空间和时间上的代价,关于Strategy模式的详细讨论请参考Strategy模式解析。
    Strategy模式和Template模式要解决的问题是相同(类似)的,都是为了给业务逻辑(算法)具体实现和抽象接口之间的解耦。Strategy模式将逻辑(算法)封装到一个类(Context)里面,通过组合的方式将具体算法的实现在组合对象中实现,再通过委托的方式将抽象接口的实现委托给组合对象实现。State模式也有类似的功能,他们之间的区别将在讨论中给出。
    这里的关键就是将算法的逻辑抽象接口(DoAction)封装到一个类中(Context),再通过委托的方式将具体的算法实现委托给具体的Strategy类来实现(ConcreteStrategeA类)。
    可以看到Strategy模式和Template模式解决了类似的问题,也正如在Template模式中分析的,Strategy模式和Template模式实际是实现一个抽象接口的两种方式:继承和组合之间的区别。要实现一个抽象接口,继承是一种方式:我们将抽象接口声明在基类中,将具体的实现放在具体子类中。组合(委托)是另外一种方式:我们将接口的实现放在被组合对象中,将抽象接口放在组合类中。这两种方式各有优缺点,先列出来:
    1) 继承:
    优点
    1)易于修改和扩展那些被复用的实现。
    缺点
    1)破坏了封装性,继承中父类的实现细节暴露给子类了;
    2)“白盒”复用,原因在1)中;
    3)当父类的实现更改时,其所有子类将不得不随之改变
    4)从父类继承而来的实现在运行期间不能改变(编译期间就已经确定了)。
    2) 组合
    优点
    1)“黑盒”复用,因为被包含对象的内部细节对外是不可见的;
    2)封装性好,原因为1);
    3)实现和抽象的依赖性很小(组合对象和被组合对象之间的依赖性小);
    4)可以在运行期间动态定义实现(通过一个指向相同类型的指针,典型的是抽象基类的指针)。
    缺点
    1)系统中对象过多。
    从上面对比中我们可以看出,组合相比继承可以取得更好的效果,因此在面向对象的设计中的有一条很重要的原则就是:优先使用(对象)组合,而非(类)继承(Favor Composition Over Inheritance)。
    实际上,继承是一种强制性很强的方式,因此也使得基类和具体子类之间的耦合性很强。例如在Template模式中在ConcreteClass1中定义的原语操作别的类是不能够直接复用(除非你继承自AbstractClass,具体分析请参看Template模式文档)。而组合(委托)的方式则有很小的耦合性,实现(具体实现)和接口(抽象接口)之间的依赖性很小,例如在本实现中,ConcreteStrategyA的具体实现操作很容易被别的类复用,例如我们要定义另一个Context类AnotherContext,只要组合一个指向Strategy的指针就可以很容易地复用ConcreteStrategyA的实现了。
    我们在Bridge模式的问题和Bridge模式的分析中,正是说明了继承和组合之间的区别。请参看相应模式解析。
    另外Strategy模式很State模式也有相似之处,但是State模式注重的对象在不同的状态下不同的操作。两者之间的区别就是State模式中具体实现类中有一个指向Context的引用,而Strategy模式则没有。具体分析请参看相应的State模式分析中。
    3.3 State模式
    每个人、事物在不同的状态下会有不同表现(动作),而一个状态又会在不同的表现下转移到下一个不同的状态(State)。最简单的一个生活中的例子就是:地铁入口处,如果你放入正确的地铁票,门就会打开让你通过。在出口处也是验票,如果正确你就可以ok,否则就不让你通过(如果你动作野蛮,或许会有报警(Alarm),:))。
    有限状态自动机(FSM)也是一个典型的状态不同,对输入有不同的响应(状态转移)。通常我们在实现这类系统会使用到很多的Switch/Case语句,Case某种状态,发生什么动作,Case另外一种状态,则发生另外一种状态。但是这种实现方式至少有以下两个问题:
    1)当状态数目不是很多的时候,Switch/Case可能可以搞定。但是当状态数目很多的时候(实际系统中也正是如此),维护一大组的Switch/Case语句将是一件异常困难并且容易出错的事情。
    2)状态逻辑和动作实现没有分离。在很多的系统实现中,动作的实现代码直接写在状态的逻辑当中。这带来的后果就是系统的扩展性和维护得不到保证。
    State模式就是被用来解决上面列出的两个问题的,在State模式中我们将状态逻辑和动作实现进行分离。当一个操作中要维护大量的case分支语句,并且这些分支依赖于对象的状态。State模式将每一个分支都封装到独立的类中。State模式典型的结构图为:
    State模式在实现中,有两个关键点:
    1)将State声明为Context的友元类(friend class),其作用是让State模式访问Context的protected接口ChangeSate()。
    2)State及其子类中的操作都将Context*传入作为参数,其主要目的是State类可以通过这个指针调用Context中的方法(在本示例代码中没有体现)。这也是State模式和Strategy模式的最大区别所在。
    运行了示例代码后可以获得以下的结果:连续3次调用了Context的OprationInterface()因为每次调用后状态都会改变(A-B-A),因此该动作随着Context的状态的转变而获得了不同的结果。
    State模式的应用也非常广泛,从最高层逻辑用户接口GUI到最底层的通讯协议(例如GoF在《设计模式》中就利用State模式模拟实现一个TCP连接的类。)都有其用武之地。
    State模式和Strategy模式又很大程度上的相似:它们都有一个Context类,都是通过委托(组合)给一个具有多个派生类的多态基类实现Context的算法逻辑。两者最大的差别就是State模式中派生类持有指向Context对象的引用,并通过这个引用调用Context中的方法,但在Strategy模式中就没有这种情况。因此可以说一个State实例同样是Strategy模式的一个实例,反之却不成立。实际上State模式和Strategy模式的区别还在于它们所关注的点不尽相同:State模式主要是要适应对象对于状态改变时的不同处理策略的实现,而Strategy则主要是具体算法和实现接口的解耦(coupling),Strategy模式中并没有状态的概念(虽然很多时候有可以被看作是状态的概念),并且更加不关心状态的改变了。
    State模式很好地实现了对象的状态逻辑和动作实现的分离,状态逻辑分布在State的派生类中实现,而动作实现则可以放在Context类中实现(这也是为什么State派生类需要拥有一个指向Context的指针)。这使得两者的变化相互独立,改变State的状态逻辑可以很容易复用Context的动作,也可以在不影响State派生类的前提下创建Context的子类来更改或替换动作实现。
    State模式问题主要是逻辑分散化,状态逻辑分布到了很多的State的子类中,很难看到整个的状态逻辑图,这也带来了代码的维护问题。
    3.4 Observer模式
    Observer模式应该可以说是应用最多、影响最广的模式之一,因为Observer的一个实例Model/View/Control(MVC)结构在系统开发架构设计中有着很重要的地位和意义,MVC实现了业务逻辑和表示层的解耦。个人也认为Observer模式是软件开发过程中必须要掌握和使用的模式之一。在MFC中,Doc/View(文档视图结构)提供了实现MVC的框架结构(有一个从设计模式(Observer模式)的角度分析分析Doc/View的文章正在进一步的撰写当中,遗憾的是时间:))。在Java阵容中,Struts则提供和MFC中Doc/View结构类似的实现MVC的框架。另外Java语言本身就提供了Observer模式的实现接口,这将在讨论中给出。
    当然,MVC只是Observer模式的一个实例。Observer模式要解决的问题为:建立一个一(Subject)对多(Observer)的依赖关系,并且做到当“一”变化的时候,依赖这个“一”的多也能够同步改变。最常见的一个例子就是:对同一组数据进行统计分析时候,我们希望能够提供多种形式的表示(例如以表格进行统计显示、柱状图统计显示、百分比统计显示等)。这些表示都依赖于同一组数据,我们当然需要当数据改变的时候,所有的统计的显示都能够同时改变。Observer模式就是解决了这一个问题。
    这里的目标Subject提供依赖于它的观察者Observer的注册(Attach)和注销(Detach)操作,并且提供了使得依赖于它的所有观察者同步的操作(Notify)。观察者Observer则提供一个Update操作,注意这里的Observer的Update操作并不在Observer改变了Subject目标状态的时候就对自己进行更新,这个更新操作要延迟到Subject对象发出Notify通知所有Observer进行修改(调用Update)。
    在Observer模式的实现中,Subject维护一个list作为存储其所有观察者的容器。每当
    调用Notify操作就遍历list中的Observer对象,并广播通知改变状态(调用Observer的Update操作)。目标的状态state可以由Subject自己改变(示例),也可以由Observer的某个操作引起state的改变(可调用Subject的SetState操作)。Notify操作可以由Subject目标主动广播(示例),也可以由Observer观察者来调用(因为Observer维护一个指向Subject的指针)。
    运行示例程序,可以看到当Subject处于状态“old”时候,依赖于它的两个观察者都显示“old”,当目标状态改变为“new”的时候,依赖于它的两个观察者也都改变为“new”Observer是影响极为深远的模式之一,也是在大型系统开发过程中要用到的模式之一。除了MFC、Struts提供了MVC的实现框架,在Java语言中还提供了专门的接口实现Observer模式:通过专门的类Observable及Observer接口来实现MVC编程模式,其UML图可以表示为:
    这里的Observer就是观察者,Observable则充当目标Subject的角色。
    Observer模式也称为发布-订阅(publish-subscribe),目标就是通知的发布者,观察者则是通知的订阅者(接受通知)。
    3.5 Memento模式
    没有人想犯错误,但是没有人能够不犯错误。犯了错误一般只能改过,却很难改正(恢复)。世界上没有后悔药,但是我们在进行软件系统的设计时候是要给用户后悔的权利(实际上可能也是用户要求的权利:)),我们对一些关键性的操作肯定需要提供诸如撤销(Undo)的操作。那这个后悔药就是Memento模式提供的。
    Memento模式的关键就是要在不破坏封装行的前提下,捕获并保存一个类的内部状态,这样就可以利用该保存的状态实施恢复操作。为了达到这个目标,可以在后面的实现中看到我们采取了一定语言支持的技术。Memento模式的典型结构图为:
    Memento模式的关键就是friend class Originator;我们可以看到,Memento的接口都声明为private,而将Originator声明为Memento的友元类。我们将Originator的状态保存在Memento类中,而将Memento接口private起来,也就达到了封装的功效。
    在Originator类中我们提供了方法让用户后悔:RestoreToMemento(Memento* mt);我们可以通过这个接口让用户后悔。在测试程序中,我们演示了这一点:Originator的状态由old变为new最后又回到了old。
    讨论
    在Command模式中,Memento模式经常被用来维护可以撤销(Undo)操作的状态。这一点将在Command模式具体说明。
    3.6 Mediator模式
    在面向对象系统的设计和开发过程中,对象之间的交互和通信是最为常见的情况,因为对象间的交互本身就是一种通信。在系统比较小的时候,可能对象间的通信不是很多、对象也比较少,我们可以直接硬编码到各个对象的方法中。但是当系统规模变大,对象的量变引起系统复杂度的急剧增加,对象间的通信也变得越来越复杂,这时候我们就要提供一个专门处理对象间交互和通信的类,这个中介者就是Mediator模式。Mediator模式提供将对象间的交互和通讯封装在一个类中,各个对象间的通信不必显势去声明和引用,大大降低了系统的复杂性能(了解一个对象总比深入熟悉n个对象要好)。另外Mediator模式还带来了系统对象间的松耦合,这些将在讨论中详细给出。
    Mediator模式典型的结构图为:
    Mediator模式中,每个Colleague维护一个Mediator,当要进行交互,例如图中ConcreteColleagueA和ConcreteColleagueB之间的交互就可以通过ConcreteMediator提供的DoActionFromAtoB来处理,ConcreteColleagueA和ConcreteColleagueB不必维护对各自的引用,甚至它们也不知道各个的存在。Mediator通过这种方式将多对多的通信简化为了一(Mediator)对多(Colleague)的通信。
    Mediator模式的实现关键就是将对象Colleague之间的通信封装到一个类种单独处理,为了模拟Mediator模式的功能,这里给每个Colleague对象一个string型别以记录其状态,并通过状态改变来演示对象之间的交互和通信。这里主要就Mediator的示例运行结果给出分析:
    1)将ConcreteColleageA对象设置状态“old”,ConcreteColleageB也设置状态“old”;
    2)ConcreteColleageA对象改变状态,并在Action中和ConcreteColleageB对象进行通信,并改变ConcreteColleageB对象的状态为“new”;
    3)ConcreteColleageB对象改变状态,并在Action中和ConcreteColleageA对象进行通信,并改变ConcreteColleageA对象的状态为“new”;
    注意到,两个Colleague对象并不知道它交互的对象,并且也不是显示地处理交互过程,这一切都是通过Mediator对象完成的,示例程序运行的结果也正是证明了这一点。
    Mediator模式是一种很有用并且很常用的模式,它通过将对象间的通信封装到一个类中,将多对多的通信转化为一对多的通信,降低了系统的复杂性。Mediator还获得系统解耦的特性,通过Mediator,各个Colleague就不必维护各自通信的对象和通信协议,降低了系统的耦合性,Mediator和各个Colleague就可以相互独立地修改了。
    Mediator模式还有一个很显著额特点就是将控制集中,集中的优点就是便于管理,也正式符合了OO设计中的每个类的职责要单一和集中的原则。
    3.7 Command模式
    Command模式通过将请求封装到一个对象(Command)中,并将请求的接受者存放到具体的ConcreteCommand类中(Receiver)中,从而实现调用操作的对象和操作的具体实现者之间的解耦。
    Command模式结构图中,将请求的接收者(处理者)放到Command的具体子类ConcreteCommand中,当请求到来时(Invoker发出Invoke消息激活Command对象),ConcreteCommand将处理请求交给Receiver对象进行处理。
    Command模式在实现的实现和思想都很简单,其关键就是将一个请求封装到一个类中(Command),再提供处理对象(Receiver),最后Command命令由Invoker激活。另外,我们可以将请求接收者的处理抽象出来作为参数传给Command对象,实际也就是回调的机制(Callback)来实现这一点,也就是说将处理操作方法地址(在对象内部)通过参数传递给Command对象,Command对象在适当的时候(Invoke激活的时候)再调用该函数。这里就要用到C++中的类成员函数指针的概念,为了方便学习,这里给出一个简单的实现源代码供参考:
    Command模式的思想非常简单,但是Command模式也十分常见,并且威力不小。实际上,Command模式关键就是提供一个抽象的Command类,并将执行操作封装到Command类接口中,Command类中一般就是只是一些接口的集合,并不包含任何的数据属性(当然在示例代码中,我们的Command类有一个处理操作的Receiver类的引用,但是其作用也仅仅就是为了实现这个Command的Excute接口)。这种方式在是纯正的面向对象设计者最为鄙视的设计方式,就像OO设计新手做系统设计的时候,仅仅将Class作为一个关键字,将C种的全局函数找一个类封装起来就以为是完成了面向对象的设计。
    但是世界上的事情不是绝对的,上面提到的方式在OO设计种绝大部分的时候可能是一个不成熟的体现,但是在Command模式中却是起到了很好的效果。主要体现在:
    1) Command模式将调用操作的对象和知道如何实现该操作的对象解耦。在上面Command的结构图中,Invoker对象根本就不知道具体的是那个对象在处理Excute操作(当然要知道是Command类别的对象,也仅此而已)。
    2) 在Command要增加新的处理操作对象很容易,我们可以通过创建新的继承自Command的子类来实现这一点。
    3) Command模式可以和Memento模式结合起来,支持取消的操作。
    3.8 Visitor模式
    在面向对象系统的开发和设计过程,经常会遇到一种情况就是需求变更(Requirement Changing),经常我们做好的一个设计、实现了一个系统原型,咱们的客户又会有了新的需求。我们又因此不得不去修改已有的设计,最常见就是解决方案就是给已经设计、实现好的类添加新的方法去实现客户新的需求,这样就陷入了设计变更的梦魇:不停地打补丁,其带来的后果就是设计根本就不可能封闭、编译永远都是整个系统代码。
    Visitor模式则提供了一种解决方案:将更新(变更)封装到一个类中(访问操作),并由待更改类提供一个接收接口,则可达到效果。
    Visitor模式在不破坏类的前提下,为类提供增加新的新操作。Visitor模式的关键是双分派(Double-Dispatch)的技术【注释1】。C++语言支持的是单分派。
    在Visitor模式中Accept()操作是一个双分派的操作。具体调用哪一个具体的Accept()操作,有两个决定因素:1)Element的类型。因为Accept()是多态的操作,需要具体的Element类型的子类才可以决定到底调用哪一个Accept()实现;2)Visitor的类型。Accept()操作有一个参数(Visitor* vis),要决定了实际传进来的Visitor的实际类别才可以决定具体是调用哪个VisitConcrete()实现。
    Visitor模式的实现过程中有以下的地方要注意:
    1)Visitor类中的Visit()操作的实现。
    这里我们可以向Element类仅仅提供一个接口Visit(),而在Accept()实现中具体调用哪一个Visit()操作则通过函数重载(overload)的方式实现:我们提供Visit()的两个重载版本a)Visit(ConcreteElementA* elmA),b)Visit(ConcreteElementB* elmB)。
    在C++中我们还可以通过RTTI(运行时类型识别:Runtime type identification)来实现,即我们只提供一个Visit()函数体,传入的参数为Element*型别参数 ,然后用RTTI决定具体是哪一类的ConcreteElement参数,再决定具体要对哪个具体类施加什么样的具体操作。【注释2】RTTI给接口带来了简单一致性,但是付出的代价是时间(RTTI的实现)和代码的Hard编码(要进行强制转换)。
    有时候我们需要为Element提供更多的修改,这样我们就可以通过为Element提供一系列的
    Visitor模式可以使得Element在不修改自己的同时增加新的操作,但是这也带来了至少以下的两个显著问题:
    1) 破坏了封装性。Visitor模式要求Visitor可以从外部修改Element对象的状态,这一般通过两个方式来实现:a)Element提供足够的public接口,使得Visitor可以通过调用这些接口达到修改Element状态的目的;b)Element暴露更多的细节给Visitor,或者让Element提供public的实现给Visitor(当然也给了系统中其他的对象),或者将Visitor声明为Element的friend类,仅将细节暴露给Visitor。但是无论那种情况,特别是后者都将是破坏了封装性原则(实际上就是C++的friend机制得到了很多的面向对象专家的诟病)。
    2) ConcreteElement的扩展很困难:每增加一个Element的子类,就要修改Visitor的
    接口,使得可以提供给这个新增加的子类的访问机制。从上面我们可以看到,或者增加一个用于处理新增类的Visit()接口,或者重载一个处理新增类的Visit()操作,或者要修改RTTI方式实现的Visit()实现。无论那种方式都给扩展新的Element子类带来了困难。
    3.9 Chain of Responsibility模式
    熟悉VC/MFC的都知道,VC是“基于消息,事件驱动”,消息在VC开发中起着举足轻重的作用。在MFC中,消息是通过一个向上递交的方式进行处理,例如一个WM_COMMAND消息的处理流程可能为:
    1) MDI主窗口(CMDIFrameWnd)收到命令消息WM_COMMAND,其ID位ID_×××;
    2) MDI主窗口将消息传给当前活动的MDI子窗口(CMDIChildWnd);
    3) MDI子窗口给自己的子窗口(View)一个处理机会,将消息交给View;
    4) View检查自己Message Map;
    5) 如果View没有发现处理该消息的程序,则将该消息传给其对应的Document对象;否则View处理,消息流程结束。
    6) Document检查自己Message Map,如果没有该消息的处理程序,则将该消息传给其对象的DocumentTemplate处理;否则自己处理,消息流程结束;
    7) 如果在6)中消息没有得到处理,则将消息返回给View;
    8) View再传回给MDI子窗口;
    9) MDI子窗口将该消息传给CwinApp对象,CwinApp为所有无主的消息提供了处理。
    注明:有关MFC消息处理更加详细信息,请参考候捷先生的《深入浅出MFC》。
    MFC提供了消息的处理的链式处理策略,处理消息的请求将沿着预先定义好的路径依
    次进行处理。消息的发送者并不知道该消息最后是由那个具体对象处理的,当然它也无须也不想知道,但是结构是该消息被某个对象处理了,或者一直到一个终极的对象进行处理了。
    Chain of Responsibility模式描述其实就是这样一类问题将可能处理一个请求的对象链接成一个链,并将请求在这个链上传递,直到有对象处理该请求(可能需要提供一个默认处理所有请求的类,例如MFC中的CwinApp类)。
    Chain of Responsibility模式中ConcreteHandler将自己的后继对象(向下传递消息的对象)记录在自己的后继表中,当一个请求到来时,ConcreteHandler会先检查看自己有没有匹配的处理程序,如果有就自己处理,否则传递给它的后继。当然这里示例程序中为了简化,ConcreteHandler只是简单的检查看自己有没有后继,有的话将请求传递给后继进行处理,没有的话就自己处理。
    Chain of Responsibility模式的示例代码实现很简单,这里就其测试结果给出说明:
    ConcreteHandleA的对象和h1拥有一个后继ConcreteHandleB的对象h2,当一个请求到来时候,h1检查看自己有后继,于是h1直接将请求传递给其后继h2进行处理,h2因为没有后继,当请求到来时候,就只有自己提供响应了。于是程序的输出为:
    1) ConcreteHandleA 我把处理权给后继节点.....;
    2) ConcreteHandleB 没有后继了,我必须自己处理....。
     讨论
    Chain of Responsibility模式的最大的一个有点就是给系统降低了耦合性,请求的发送者完全不必知道该请求会被哪个应答对象处理,极大地降低了系统的耦合性。
    3.10 Iterator模式
    Iterator模式应该是最为熟悉的模式了,最简单的证明就是我在实现Composite模式、Flyweight模式、Observer模式中就直接用到了STL提供的Iterator来遍历Vector或者List数据结构。
    Iterator模式也正是用来解决对一个聚合对象的遍历问题,将对聚合的遍历封装到一个类中进行,这样就避免了暴露这个聚合对象的内部表示的可能。
    Iterator模式中定义的对外接口可以视客户成员的便捷定义,但是基本的接口在图中的Iterator中已经给出了(参考STL的Iterator就知道了)。
    Iterator模式的实现代码很简单,实际上为了更好地保护Aggregate的状态,我们可以尽量减小Aggregate的public接口,而通过将Iterator对象声明位Aggregate的友元来给予Iterator一些特权,获得访问Aggregate私有数据和方法的机会。
    讨论
    Iterator模式的应用很常见,我们在开发中就经常会用到STL中预定义好的Iterator来对STL类进行遍历(Vector、Set等)。
    3.11 Interpreter模式
    一些应用提供了内建(Build-In)的脚本或者宏语言来让用户可以定义他们能够在系统中进行的操作。Interpreter模式的目的就是使用一个解释器为用户提供一个一门定义语言的语法表示的解释器,然后通过这个解释器来解释语言中的句子。
    Interpreter模式提供了这样的一个实现语法解释器的框架,笔者曾经也正在构建一个编译系统Visual CMCS,现在已经发布了Visual CMCS1.0 (Beta),请大家访问Visual CMCS网站获取详细信息。
    Interpreter模式中,提供了TerminalExpression和NonterminalExpression两种表达式的解释方式,Context类用于为解释过程提供一些附加的信息(例如全局的信息)。
    XML格式的数据解析是一个在应用开发中很常见并且有时候是很难处理的事情,虽然目前很多的开发平台、语言都提供了对XML格式数据的解析,但是例如到了移动终端设备上,由于处理速度、计算能力、存储容量的原因解析XML格式的数据却是很复杂的一件事情,最近也提出了很多的移动设备的XML格式解析器,但是总体上在项目开发时候还是需要自己去设计和实现这一个过程(笔者就有过这个方面的痛苦经历)。
    Interpreter模式则提供了一种很好的组织和设计这种解析器的架构。
    Interpreter模式中使用类来表示文法规则,因此可以很容易实现文法的扩展。另外对于终结符我们可以使用Flyweight模式来实现终结符的共享。
    4  说明
  • 相关阅读:
    <iframe>相关问题
    文字描边
    jquery 设置css margin-left
    uni-app mustache表达式
    Uni-app 响应式像素upx
    Uni-app页面进入和返回
    Uni-app 生命周期
    Sql添加/删除默认值
    Uni-app初体验(创建新路由)
    Uni-app初体验(页面绑定数据)
  • 原文地址:https://www.cnblogs.com/lqc1002000/p/2437648.html
Copyright © 2020-2023  润新知