GOF设计模式

【前言】
(1),在面向对象中,类的关系分为以下几种--
依赖,一般是一个类想要实现某个行为就必须借助另一个类的帮助,这种关系是临时的,体现在代码上比如传参,包含另一个类的头文件并在自己的方法实现中new它以获取实例等等。
关联,这种关系一般是比较持久的,在代码上的实现是一个类里面定义一个成员指针变量,通过这个变量获取另一个类对象的地址。
聚合,关联方式的一种,比如在一个类里维护一个指针数组成员,每个指针指向一个关联的对象,这些被关联的对象通过这种方式就聚合在一起从而形成该聚合对象。(聚合进聚合对象的对象的生命周期与聚合对象本身没有直接联系)。
组合(合成),比如在一个对象的类定义里面添加另一个类类型的成员变量,后者组合形成了这个新类型的对象。(组合进聚合对象的对象的生命周期和聚合对象保持一致,一个子类继承多个父类,并且该类由多个其他类型的对象组合而成,那么构造器的调用顺序是,第一个父类,第二个父类... ...,第一个声明的组件对象,第二个声明的组件对象... ...,最后再调用自己的构造器,析构器的调用顺序与此相反,此种调用规则是递归进行的)。
继承,子类接口继承纯抽象类(这种类继承==接口实现)。子类接口继承和实现继承父类。
(2),类与类的这几种关系的耦合度由弱到强依次是--
依赖 < 关联 < 聚合 < 组合 < 继承 = 实现。
设计模式其实就是对类的这六种关系的应用。




【一】状态模式--对象行为型模式:
理解:
上下文对象维护一个状态对象的指针,上下文对象将它的与状态相关的行为委托给状态对象来封装。实现程序运行期间上下文对象的行为随着状态变化来动态的改变。


好处:
(1),在游戏状态要变化的时候,只需要实例化相应状态的子类对象,把这个状态对象赋予GameManager,GameManager将它有关状态的行为委托给这个状态实例对象,此对象封装了有关状态下的行为,这么一来状态变化了GameManager对应的行为也会跟着变化,实现GameManager行为可以动态变化。类继承也可以给一个对象添加行为,但是这种行为在编译时就静态确定好了,所以在游戏运行期间用户是无法灵活改变的,状态模式不一样,可以用每一个状态子类对象的替换来在运行期间变化与状态相关的对象的行为。
(2),现在的GameManager里面,很多方法都要做与状态有关的判断逻辑继而执行相应的功能,这么一来,假如十个方法需要做这项判断逻辑,而且现在的状态有五种,代码里的if...else或者switch语句会剧增,导致的恶果就是代码结构混乱,对应方法的可读性、维护性以及复用性大大降低。多个状态对象把这些分支判断封装进自己的类里面,解决了方法里面做判断的问题,代码结构清晰,还有就是想要再添加新的状态的时候,只需要增加新的状态子类就行,不需要更改原来的方法实现,这个正好符合了开闭原则。
(3),只在状态对象里封装行为,减少属性的封装,需要数据了就用参数传进去,这么一来这个状态类就是轻量级的,更容易拿来共享。想想一个状态里封装了很多属性,用户甲维护该状态时修改了一些属性值,用户乙也想共享这个状态对象,但是它是改还是不改这些状态属性呢,改了的话,用户甲也在使用它,会不会对用户甲造成影响呢。


弊端:
状态很多的话,导致子类变多,维护这些类以及类之间的通信的成本就会增加。


注意事项:
(1),状态的切换问题,可以由状态拥有者(GameManager)自己来驾驭,也可以交由每个状态自身维护一个后继状态,交由状态对象来处理,后者更加灵活,但是每个状态至少知道一个其他状态对象的存在,彼此有了实现依赖。
(2),创建与销毁,一是需要时创建,不用了就销毁。二是开始时创建,后面就一个存在下去。如果状态不经常改变或者一个状态对象存储着大量的信息时,前者会更好。如果状态频繁切换后者会更好。


例子:
GameManager里面E_GAMESTATE枚举类型的m_dwGameState,可以把各个枚举常量表示的当前游戏状态封装成单独的一个类,这些类以一个抽象类作为父类,在GameManager里面维护一个抽象类的指针变量,每个状态因为数量是确定的,而且在场景切换时会频繁变化,所以把它们都当做一个个单例来处理会好些。




【二】命令模式--对象行为型模式:
理解:
函数调用我们可以视为一个消息请求,对应对象执行该函数的实现部分我们可以视为一个对消息的处理过程,返回值当然就是返回响应了,回到调用者内部后执行的过程,如果需要返回值来协助做处理,也就是处理响应了。
现在的问题是甲发一个请求给乙,甲和乙就是直接耦合在一起了,甲的命令(也就是请求)是固定的,这个固定的东西就是乙的处理请求的过程。如果甲现在不知道请求的响应者是谁,只是负责发命令,谁来处理都可以,或者甲想要同样的命令收到不同的效果,那么这种耦合的实现显然是不够灵活也是不可取的。
命令模式就是将请求者和接受者进行了解耦,由命令对象来做请求者的接受者,真正的接受者来做命令对象的接受者。原先设计上的确定的请求现在也就变成了可变的抽象的请求了。抽象的具体实现交由每个不同的命令自行负责。
其实命令模式是C语言里面的回调函数机制的面向对象的实现方式。在C里面,用户开始注册一个函数指针,在满足一定条件时由被注册者回调这个先前注册了的函数,当然这种实现方式是面向过程式的,没有对象的概念可言。而命令模式是在先前的某个时机,用户将一个命令对象注册给以后会请求命令的对象,在满足一定条件的时候,请求者发出这个命令,处理逻辑转移到命令对象本身来处理,然后命令本身再去调用真正的接受者去处理请求。当然也可以由命令对象自己负责接受者的职责,这也就成了退化了的命令模式了,它和原先的甲向乙做请求也就无异了。我们在做函数调用其实就是在做这种退化的命令模式。


好处:
将请求者与接收者进行解耦,请求的具体实现不再是固定的,而是灵活可变的。我们可以单独去复用请求对象和接收对象,而不用担心它们二者会有依赖关系导致单独只复用一个会造成复用失败的问题。


弊端:
需要对命令集做维护,提高了维护成本。


注意事项:
(1),可以和组合模式混用以实现宏命令(批量命令)。
(2),和备忘录模式混用以实现撤销命令。
(3),如果命令的接受者不需要在运行期间变更,可以用类模板的形式实现接受者类型的参数化(将一个具体接受者的对象指针和其成员函数传进命令对象即可),这样实现的好处是命令子类数量只有一份,而不是需要定义新的命令就得增加子类实现。缺点是命令所维护的接受者是具体的,你无法在运行期间做变更。


例子:
游戏里现在的函数回调机制就可以换用命令模式来实现,我现在想要取消命令,由一个对象封装好了,以后有关该取消的操作我都可以去复用这个命令对象,不用每次都去实现一个回调函数,方便代码的模块化管理。




【三】职责链模式--对象行为型模式:
理解:
将处理请求的对象组织成一个序列,每一个处理者对象维护一个后继处理者的引用,当请求者发出一个请求,这个请求实体会在链中传递,直到请求被处理或者传送到链末截止。


好处:
(1),处理者对象集合链可以动态的add和remove它的处理者实体对象,从而改变处理一个请求的那些职责。
(2),处理者不知道到底是谁最初发的请求,请求者也不知道最终是由谁来处理了它发出去的请求,所以处理者们和发请求的对象实现了解耦。
(3),可以定义一个处理者对象集的公共接口类,用于缺省实现,比如维护一个后继对象的引用并且默认将请求直接转发给它的后继者,而相应子类的实现是先确定自己能否处理该请求,可以就处理,不可以就调用它的父类函数缺省实现将请求转发给后继者。这么一来转发请求和维护后继者的代码逻辑被父类提炼成一份共有功能,减少了子类的冗余实现。


弊端:
因为请求者不明确它的处理者是谁,所以这个请求可能会因为处理链没有正确被配置或其他原因导致这个请求没有被处理。


注意事项:
(1),在比如树形结构这种地方,如果子节点定义了父节点的引用而且这种链接路径符合职责链所要求的处理职责的话,那么可以复用这种链接而无需单独定义后继者引用来组织职责链。
(2),可以将请求作为固定的第一个处理者对象的方法调用来实现,还有一种办法比较灵活多变,就是利用命令模式将请求单独封装成类,从而实现请求的多变性(支持多种请求)。


例子:
游戏里面现在到处充斥着提示帮助的页面逻辑,可以把这些帮助信息组织成职责链来维护。比如在跨服战页面上点击一个押注规则说明按钮,请求在传递的时候,先会在职责链中找押注相关的规则显示,如果这个规则策划忘了配置,那么继续传递到跨服战玩法规则上,从而将玩法规则显示给玩家。这么一来就避免了点击一个帮助按钮却因为没有帮助信息而显示一个空页面的问题了。




【四】中介者模式--对象行为型模式:
理解:
如果存在一个对象集合,它们之间需要彼此依赖才能完成工作,随着对象数量的增加,这种依赖关系就会变的错综复杂难以掌控。我们可以把这些依赖关系抽象出来封装成一个单独的类,这个类就是中介者。


好处:
(1),中介者将各个同事类进行了解耦,因为同事类之间不存在依赖关系,所以现在就可以复用一个单独的同事类了(用户不必担心这个同事类因为找不到其他的同事而无法完成工作)。
(2),同事们之间如果没有项目负责人去协调工作,都是你找我我找他的来完成一个工作,那么这种关系就是多对多的,一是工作关系混乱,难以搞清楚。再者就是效率低下,工作流程卡在一个同事那里就会导致整个工作无法进展了。有了项目负责人(中介者)的话,所有同事只需把它们的请求告诉负责人,由负责人决定谁来处理请求,比如同事甲对一个需求不明白,告诉负责人,负责人联系其他同事,他知道乙也不清楚,就会找到相应的清楚这件事的丙来处理。可以想想,如果是在很少人的项目组里面,感觉这么做多此一举,还不如直接点儿来得更快。但是如果在一个拥有上百号人的项目组,恐怕很多人都不认识,或者认识也不清楚对方到底是搞哪方面的,这时候如果有一个负责人,他专门负责这些同事之间的工作交互,那么工作效率明显就会提高很多。
(3),中介者与同事们是一对多的关系,它的存在将原本多对多的关系转化为一对多的关系。这种关系显然更易于理解和维护。


弊端:
因为中介者将同事类之间的交互行为集于一身,原先同事类之间交互的复杂性现在变成了中介者的复杂性,这致使中介者比任何一个同事类都复杂的多。


注意事项:
(1),如果对象集只需一个中介者,就没有必要提供抽象中介者父类。当然中介者不唯一的话可以这么做,同事类与抽象耦合而不与具体的中介者耦合,会允许同事类与多个具体的中介者交互。
(2),可以结合观察者模式和中介者模式,观察者模式提供促使各个同事状态一致的机制,中介者维护各个同事之间的交互却不会使各同事之间关系耦合。
(3),看似外观模式和中介者模式的类图关系一致,但是别忘了:外观模式是为了一个子系统好用而抽象出一个公共接口供外部来使用,它简化了子系统的使用,外部用户向外观发请求,外观向子系统发请求从而完成相应的工作,这种交互是单向的。我们用cocos2dx里面的SimpleAudioEngine单例外观对象时,都是向它发出请求,它再去调用相应平台上实现的音频接口来完成工作的。没见过SimpleAudioEngine向用户发出请求。对于中介者模式,交互却是双向的,同事甲->中介者->同事乙,这种关系反过来也可以。而且对于外观模式,如果专业点儿的人想直接利用子系统来满足他的需求,就可以直接跳过外观,外观不会对子系统实现对外隐藏,但是中介者模式里的同事们彼此没有依赖关系,就只能通过中介者来交互了。


例子:
我们游戏里的Interface类其实就是一个中介者,它在各个页面之间协调工作,一个页面的某项功能实现(说白了就是实现一个方法)需要另一个页面的协助,那么这个页面就会去找Interface对象,Interface对象再去找需要的那一个页面。当然我们这个Interface对象只是简单的找寻页面地址,它没有去负责具体的功能性实现,所以它只是庞大但却不复杂。




【五】备忘录模式--对象行为型模式:
理解:
对于一个对象(原发器),我们想让它支持撤销功能,那么保证撤销操作能够做到精确无误的话,备忘录模式就是一个好的选择。备忘录模式在不破坏一个对象的封装性的前提下将这个对象的全部或部分状态保存在备忘录里面(也就是这个对象的内部状态被挪到了外部对象里面去保存)。试想一个对象(原发器),它的私有属性外面是拿不到的,我们如果实现的这个类只是自己去用这些状态,不想暴露到外面,那么连Get()这种接口都不会去提供,更别说提供Set()接口了。那么问题来了:外面怎么获得这些内部状态呢?
这个模式的精髓就是自己内部定义一个公共的接口供用户使用(用户向某对象提出请求保存一个备忘录来记录该对象当下的状态,就会调用这个接口),它用来创建一个备忘录。它依赖备忘录而不去关联它来实现它的方法体。当然关联也行,但是依赖关系比关联关系更加松散些,考虑到复用性会更好。在这个方法体内该对象实例化备忘录然后自己负责自己将有关时刻下的状态保存进备忘录(没有破坏封装性)。然后在某个时机,用户将先前请求得到的备忘录传回原发器对象,在原发器接口内部取出备忘录之前保存的状态赋值回原发器对象。这样就实现了原发器对象的恢复工作。


好处:
(1),简化原发器对象,一些信息需要原发器加工管理,但是我们不想把用户各个时刻下都想利用的信息保存进原发器而导致它变得臃肿,那么可以利用备忘录模式在不破坏原发器封装性的前提下满足某个时刻下有价值的数据的迁移工作。
(2),减轻原发器的存储负担,一些状态需要原发器的存在才能加以利用进行工作,把这些状态属性存储在外面交由用户自己管理,(原发器拥有的只是一个可能会不断变化的信息,我们保存了它某个时刻下的不变数据,这些数据正是我们此刻需要保留并且以后可能会用上的数据)。


弊端:
如果备忘录的内存开销很大,那么创建备忘录和恢复备忘录的操作过于频繁的话,会大大影响程序的运行性能。


注意事项:
宽接口与窄接口,宽接口供创建备忘录的原发器使用,原发器利用这些接口存取备忘录信息。窄接口供用户使用,用户可以利用窄接口来配置原发器。C++一般的实现上是将宽接口定义为备忘录的私有成员,原发器作为备忘录的友元就可以访问了,外部却不可以访问。窄接口定义成公有的,供用户来调用配置原发器对象用。


例子:
游戏里的存档机制就是用备忘录模式来实现的,不过这个备忘录不是一个内存块,而是一个磁盘文件(可以利用标准I/O库里的输入输出函数或者系统提供的read、write函数来实现文件的读写)。




【六】观察者模式--对象行为型模式:
理解:
定义对象的一对多的关系,当一个对象的状态发生了变化,所有依赖它的对象都得到通知并被自动更新。


好处:
(1),实现目标和观察者之间的抽象耦合,目标负责维护一个观察者的抽象父类引用的聚合,它并不清楚具体的观察者类型是什么,目标只是和一个抽象(观察者抽象父类)进行了耦合,这样目标和观察者之间的耦合是抽象的和最小的。
(2),目标可以向它的观察者们广播它的变化。被广播到的对象们可以选择响应这个变化或者忽略它。
(3),提供一种同步和保持对象彼此之间状态一致的良好机制。


弊端:
容易导致意外的更新,观察者彼此之间是不相识的,一个观察者改变了目标对象,导致其他相关的观察者被更新,而这种更新是否有用不可知。这个改变造成的直接后果是一些被强迫更新的观察者以及依赖这些观察者的对象会受到波及或影响。


注意事项:
(1),观察者可以观察多个目标,在观察者被更新时便于进行目标的区分,目标的通知方法里可以将自身的this指针传进观察者的更新方法里面。
(2),目标负责通知,优点是客户不需要记住在目标对象上调用通知方法。缺点是多次连续的操作导致多次连续的更新,可能效率较低。
(3),用户负责通知,可以实现多个操作处理完成之后,再进行一次性的通知来更新相应的观察者们,减少不必要的更新开销。缺点是用户需要记住在相应操作完成了后进行一次通知,这个流程是必须的,如果用户忘记了就会导致出错。
(4),可能观察者们更新的状态在当下与被观察的目标不一致。比如一个目标父类通知接口(Notify)里缺省实现了更新所有观察者的操作(Update),目标子类重写该方法(Notify),然后显示调用父类的缺省实现,再去做自己的事情,这么一来会有一个问题。调用父类缺省实现更新了所有的观察者们的状态,后面接着的操作搞不好立马把目标对象的状态改变了,这刚更新的状态就和目标不一致了,观察者们为什么总是慢半拍啊。当然我们可以每次在方法实现体的最后去调用父类缺省实现来进行广播通知,但是这么一来方法实现就与顺序相关了,熟悉这块儿功能的人还好维护,让别人来维护一个必须要有假设条件才能好好工作的代码显然是不可取也是不明智的做法。
(5),推拉模型,推模型是发出通知时目标将一部分信息打包发给观察者们,这么一来观察者们在实现自身的状态更新时就不必再向目标咨询具体的更新细则了,好处是效率高,观察者不需要维护目标的引用,减少耦合。坏处是目标对它的观察者们做了假设,这种假设不一定百分百正确,甚至某些观察者会忽视这些传来的数据参数。
(6),拉模型,目标不向观察者发送任何信息,观察者需要维护一个目标的引用,在观察者收到更新通知时,它会在更新的过程中向目标咨询相关的变化细则,好处是减少了多余的数据传参,坏处是效率会比较低,而且观察者们知道了它的具体目标,增加了耦合。
(7),显示的指定感兴趣的改变,实现方法是在目标的Add观察者接口处增加一个参数用于指定感兴趣的方面。以后在这个方面改变了的时候,只需通知对该方面感兴趣的观察者做更新操作。
(8),如果目标和观察者的交互逻辑比较复杂,可以考虑把这种复杂的对象之间的彼此交互关系封装成一个对象(中介者),它用来维护一个目标和观察者之间的映射表。这就不需要目标自己去维护它的观察者们的引用了,对目标上的添加,移除,通知操作,实际的实现是转交给这个中介者来执行了。比如调用目标的Add操作添加一个观察者,其内部的实现是调用中介者的注册方法,将要添加的观察者和发出调用请求的目标对象自身加入中介者维护的目标-观察者映射里面。这种实现方式的最大好处是因为多个目标和观察者被一个中介者所掌控,中介者就能够很清楚的知道怎么处理多个目标发生变化后怎么去更新的问题。以前的做法是目标发生了改变,会通知其观察者做出更新。如果现在是有很多个目标发生了变化,我们就要做很多次的更新。中介者(美其名曰更改管理器)可以在多个目标的变化结束了后再去更新相关的所有观察者们。减少了观察者反映其目标的状态变化所需的工作量。


例子:
组队副本里的控制玩家血条变化的机制就是观察者模式的一个变种,玩家对象甲被打后血条发生变化,客户端减血逻辑处理了这个变化并将血槽状态绘制表现给玩游戏的人,游戏相关逻辑同时向服务器发送请求,服务器将所在队伍里其他玩游戏的人的客户端里面与玩家对象甲的ID匹配的玩家对象的血条进行更新,其他玩家看到的效果和第一个人看到的血条状态效果也就保持一致了(这个例子可能不太准确,服务器我不懂)。




【七】策略模式--对象行为型模式:
理解:
定义一系列的算法,把它们封装成一个个的类,这些类之间彼此可以替换并且独立于使用它们的客户。


好处:
(1),因为具体算法和使用这个算法的对象进行了解耦,所以可以独立的复用算法类和客户类。
(2),继承也可以实现给一个对象添加新的行为,但是这种做法在编译时就决定了对象的行为,不够灵活,而且最后形成的一堆类,它们的唯一区别仅仅是它们使用了不同的算法和行为。而将这些算法单独封装成一个类就可以动态的去改变一个上下文对象的行为,它可以不依赖上下文对象随意变化,并且易于切换(改变上下文对象里算法指针变量引用的实例算法),易于理解(都是一个个单一的类,没有那么多多余的功能,我们能够很容易就从中筛选出有用的信息),易于扩展(添加新的算法对象即可)。
(3),消除了一些条件语句。


弊端:
增加了对象的数量。


注意事项:
(1),策略模式和装饰模式是两个极端的体现,装饰模式从外壳上改变一个对象的行为,而策略模式相反,它是从内核入手来改变一个对象的行为。
(2),策略模式和状态模式的类结构图是一样的,但是它们的目的是不同的,状态模式重在强调对象的行为会随着状态的切换而改变,而策略模式强调的是一个对象可以通过封装不同的算法来实现不同的功能。
(3),减少策略对象的一个可行办法是共享该策略,可以在策略对象中不去封装属性,只封装行为。策略需要的数据可以通过传参形式获取。这么一来就可以实现一个轻量级的对象便于各个上下文对象共享了。
(4),利用模板类,将具体策略类作为模板参数赋予上下文对象,可以避免给策略定义公共接口,将策略和上下文进行了静态绑定,从而提高效率,但是灵活性会受到影响。
(5),让策略对象成为可选的,上下文在执行相应策略时先检查自己有没有这个策略,有就调用,没有就实现一个缺省的行为,这么一来就不需要用户去了解策略并给上下文配置策略了。


例子:
游戏里的AI机制就可以用策略模式实现,比如CPlayer,CMonster作为客户类,CAI作为上下文对象,将游戏对象的移动,攻击等等封装成一个个的策略对象,我们可以在基类CShape里维护一份CAI上下文对象的引用,不同的子类客户对象可以根据自己的需求来给CAI上下文对象配置自己想要的策略。CShape的子类对象现在就可以使用一份CAI上下文的接口调用从而实现不同的运行表现,同类行为的实现也是可变更的。




【八】迭代器模式--对象行为型模式:
理解:
提供一种顺序遍历聚合里元素的机制。迭代器对象其实就是聚合对象想要的一种关于遍历其内部元素的功能性扩展。我现在定义了一个聚合类,它支持元素添加,元素移除等等,聚合目前的关注点只是一个个元素本身,缺乏统筹管理元素们关系的方法。现在类的设计者想要实现元素的遍历、排序、查找,前提是元素彼此之间要有一种序列关系方便这么去操作。最容易想到的就是给这个聚合类继续添加有关元素遍历方面的方法,但是这么做的后果是关注单一元素的方法和关注元素之间关系的方法就都混杂在一个类里面了。以后我要是想在这个元素集合(此聚合类)上运用其他的遍历,查找等操作,已有的方法不符合我这个需求,是不是又得向这个聚合类继续添加其他遍历操作方面的方法啊。为了让这个聚合类不至于因为用户的种种要求而变得臃肿不堪,软件设计的先驱者们想到了将它的有关遍历等方面的操作提取出来单独封装成一个类。


好处:
(1),聚合对象可以支持多种类型的遍历操作却不会变得臃肿,在同一时刻能够轻松的维系多个遍历状态。
(2),结合工厂方法模式可以实现多态迭代,所有聚合类实现一个公共接口,所有迭代器实现一个公共接口,两个接口(抽象)进行耦合。这么一来就能够让一个聚合类动态更改它的迭代器了,反之亦然。


弊端:
如果迭代器不够健壮,在遍历的同时聚合对象进行了插入删除操作,很容易导致迭代出错。


注意事项:
(1),如果将遍历算法交给聚合类实现,迭代器只负责记录当前的迭代状态,那么这就是一个游标迭代器。
(2),外部迭代器,我们使用的std::vector,std::list,std::map的迭代器就是一个外部迭代器,它真正的循环迭代是在我们在客户代码里写的for循环的驱动下进行的。
(3),内部迭代器,因为lua语言支持匿名函数,闭包,所以它实现内部迭代器很方便。在C++中是通过一个类将外部迭代器封装进去,这个包装类提供原先客户代码提供的迭代驱动功能。这么一来我们就可以在这个包装类里添加自己觉得有趣的功能了(迭代一个元素时的限制条件等等),当然这个功能要通用,以后我们就可以把这个内部迭代器到处复用而不用在客户代码里重新写那些迭代驱动和限制条件等等的代码了。
(4),注意到没,多态迭代利用工厂方法模式建立抽象依赖而不是实现依赖。我们想要这个迭代器生命周期跨越出创造它的方法体,这个迭代器对象必然要在堆里面new出来。通过指针获取迭代器对象后,我们在客户代码里完成了自己的有关迭代等等相关的工作,还要记得delete它,这个是不是很烦。如果客户代码里忘记了delete它,就会造成内存泄露。现在是代理模式出场的时候了,代理对象作为迭代器的代理,可以在其构造器里面接受工厂new出来的迭代器对象,然后在客户代码里完成相关的迭代工作后,代理在它的析构器里面delete这个迭代器,这么一来客户就不必关心迭代器的删除工作了。当然这个代理对象要在栈上分配内存。


例子:
你把int变量的类型想象成一个自定义类类型,++操作想想成这个类类型上重载的操作运算符,那么这个int变量本身就是一个迭代器。类类型聚合对象的迭代器迭代的是它里面的元素,int变量迭代器迭代的是根据它的值关联到的某个实体,比如数组下标为n的元素。




【九】适配器模式--类、对象结构型模式
理解:
原先已经存在了一个类,现在你想复用它的功能,但是在你的系统框架下,这个类的接口与你的不兼容,那么可以定义一个新的类,这个新类的接口用来匹配你的系统,实际的实现是调用被适配的类,那么这个新定义的类就是一个适配器类。


好处:
(1),类结构型的适配器可以在只生成一个对象的前提下来适配客户可以用的接口与实际想要的功能。你可以在适配器中重写这个被适配的类方法,因为适配器类是被适配器类的子类。
(2),对象结构型适配器支持动态适配一个类层次结构中的所有类对象,这个类层次提供一个公有接口,适配器只需要和被适配对象的抽象进行耦合,它并不清楚具体所适配的实例对象类型是什么。


弊端:
(1),类结构型的适配器不够灵活,如果要适配很多个对象就要产生很多的子类,显然这种做法是不可取的。
(2),对象结构型适配器无法重定义被适配对象的行为。


注意事项:
(1),这个模式分为两种形式,一种是类结构型模式,一种是对象结构型模式。在C++里,前者的实现是使用多重继承,接口继承并实现继承目标对象,实现继承被适配的对象。这两种继承方式的区别是什么呢?我的总结里面说了,接口就是方法声明,你只要知道这个方法是干什么的就行了,你却不知道它是怎么干的,所以它的实现细节你不清楚,不知道,它就是一个抽象,这里叫它接口。我们把目标对象公有继承了,继承来的原本的公有接口外面可以调用,所以就叫它接口继承(这里的接口继承严格上说指的是公有接口继承)。而实现继承呢,实现就是方法体,我现在在子类里面私有继承了一个被适配的对象,它的方法在我这里全都变成私有的了,外部用不了,但是我可以用,我既然能用它的实现来满足我的实现,它的实现是不是被我继承了啊。但是呢,被适配类的原先的方法声明在我这里都是私有的,外部拿不到,所以这些原先意义上的公有接口在我这里都变成私有接口了,当然对外面是不可见的。所以对被适配对象的继承就只包含实现继承而不包含接口继承了。(一般情况下接口继承用公有继承来办到,实现继承用私有继承来办到)。
(2),纯虚函数的公有继承是对外部、子类、自己的接口继承,虚函数和非虚函数的公有继承是对外部、子类、自己的接口继承和实现继承。
(3),纯虚函数的保护继承是对子类、自己的接口继承,虚函数和非虚函数的保护继承是对子类、自己的接口继承和实现继承。
(4),纯虚函数的私有继承是对自己的接口继承,虚函数和非虚函数的私有继承是对自己的接口继承和实现继承。
(5),虚函数和非虚函数的区别就是虚函数可以被覆盖(重写),以实现父类指针指向子类对象或者父类引用引用子类对象产生多态。你实现一个在子类里面和父类同名的非虚函数,这个不能说是覆盖,只是隐藏而已。前面说的条件它们办不到,那么做的话父类其实还是会去调用父类里面的方法的,子类写的同名方法它不会去调用。虚函数实现机制是在每个对象首地址的前四个字节(64位系统是八个字节保存一个虚函数表的地址),具体细节见C++对象内存模型。
(6),这里要脑补一下覆盖(重写),重载,隐藏这几个概念,它们在本质上是不同的,所以以后请不要再张冠李戴了。
(7),覆盖(重写):就是子类虚方法重写父类的虚方法,如果父类的对应方法注明了virtual,那么子类要进行覆盖它的方法可以不写virtual关键字。相同点:函数名,参数类型和个数要求一致。不同点:一个在父类,一个在子类,作用域(空间)和类型不同。(类其实涵盖了两个概念:类型和作用域,不同类的对象拥有不同的类型和不同的作用域,一个类的方法的作用域是这个类,C语言里面的函数作用域当然就是全局的了)。
(8),重载:严重声明它和重写不是一回事,上面说了,重写就是覆盖,重载是对于一个全局函数或者类方法,要求它们彼此的名字必须相同,但是参数的个数或类型必须不同,返回值忽略。这里要说明的是无论重载的是全局函数还是类方法,这些个玩意儿的作用域都是相同的,重载的全局函数彼此之间作用域都处于全局之下。而重载的类方法彼此之间的作用域都处于当前这个类定义的作用域下。不在同一个作用域下的同名不同参的函数,请不要叫它重载好吗,它们不属于重载。
(9),隐藏:对于同名的变量(或属性),小范围的会隐藏大范围的。对于同名的方法(或者全局函数,这里的同名指的是函数名和参数类型、个数都相同,要和重载区分。),相同作用域下如果是内部(static)全局函数在不同的编译单元下,没问题,在同一个编译单元下会报错重名。外部(缺省extern)的全局函数在同一个编译单元下,编译不通过,提示重名,不同编译单元下编译通过,你把编译器欺骗了但是你却骗不了链接器,链接出错。obj文件的符号重定位表搞不清要定位哪个到exe文件中的相应地址处。对于类方法,相同类(即相同作用域下),你定义两个重名的方法肯定编译报错重名问题,你在子类里或者其他不相关的类里面定义和这个类重名的方法,因为作用域不同所以编译通过,链接通过,运行OK。但是有一点就是你在子类方法中调用一个方法void fun(void),它在这个子类和父类里面都有定义,但是不是虚的,所以默认调用子类的,它隐藏了父类作用域下的该方法,要想调用父类的同名方法就必须在方法名前面加上父类的名称和双冒号。这时候你如果用父类指针指向子类对象或者父类引用引用子类对象的话,其实程序会去调用父类的同名方法而不去调用子类的这个同名方法,没有多态了啊。


例子:
有一个已经实现了的矩形类,它的坐标系统是基于左下角的坐标、宽和高的,而你自己的系统里使用的矩形要求是通过左下角和右上角来确定一个矩形的,如果你不想重新实现一个矩形类而是复用现有的,那么你可以提供一个适配器对象对两者进行适配。适配器接口和自己系统下要求的矩形传参模式一致,实际的功能实现是调用被适配对象的操作进行转换而得来的。




【十】桥接模式--对象结构型模式
理解:
通过将一个对象的抽象(接口)部分和实现部分分离,达到抽象部分和实现部分可以独立变化的目的。抽象类层次给用户提供不同的服务,实现类层次实现具体的不同的服务,因为这两个类层次是抽象耦合的,所以它们可以根据用户的需求随意组合。


好处:
(1),实现细节对客户透明,可以完全隐藏实现部分,包括实现部分的类定义。用户只需和抽象部分进行交互,具体的实现细节甚至连实现的接口用户都是看不到的。
(2),易扩展,抽象部分和实现部分可以独立变化与扩充。
(3),降低编译依赖性,实现部分如果只是实现变了,只需要重新编译实现部分,不需要编译客户代码和抽象部分,而只需在实现部分编译完成后将它们进行链接即可。实现部分如果是实现的接口也变化了,那么只需编译实现部分以及调用实现部分的抽象部分即可,无需编译客户代码。所以不管你怎么搞,我客户代码总是不变和稳定的。


弊端:
违背了开源社区的意志。


注意事项:
(1),可以利用引用计数对实现部分的单个对象进行共享,多个抽象部分的对象都可以调用这个实现部分的对象来实现自己的功能。
(2),可以结合抽象工厂模式来将抽象部分和实现部分进一步解耦,可以使用此方法来实现跨平台。
(3),适配器模式关注的是系统设计完成后要怎么利用有价值的类和自己的类协同工作,这个只是对接口进行了适配,实现没有本质上的变化。桥接模式是在设计的刚开始进行的,它促成了抽象接口和实现部分能够进行独立的变化与组合。


例子:
我们在编码中要做的工作其实就是封装变化。把一个变化单独封装成一个类来看待,我们就能很清楚的掌控和研究这个变化的具体细节了。比如桥接模式将系统的接口和实现进行了分离,现在要做一个音乐播放器软件,最初的设计是我们将音乐播放器软件的接口留待客户来使用,实现方面直接是调用基于win32平台的底层功能代码。现在这个软件想移植到IOS系统或者安卓系统上,那么我们不得不去更改所有的实现,重新调用新平台的底层功能,完了还要对应新的实现来更改客户代码以保证我们的音乐播放器软件能在其他平台上也正常工作,这个和重做这个软件的工作量也就无异了。一开始如果在设计上我们将接口和实现两个部分做分离,客户代码耦合系统接口,系统接口耦合实现类层次的接口,这么一来如果实现要变化,我们只需替换到相应的实现上面而不用去更改原有的代码。这种因为需求变化而导致的代码修改也就可以避免了,现在只是扩展新功能的时候了。




【十一】组合模式--对象结构型模式
理解:
将对象组合成树形结构以表示部分-整体的层次结构,组合模式可以使得用户对单个对象和组合对象的使用具有一致性。


好处:
(1),简化了客户代码,因为客户是通过一个共有的接口来使用单个对象和组合对象的,所以无论客户引用的是单个对象还是组合对象,都不必再用条件判断语句作区分了。
(2),容易增加新类型的组件,新组件可以直接实现公共接口即可,它很容易被添加到现有的类层次结构中而且客户代码不需要做任何调整就能使用这个新成员。使得设计更加一般化。


弊端:
(1),用户要一致看待组合和叶子,势必要求这两者的共有接口要足够强大到支撑他们的实现,而面向对象的类层次结构的设计原则要求:一个类只能定义那些对它的子类有意义的操作。
(2),因为用户可以一致的看待单个对象和组合对象,而且组合对象可以递归的再产生新的组合对象,所以如果现在你的设计里面想要的只是添加特定类型的组件,你就无法在编译时期利用类型系统来检测现在的组合对象是否满足你的要求了。你只能在运行时刻去做判定来检查,这势必会造成每次要用一个组合对象的时候都要if一次才放心。


注意事项:
(1),基于安全性的考虑,叶子节点被视为没有子部件的节点,组合部件被视为有子节点的父节点,那么你如果在它们共有的接口中去定义只有对组合组件才有意义的操作(例如AddChild,RemoveChild),用户用这种操作对叶子节点进行访问,势必会造成麻烦,所以只把这类接口引入到组合部件的父类里面会更安全些。
(2),基于透明性的考虑,如果按照上面的办法去做,就会引入另一个问题,我确实不用担心用户对叶子节点进行非法操作了,但是用户在用叶子节点和组合节点时就要区分看待了,我拿到一个对象,在运行时刻还要判断下它是组合还是叶子,这就很麻烦了。
(3),组合模式侧重的就是透明性,让叶子和组合实现一份相同的接口,用户可以不加区分的使用叶子和组合。这份相同的接口里面实现对组合对象特有操作的缺省实现,而在组合对象里面覆盖这个缺省实现来进行实际的对子部件的操作(不如添加移除子部件)。这里有一个问题就是如果在用户使用叶子的时候去给他传递一个参数进去(给叶子加子部件),那么这个缺省实现可能会忽略或者删除这个传进来的数据对象,显然用户的初衷并不想要这么做。
(4),最好在缺省实现里面处理add和remove的失败(可能是产生一个异常)。remove有一个解决办法是在父部件里面删除相应的叶子,就不会因为对叶子进行remove它的子部件操作而出错了。add没有这种对应的解决办法。
(5),显示的父部件引用可以被巧妙的应用到职责链模式之中。迭代器模式可以用来对组合对象的子部件进行遍历和排序等操作。


例子:
游戏里面的CGameObject及其子类就应用了组合模式,不过这里面的对象都是组合对象,没有严格意义上的叶子对象。因为你把孩子聚合定义到了CGameObject里面,所以这就默许了所有CGameObject对象或其子类对象都可以去递归组合成新的对象。你可以把没有孩子的CGameObject对象或其子类对象当做叶子节点来看待。




【十二】装饰模式--对象结构型模式
理解:
如果你想在不用类继承方式的情况下给一个对象添加更多的职责、行为,那么装饰模式就是很好的选择。装饰模式保证从外表入手来改变一个对象的行为。它要求装饰对象和被装饰对象实现同一个接口以保证装饰对象的存在对客户透明化,客户代码原先在使用一个组件对象,你用装饰对象对这个组件对象进行包装装饰之后,然后拿来给客户用,客户并不会觉察到它的存在,所以在将组件对象动态替换成装饰对象的时候,并不需要更改客户代码的实现。


好处:
(1),支持递归装饰,装饰对象先是包装了组件对象,再用另一个装饰对象来包装前者这个整体对象。类继承的方式实现添加职责会导致随着职责数量的增多与多异性,子类数量会呈现爆炸性的增长趋势。而装饰模式避免了这一点。
(2),可以从简入繁来解决一个问题,你在类层次结构的最顶层定义最简单的接口,实现简单的类。随着职责的增多和多异性的需求,你可以一步步去包装这些类对象从而形成复杂的类对象。这么一来用户想用简单的就用简单的,想用复杂的就用复杂的。如果在设计之初就给用户提供一个涵盖了很多功能的类来使用,往往一些功能是某个特定用户不想要的,你打包给用户了是不是会让用户眼花缭乱起来,一个工具类的接口一定要简单别人才好用,你提供一大堆功能、接口给用户来使用,用户自己都晕了。


弊端:
上面也说了,装饰模式避免了类的数量增多的问题,但是它却引入了对象数量变多的问题。你想想,在你的系统里到处都是一些仅仅是因为某些行为有差异的对象充斥着,是不是很影响空间效率啊。


注意事项:
(1),改变对象外壳与改变对象内核。装饰模式用来改变对象的外部行为,它只是从外面动态的来给一个对象添加新的职责,对象自身的行为并没有变化,类似于锦上添花。策略模式是从内部改变一个对象的行为,这个对象可以通过选用不同的策略来响应客户发出的相同的请求。
(2),装饰模式要求被装饰对象的类足够简单,以便于自己进行递归装饰以后不会形成太过复杂的对象。而策略模式没有这个限制,原有对象可以很复杂,我只需要替换相应的内部策略即可,不会导致这个类变得更加复杂。
(3),装饰模式其实就是退化的组合模式,组合模式里的组合对象可以拥有很多个子部件对象,而装饰模式里的组合对象(即装饰对象)只引用一个子部件对象(即被装饰的对象,他可以是元对象,也可以是装饰对象,就是递归装饰啦),不过装饰模式在问题域是动态的给一个被装饰的对象添加额外的职责,而组合模式的问题域是对象的聚集。
(4),装饰模式是改变一个对象的职责(行为),它要求接口对客户透明,所以装饰对象和被装饰对象在设计之初要求接口统一。而适配器模式是给一个要适配的对象赋予一个全新的接口,它不改变被适配对象的职责(行为),这种过程往往是发生在设计结束以后,我们想要复用一个已有功能时所作出的裁决。
(5),改变对象的两种途径,装饰模式从外壳着手,策略模式从内核开刀。


例子:
Cocos2d-x里面的Action子系统就是用装饰模式实现的。




【十三】外观模式--对象结构型模式
理解:
对于一个现有的复杂的系统,这个系统设计的时候,为了通用性,里面添加了很多的类,提供了很多客户可以使用的功能。然而这么做势必增加了学习这个系统的难度。为了易用性,设计者给这个子系统添加一个外观类,该外观类对子系统的接口进行了抽象并且封装好供外部使用。


好处:
(1),为一个复杂的子系统提供一组简单易用的接口来调用,减少了客户处理的对象的数目并且使得子系统使用起来更加方便。
(2),降低客户-子系统之间的耦合,客户现在是通过外观对象来向子系统发请求的,而不是直接与子系统交互。
(3),外观对象不会限制客户直接去使用子系统,所以我们可以在系统的易用性和通用性之间加以选择。
(4),方便客户在子系统的具体实现之间进行切换,提供一个抽象外观的引用交由客户维护,不同的子类外观抽象不同的子系统实现,那么我们就可以根据需求的变化来实现动态的改变一个具体的子系统实现。
(5),降低编译依赖性,子系统实现变化了,只需要重编外观类,只要外观类的接口不变化,客户代码就不会受到任何影响。(客户代码不需要重编,它只需要和子系统重新进行链接即可)。


弊端:
将一个子系统托付给了外观,职责过重。


注意事项:
(1),子系统并不知道外观对象的存在。
(2),公共子系统类和私有子系统类,类和子系统的相似之处是,它们都有接口并且都封装了一些东西。类封装了一个对象的属性和行为,而子系统封装了一些类。类的公有、保护和私有接口的定义方式,在C++里面用类的访问权限限定符关键字可以实现。对于子系统的公有接口,默认你定义一个属于子系统下的类,他就是公有的,那么怎么去实现子系统的私有接口呢,就是怎样去实现一些私有的类(外部无法使用)呢,C++提供了命名空间的机制,你可以借此来实现子系统的私有接口。
(3),前面提到过,外观模式和中介者模式都是用来协调类集合的关系的,但是外观模式是单向通信的,而中介者模式是双向通信的。


例子:
Cocos2d-x里面的SimpleAudioEngine就是一个外观类,它对各个平台下的音频子系统进行抽象。它的设计架构是提供一个外观抽象类,然后各个平台对这个接口进行不同的实现(利用抽象工厂模式,它封装了与平台相关的细节,不同平台对应下的工厂子类对象用于创建不同的具体外观对象,这样就实现了跨平台的需求了),每个平台下对应的外观实现与对应平台下的音频子系统进行交互。




【十四】享元模式--对象结构型模式
理解:
利用共享对象的方式来减少程序运行期间对象太多的问题。


好处:
(1),实现享元对象时,需要区分出要被共享对象的内部状态(它不依赖对象所处的上下文)和外部状态(依赖对象所处的上下文),内部状态就是要共享的部分(通过对象共享的机制来减少内部状态的重复次数),而外部状态是这个被共享对象所处的上下文下的差异性表现。举个例子吧,现在要做一个文档编辑软件,那么你想要这个文档编辑软件的功能足够强大,强大到用户对每一个字符都可以进行操作,那么文档中的每一个字符都要被视为对象才能满足这个需求。想想看,一个文档下的一页如果密密麻麻的布满了字符,那么你得创建多少个文字对象啊。而且如果你想在翻页时不想让这个文档软件卡顿,就要一下子将所有文字对象创建缓存好,这个势必导致程序里对象暴多,很浪费内存,而如果你想节约内存,只在翻页的时候创建下一页的所有文字对象,那么就会造成文档编辑软件变卡,两种做法都极不可取。
想想看,文档页面下的字符或文字有很多是重复的,我们把它们单独提取出来视为一个对象,这个字符本身就是内部状态,用类将其封装后这个内部状态也就成了对象的属性了。文档中的每个文字的颜色,字体,位置信息等可能各有差异,它们不能被共享,我们把这些信息视为外部状态。那么在文档编辑软件运行期间,映入我们眼帘的密密麻麻的一页文字,其实并没有创建太多的对象,每个相同的字符或文字其实都是一个对象,至于它们所处文档页中的位置不同,字体颜色等等的差异,就是通过其他对象以传参的形式将信息传进文字对象的。
(2),接着第一条说明,那么怎么实现共享产生的空间节约效率最大化呢,如果对象外部状态的种类越少,而内部状态越多,且这个对象被共享的越多,那么就越节约内存。如果对象的外部状态几乎和对象被共享前的个数相当,而内部状态少到只剩下行为共享了,我想说的是你这能叫享元对象吗。外部状态那么多,对象在没有共享期间它的存储位置位于对象内部,现在你要共享这个对象就把这些状态放到了外面存储还美其名我这是一个享元对象,其实这只是给信息做了一次搬家,不管你怎么搬家这数据都是存在内存里面的,这当然是起不到半点儿节约内存的目的了。所以说享元模式的使用是有条件的,只有你在能确定哪些数据是内部状态可以共享,它不依赖于上下文,哪些是外部状态,它的消耗要尽量小,最好是可以把这些外部状态能够单独封装成一个类来管理、存储和计算,那么这时候才适合使用享元模式。


弊端:
(1),享元对象对外部状态和内部状态进行了区分,所以在某种程度上导致系统变的复杂了起来。
(2),因为享元对象要从外面获取外部状态的信息,所以对时间效率有一定的影响。


注意事项:
(1),外部状态可以通过其他对象存储或者计算得来。用其他对象来存储外部状态会有一定的内存开销,当然上面说过了,外部状态的种类如果很少的话,其实共享比不共享所产生的空间效益还是大很多的。如果这些外部状态计算得来,那么就是以时间换空间了,这种做法会将享元模式节省空间开销的收益进一步最大化。
(2),由谁来管理共享对象是一个问题,比较好的做法是将共享对象交由一个工厂类的对象来管理,用户向工厂对象请求共享对象,如果工厂里面有就直接返回,没有满足用户要求的共享对象就创建然后返回给用户(因为牵扯到共享规则的问题,何时共享,怎么共享,所以让用户自己负责共享对象的创建显然是不合理的)。
(3),状态对象和策略对象可以用享元模式来实现。
(4),共享就要有某种形式的引用计数和垃圾回收处理能力的协助,如果共享对象数量固定而且很小的时候,这两种操作就不必要了,可以完全永久保存。


例子:
粒子系统里的对象集是不是用享元模式实现的啊?!




【十五】代理模式--对象结构型模式
理解:
对一个对象的访问进行可控性的自定义操作,而这个自定义操作就是我们想要抽象出来的类,这个类就是代理类,而此类的实例就是代理对象。


好处;
(1),使得对目标对象的访问可控。
(2),给用户提供了一种写时拷贝(copy-on-write)的优化方式,比如现在有一个请求要拷贝一个实体对象,利用代理对象在每次收到拷贝请求时将拷贝引用计数加一,等到代理对象收到了这个被拷贝对象发生变化的消息后,代理就将这个对象拷贝给所有发过请求的对象,然后将引用计数减一。直到引用计数为零,代理就会负责将这个被拷贝实例对象销毁。这样做的好处就是,假若此实例对象的内存开销很大,那么只在它发生变化时才进行有意义的拷贝,从而优化了程序的执行性能。


弊端:
某些代理会造成客户对目标的请求速度变慢。


注意事项:
(1),远程代理,不包含对实体的直接引用,而只是一个间接引用,例如主机ID。
(2),虚代理,开始的时候使用一个间接引用,例如一个文件名,但是最终将会获取一个直接引用,类似于lazy-init。
(3),保护代理,在用户对目标对象发请求的时候,请求先传递给代理,再由代理转发这个请求给目标对象。在代理转发请求之前可以做一些附加操作,比如检查请求的合法性等等,或者直接忽略该请求。这种代理类似于装饰模式里面的装饰对象,不过二者的目的不同,前者是对目标对象的访问权限做限制,后者是给被装饰对象增加额外的职责。
(4),智能指针,用于取代一个简单的指针,以便在通过这个指针访问它所指的目标对象时进行附加操作,智能指针类型的代理可以认为是一种抽象化的简单指针(技巧:在C++中可以通过重载运算符*和->来实现与使用简单指针同样的写法)。
(5),如果代理和目标对象实现同一份接口,那么就可以保证在客户代码里使用目标对象的地方能够全部动态的替换成相应的代理对象,客户对此变化不会有所察觉。


例子:
打开一个网页的时候图片的显示总是滞后于文字显示,在图片显示前会有一些提示信息的显示,这个处理方式的实现就是利用了代理模式里面的虚代理。




【十六】抽象工厂模式--对象创建型模式
理解:
举一个例子,假设一个UI系统拥有简约风格的A、魔幻风格的B、科幻风格的C一共三种类型的产品集,而每类产品集对应有两个元素(按钮和输入框),那么我们就可以在抽象工厂接口里面声明两个操作,然后实现三个工厂子类,第一个工厂子类对象负责创建简约风格的按钮和输入框对象,第二个工厂子类对象负责创建魔幻风格的按钮和输入框对象,第三个工厂子类对象负责创建科幻风格的按钮和输入框对象。那么我们想要在运行时动态的切换三种风格的UI控件就很随意了(方便在不同的产品系列之间进行选择和切换)。


好处:
将产品对象的创建与客户进行分离,因为现在对象创建的逻辑被抽象工厂进行了封装,对象实例创建过程抽象化,所以客户就可以根据配置自由的切换想要类型的产品集(其实对象创建型模式都是这个思想)。


弊端:
因为抽象工厂的顶层父类(或者接口)要事先涵盖所有的产品种类,每个产品种类就要对应一个方法,那么想要添加新的产品对象就很困难。你必须要在抽象工厂父类及其所有子类里面都得添加一个新的方法来实现对该新类型对象创建工作的支持。所以当一个类型产品集下的产品种类确定下来的情况下,而且之后基本不会有改动,那么用抽象工厂模式才会更加合理。


注意事项:
(1),有多少个产品集,就有多少个继承自抽象工厂模式的抽象父类的子类定义。
(2),有多少种类型的具体产品(比如按钮、输入框、列表盒等等),就得在抽象父类里面声明多少个方法。


例子:
一个支持多种视感标准的用户界面工具包。




【十七】生成器模式--对象创建型模式
理解:
考虑现在要构造一台计算机,计算机对象是由许多零部件组合而成,上一节说的抽象工厂模式如果放到现在这个问题域上面,它可以帮助我们生成不同计算机品牌下的主板、鼠标、显示器等等。用户可以通过不同的工厂子类对象获取不同计算机品牌的零部件集合,但是用户可不想要这么一大堆零部件啊,他想要的是一台完整的计算机。
现在就是生成器模式出场的时候了,你想让用户自己学会组装计算机吗?当然不是了,所以生成器模式给我们提供了一个导向器,导向器负责各个零部件的组装过程,它封装了组装一台计算机的步骤,而组装过程所需的零部件由建造器(一个工厂对象)提供。如果你想用导向器以相同的流水线作业组装不同品牌的计算机,那么就定义一个工厂类的接口,不同的工厂子类负责生产不同品牌的零部件,将这个工厂子类的实例对象赋给导向器,导向器在其内部进行零部件的组装过程,最后组装好一个成型的产品(我想要的品牌类型的电脑)后,将这个产品交付给客户使用。


好处:
(1),以同样的构建方式(导向器封装了此规则),可以创造出不同的表示(通过给导向器配置不同种类的生成器对象,会构造出不同表现形式的产品)。
(2),导向器为用户提供了一种便利,这种便利是用户不必关心一个对象的具体构建方式,用户只需要调用导向器提供给它的接口就可以得到自己想要的产品,将具体的构建过程抽象化。
(3),生成器(即工厂对象)将一个对象的各各组成部分的创建抽象化,它使得我们可以通过配置不同的生成器子类对象生产出不同种类的产品零部件对象。当然你可以把零部件的构造集成到聚合对象的内部,但是将聚合对象变的小一些是不是更容易理解和修改呢。重要的一点是你可以通过提供不同的构造器来生成各式各样的产品。


弊端:
生成的最终产品可能很复杂,难以理解与维护。


注意事项:
(1),一般在构造器父类中定义创建一个零部件的方法的缺省实现,比如一个空的实现。由具体的子类来重写它们感兴趣的方法。
(2),生成器模式一般用来生成聚合对象,可与组合模式一同使用。


例子:
针对游戏里的玩家或怪物模型,每次我们想绘制一个具备不同表现形式的玩家或怪物模型的时候,都需要ctrl+c、ctrl+v原来的代码,然后把它的武器换一个新的,翅膀换一个新的等等。其实可以这么做,因为一个玩家模型,它身上穿戴的东西也就那么多,有多少穿戴物,你就在构造器接口里定义多少个方法,当然玩家模型本身和武器、翅膀这些要同等对待。不同的构造器子类对象负责创建不同种类的武器模型对象、翅膀模型对象、玩家本身模型对象等等。它们的构造方式都是一样的,翅膀肯定在背部,武器肯定在手上,所以这个装配组件的规则很简单,可以把这个装配规则抽象出一个导向器类。在以后的客户代码中,如果我们想要一个手持大宝剑,背扛天使翅膀的德鲁伊模型的话,只需要给这个导向器配置一个具备创建大宝剑模型对象方法,创建天使翅膀模型对象方法和创建德鲁伊模型对象方法的构造器即可。这里有一个问题,我想支持各种搭配方式的话,是不是得要创建很多种构造器啊,还好我们的这个例子很简单,这些零部件有一个共同点,它们都是模型对象,并不像电脑部件那样主板和显示器的差异性会那么大。所以你只需要一个构造器,甚至连构造器的接口都不必提供了。想要什么类型的翅膀模型等等这些信息,可以通过用户自己进行配置,比如方法调用的时候将模型ID和模型分组信息传递进构造器对象里。那么构造器就可以生成不同表现形式的模型,然后用户将这个初始化好了的构造器交给导向器,导向器负责进行各个模型的组装,完事后我们将这个成品返回给用户,用户就可以使用它了。将这个组装好的模型绘制相关的对象以组合或聚合方式关联进我们的CPlayer类,那么如果我这个CPlayer对象想隐身(不可见),原先的做法是用一个布尔变量来控制,它有一个很明显的缺点就是不管你绘不绘我,我这绘制相关的数据都会占着茅坑不拉屎。而且这些绘制数据隶属于不同的玩家,无法共享。现在的做法是我把绘制玩家模型(包括身上穿戴的模型)的数据对象通过关联的方式引进,一开始并没有关联,等到要绘制了就关联进来(lazy-init),而且如果模型绘制的外部状态可以通过传递参数来告诉模型对象,那么这些与各个CPalyer对象、CMonster对象绘制相关的模型对象也可以实现共享,这么一来就节约了不少内存上的开销。还有,你这CPlayer、CMonster想分身了,就多关联几个进去,那么我的玩家就具备了三头六臂的能力了。现在的特效类貌似是这么做的。
一个CPlayer对象包含那么多东西进去的做法确实不妥,所以还是建议把它的各个功能单独进行抽象归类,再以组合或聚合的方式构建起来会好些,这样做的好处是CPlayer的各个功能模块方便复用,比如上面说的绘制模型代码。结构清晰一目了然,让别人维护起来也就轻松许多了。




【十八】单例模式--对象创建型模式
理解:
保证一个类只可以被实例化一个对象。


好处:
(1),对唯一的实例可以实现受控访问。
(2),是对全局变量的改进,而且避免了全局变量污染全局命名空间的问题。


弊端:
它的对象生存期是个问题。


注意事项:
(1),懒汉式单例,第一次调用时进行初始化,避免内存浪费,多线程下必须加锁才能保证单例,但是加锁会影响效率。
(2),饿汉式单例,多线程下可以不用加锁,执行效率高,但是在类加载时就创建好了,浪费内存。
(3),将创建单例与获取单例做分离,就可以实现单例对象集的统一创建和销毁工作了,方便单例的管理。
(4),实现一个单例注册类,所有单例类继承该类,然后在构造这些单例对象的时候在其构造器内调用父类的注册单例接口,单例注册类维护一个关联数组(此单例注册类的静态成员,所有单例类型子类对象公有),单例在创建并初始化的时候将自己注册进这个关联数组,可以用一个std::map来维护这些被注册进的单例对象,以一个枚举类型或字符串类型索引其单例。然后用一个创建单例的工厂对象来统一实例化不同类型的单例类。后面想要获取系统下的所有单例对象就很方便了。(通过这种方式获取某个单例时可能需要dynamic_cast,避免此种做法的途径有两个,一是在单例注册类里定义足够多的虚函数让子类来覆盖,二是利用模板)。


例子:
ga::ui::Manager类的实例就是一个单例。




【十九】原型模式--对象创建型模式
理解:
实现一种机制,给定一个原型对象,客户可以通过这个原型对象提供的克隆接口来进行对自身的拷贝。


好处:
用户可以很方便的就能获取一份自己想要对象的副本而不用关心这个克隆对象是怎么生成的。


弊端:
因为在C++中实际的拷贝工作是由拷贝构造器来完成的,所以拷贝构造器的算法实现必须得正确。尤其是在一个类的继承层次结构中对底层子类对象的拷贝。


注意事项:
(1),在C++中的实现是这样的,如果客户想要一个对象的拷贝副本,那么你在一个全局函数或者另一个对象的方法内部调用你想要的对象的拷贝构造函数来生成这个副本显然是不合适的,你可以把想拷贝的原型对象的地址的解引用传给该对象的拷贝构造器,但是这种拷贝原型对象的职责就乱了,我客户(刚才说的全局函数和其他对象)还要负责来拷贝原型对象,这样做不可取。
(2),现在可以在原型对象的成员方法内部去调用自身的拷贝构造器,然后将自身的this指针的解引用传进拷贝构造器,那么这个原型对象就可以自己负责拷贝自己了,现在你想把原型对象的拷贝自身的功能单独封装成一个方法以便于拷贝自身的功能能够拿来复用,那么你就可以把调用自身构造器并传递this指针的解引用的操作独立成一个成员方法,一般给这个方法命名为Clone(),那么就形成了一个C++实现版本的原型模式了。
(3),拷贝有深拷贝和浅拷贝的问题,浅拷贝是原型对象和克隆对象共享原型对象里的成员指针变量所引用的实例对象,深拷贝是原型对象和克隆对象不共享原型对象里的成员指针变量所引用的实例对象,它们各自都有一份。
(4),当一个系统的原型数量不固定的时候(可以动态的销毁和创建),那么可以实现一个原型管理器给客户直接提供服务,客户只需要在管理器中存储,检索原型,它不用知道所有的原型本身,而只需知道存储在原型管理器中的关联数组里所有原型对象的关键字即可。


例子:
Cocos2d-x里面的原型模式是这么实现的,它的Clone()接口通过继承自一个公共基类获取,实际的拷贝工作并不是利用拷贝构造器完成,而是默认构造器配合初始化函数来完成实际的拷贝工作,它的拷贝原型可以由用户自己进行配置(即用户自己来选择自己想要拷贝的原型对象),也可以实现一个正在使用的对象对自己的自身拷贝。
拿CCAction子系统来举例子,我调用原型对象的成员方法copyWithZone()并传递一个空指针进去,根据多态,不管是父类指针指向子类对象还是正常情况下,这个copyWithZone()都会被正确调用,在其内部,先是调用默认构造器new一个我要拷贝对象类型的实例,现在只是有了内存块,但是原型和克隆体的数据不一致,这时候方法体内创建一个CCZone对象对我要拷贝的对象进行包装,然后在调用CCAction当前子类对象的父类copyWithZone()将执行权转交给父类,这么一层层去递归调用,而在每一层的最后,当前类型下的克隆对象调用它的初始化函数,并将外部状态通过传参的形式传递进克隆对象。现在通过初始化方法传递进克隆对象的参数其实就是原型对象当前时间段的状态信息,所以经过这种递归过程后,父类一直到最底层的子类所有属性聚合经过实例化构成的这个克隆对象的的每一层数据(自己类定义的,继承自父类的,父类继承自它的父类的)都和原型对象的状态保持了一致,所以就实现了对原型对象的最终拷贝。这里是用递归调用的方法来让每一个类层次本身来自己负责自己将数据传递给克隆对象的,调理清晰。一般在子类里面初始化父类状态的做法是不建议的。




以下这四个模式不熟悉:
【二十】工厂方法模式--对象创建型模式
【二十一】解释器模式--类行为型模式
【二十二】模板方法模式--类行为型模式
【二十三】访问者模式--对象行为型模式




【总结】
(1),不知道==不断变化的==变量==抽象==接口==方法声明,很清楚==状态稳定的==常量==具体==实现==方法体。
(2),复用,如果你想用一个东西来满足你的需求,那么你不必为了使用这个东西而自身去改变或者改变很少,那么这个东西对于你来说就是可方便利用的,这个东西的特性对于其他人也适用,那么这个东西就是兼具通用性和可复用性的。这个解释如果嫁接到代码上面是同样的道理,客户代码想用一个已经实现了的模块来满足它的需求,那么如果客户代码因为要使用这个模块而做出的改变越少,甚至不作出任何改变,就说明这个模块的复用性越好。设计模式就是想让我们写出这种复用性良好的代码。
(3),人,是一个独立的个体,你可以称他为对象,而人与人之间有一门学问,那就是人际关系。计算机世界里面的内存块等等,在编程领域程序员们称它为对象,而对象与对象之间的关系也是一门学问,这就是设计模式要教会我们的东西。设计模式就是要教程序员们在计算机世界里面怎么去搞好“人际关系”。




【结尾语】
各种设计模式的具体代码实现可参考GOF的那本书。
由于时间仓促,本人没有审稿,如有不对的地方敬请谅解。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值