GOF设计模式

xiaoxiao2021-02-28  3

为什么学习设计模式

刚工作的时候其实阅读过一遍这本书,但是当时并没有太多感悟。最近重读时才发现,学习设计模式其实是很有必要的。

不管是有意还是无意识中,日常工作中的很多代码实际上就是某一种设计模式,学习设计模式一方面有助于规范代码,另一方面也有助于理解他们的代码。

在代码中,规范是很重要的一个环节,尽量使用较为通用和为人所知的方法来解决问题实际上也是规范的一环。

阅读之后,稍微整理了这些设计模式,以便将来不时翻阅记忆。

ABSTRACT FACTORY(抽象工厂)

创建一系列相关或者相互依赖的对象的接口。它隐藏了要创建的类的信息,以及如何创建这些类,在让逻辑清晰的同时也能够在不改变客户代码的前提下对类进行修改。但由于在扩展产品数量的时候要对所有的具体实现的工厂类同时进行扩展,所以抽象工厂难以支持新的产品。在实现上,可以用Singleton模式创建工厂的实例,对具体的工厂,除了创建子类之外,也可以选择使用Prototype,在创建类的接口定义方面,除了为每一种产品都定义一个接口之外,也可以仅使用一个接口,通过不同的参数来区分所需要的产品类型。

BUILDER(生成器)

将复杂的对象的构建过程的抽象出来。Builder对外提供构建产品的步骤的接口,产品的装配细节只有Builder的提供者可见,而使用者可以通过调用这些接口,可以较为精确的控制自己想要构建的产品。与ABSTRACT FACTORY不同,BUILDER是在最后一步才返回产品。

PROTOTYPE(原型)

通过创建好的实例为基础,通过拷贝它们来创建新的实例的方法。好处在于能够随时添加新的原型,缺点就是所有的原型类都必须实现拷贝操作,否则无法正常工作。实现上,需要有一个用于管理原型的原型管理器,也可以添加一个初始化函数在拷贝完成之后对行实例进行初始化。

SINGLETON(单例)

保证类仅有一个实例,且只能通过一个访问点来访问这个实例。由于是唯一的实例,所以能够很容易的控制它的行为,控制何时能访问它,也可以创建多个子类,在需要的时候替换这个实例的类。当然单例不局限于只有一个实例,你也可以拥有多个实例,依据需要而定。单例是对全局变量的一种改进,能够避免命名空间的污染。在实现上,可以通过模板类的方式来快速实现一个单例基类。

ADAPTER(适配器)

包装一个已有的类,使它包装后的接口能够与另一个类一同工作。适配器在实现上有两种方式,一种是通过多继承的方式,称为类适配器,它不需要额外的指针来指向需要适配的对象,缺点是无法同时适配它的子类。另一种是通过将需要适配的对象作为成员,用指针指向它,优点是可以同时适配对象及它的子类。

BRIDGE(桥接) 

将抽象部分与它的实现分离,使得它们可以相互独立变化。相比于继承,桥接能更加灵活的对抽象部分和实现部分独立地进行修改、扩充和重用。使用桥接,抽象类的实现能够在运行时进行配置,抽象与实现的相互独立有助于降低实现部分对编译的依赖,有助于分层,从而产生更好的结构化系统,此外还能对客户隐藏实现细节。实现上,当Implementor仅有一个时可以不需要抽象的Implementor类,但使用桥接仍有许多优点。在Implementor的选择上,可以通过在构造器中传递参数来决定,也可以代理给某个对象例如前面的抽象工厂。几个抽象接口可以同时共享一个实现,使用引用计数方法来共享对象。 

COMPOSITE(组合) 

将对象组合成树形结构以表示“部分-整体的层次结构”。COMPOSITE使得单个对象和组合对象在使用上具有一致性。它能够很容易的增加一些新的组件,但带来的问题是设计上的一般化,你很难限制组合中的组件。实现上,需要维持一个显示的父部件的引用。由于COMPOSITE目的之一就是尽量让Leaf和Composite在外观和行为上保持一定的一致,所以应当尽量最大化Component的接口,对一些只对Composite有意义的接口,Leaf保持缺省行为。但这样做可能会带来安全性问题,因为从Leaf中删除和增加对象是不允许的。在Component上维护Component的子节点列表会浪费存储空间,因为叶节点不存在子节点。在需要频繁查找遍历的情况下,使用高速缓存能提升效率。在组件的存储上,使用何种数据结构取决于实际的需求。 

DECORATOR(装饰) 

维护一个指向修饰对象的指针,在请求时,装饰者将请求转发给被修饰的对象,并在对象处理请求前/后附加一些额外操作,以此来为对象动态添加额外的职责。它相比生成子类更加灵活,并且能够较容易的组合这些额外功能,避免子类爆炸的问题。在无法生成子类的情况下,用于替代子类。实现上,为保证装饰者和被修饰对象接口的一致性,它们应该有统一的基类。对它们的共同基类而言,这个基类应当尽量保持简单,即集中于接口的定义而不是数据的存储,否则会导致它的子类过于庞大。在处理一些本身已经很庞大的类时,慎用装饰模式,可以选择用Strategy代替。 

FACADE(外观) 

定义一个高层接口来为子系统中的一组接口提供一个一致的界面,使得子系统更加容易使用。将系统划分为子系统有助于降低系统复杂性,子系统通过统一的Facade来相互联系能减少子系统间的相互耦合。Facade提供的统一对外接口,也有助于在子系统演变得愈发复杂的情况下,还能对外保持较好的易用性。子系统和Facade的层次关系也能够降低客户和子系统间的耦合,便于消除客户程序和子系统间的依赖关系,从而能够更加方便的维护修改子系统。 

FLYWEIGHT(享元) 

主要用于应对大量细粒度对象,通过共享对象的方式,牺牲性能来换去存储空间上的节省。实现上,首先是对问题的建模,如何将这些对象映射到共享的对象上,另外如何管理共享对象也是需要考虑的问题,应该有一个统一的共享对象管理器,在需要的时候添加删除共享对象。 

PROXY(代理) 

提供一种通过代理来访问对象的方式。应用上,主要有远程代理(为一个不同地址空间的对象提供一个代表,一般存储它的地址),虚代理(创建开销很大的对象,如图片对象,根据需求来创建对象),保护代理(用于控制对原始对象的访问),智能指引(智能指针/引用计数)。

CHAIN OF RESPONSIBILITY(职责链) 

一条用于处理请求的对象链,并且在发送请求时不需要为请求指定接收者,而由链上的接收者决定是否去处理这条请求。它降低了耦合并使系统更加灵活,但相对的它无法保证请求一定被接收。实现上,其一是链接的实现(已有链接或维护新链接),其二是请求的表示形式(Hardcode请求也接收者的对应关系或者使用整形/字符串标识来识别请求)。 

COMMAND(命令) 

把请求封装成对象,从而可以对请求进行排队/记录,并支持撤销。命令本身包含了执行所需要的全部信息(接收者/执行者/如何执行),利于命令调用者和执行者之间的解耦。实现上,提供一个Reserve操作就可以实现Redo和Undo,但是需要避免这一过程中可能产生的累积错误,对简单命令,可以把接收者作为模板参数构建模板化的命令,从而简化代码。 

INTERPRETER(解释器) 

构建一种语言/文法来描述一类特定的问题,并定义相应的解释器来解释句子。它易于拓展和改变,文法也易于实现,但过于复杂的文法难以维护。实现上主要是构建语法树和定义解释操作,在某些情况下使用Flyweight共享符号有助于节省存储空间。 

ITERATOR(迭代器) 

对聚合对象内部顺序访问的封装,能够在不暴露内部表示的情况下提供对聚合对象的遍历,并支持用统一接口遍历不同聚合结构。它简化了聚合结构的接口,并且能够在同一聚合上同时进行多个遍历。实现上,由客户控制迭代的迭代器被称为外部迭代器,而由迭代器自身控制迭代称为内部迭代器,外部迭代器更加灵活。遍历算法可以由迭代器负责,这时候迭代器可能会需要一些额外访问权(友元),当然也可以由容器提供遍历算法,此时迭代器退化为一个记录当前遍历状态的Cursor。在使用迭代器的时候一定要注意迭代器失效的问题,迭代的同时增加/删除聚合中的元素是危险的,实现健壮的迭代器需要让聚合来注册迭代器,并在聚合元素变化的时候通知并更新迭代器。使用空迭代器(NullIterator)有助于处理边界条件。 

MEDIATOR(中介者) 

对一系列对象交互的封装,降低各对象之间的耦合。它将各个对象之间的交互集中到一个类中,有助于理清对象之间的相互关系,逻辑更加清晰。但这样可能导致中介者过于庞大复杂,使得中介者本身难以维护。实现上主要是需要实现Mediator和Colleague之间的通信,Observer模式可以用于这样的通信。 

MEMENTO(备忘录) 

在不破坏封装的前提下捕获一个对象的内部状态并在外部保存起来,以便在需要之时能够将对象恢复到保存的状态。它能够保持对象原有的封装,把存储工作交给备忘录能够简化对象的设计,但使用上代价可能会很高(大量的拷贝/存储),在某些语言下封装性很难保证。实现上,需要语言支持宽/窄接口,宽接口由Memento对对象,而窄接口对外。另外在顺序可以预测的情况下,可以使用增量存储来记录改变。 

OBSERVER(观察者) 

定义了对象间一种一对多的依赖关系,当一个对象的状态发生改变的时候,依赖于它的对象能够得到通知并被自动更新。目标仅仅知道它有一系列观察者,这能够去除目标和观察者之间的耦合,各自之间可以独立的相互改变,能够支持广播,观察者可以自由地选择忽略还是执行请求,此外也可以时刻增删观察者。但带来的问题是观察者不知道其他的观察者,它对最终改变的代价一无所知,如果观察者之间存在相互的依赖,会导致最终结果的不确定。 

实现上,首先要创建目标到观察者的映射,显示的保存引用或者用hash表等方法。也可以扩展接口,添加和改变内容相关数,让观察者能够只对它感兴趣的内容作出回应。要避免悬挂引用的问题,删除一个目标时,应当通知它的观察者。 

在发送的内容上,经常可以包含一些额外的信息,这里有两个极端的情况,其一是发送所有的信息不管需要与否,称之为推模型;另一个情况是除了通知之外什么也不送出,观察者需要显示的请求信息,称之为拉模型。 

在发送的时机上,有两种选择,其一是在状态变化时自动发送通知,另一个是在一系列状态改变完成后手动调用发送通知。无论哪种情况都要确保目标的状态是一致的,避免在通知发出之后再改变自身状态。 

在复杂的情况下可以建立管理器来维护观察者和目标之间的引用,而不是之间由目标来维护对观察者的引用。此外管理器还能够定义合适的更新策略,根据目标请求来通知依赖目标的观察者。 

STATE(状态) 

在内部状态改变时改变它的行为,避免了子类的生成,同时也避免了大量的条件语句。这个状态通常用枚举表示。它将特定状态下的行为分割开来,状态之间的显式切换让对象的行为更加明确。实现上,状态的切换可以由使用的环境(Context)来指定,当然也可以由State/State子类来指定它的后继状态。另外可以用表来驱动状态,每一个状态都映射到表中,从表中获取后继状态。状态的创建于销毁也是一个问题,可以选择在需要的时候创建/销毁它们,也可以选择提前创建好状态对象,始终不销毁它们。 

STRATEGY(策略) 

将一系列算法封装起来使它们可以相互替换,它可以在一定程度上代替继承,并能消除一些条件语句。在一些情况下许多类只是行为上有区别,将这些行为作为“策略”单独封装从而可以配置一个类的行为,在类有多种行为的情况下,通过封装成策略可以减少条件语句。它能够提供相同行为的不同实现,客户可以根据需求选择。缺点是客户必须了解Strategy,才能选择合适的策略。由于多个策略共享接口,那么会存在一些永远不会被使用的参数,而为了避免这个问题则需要Strategy和Context之间更加紧密的耦合。实现上,具体策略需要知道上下文中它所需要的所有信息,一种方法是把Context中数据放入参数传递给Strategy,但可能发送一些不需要的数据。另一种办法是将Context自身作为一个参数传递给Strategy,再由Strategy向Context请求数据。可以将Strategy作模板参数来配置一个类。为Strategy定义缺省的对象能够简化它的使用,只有在缺省行为不能满足需求时才需要配置Strategy。 

TEMPLATE METHOD(模板方法) 

在基类中定义好算法的骨架,再将一部分步骤延迟到子类之中。模板方法可以使子类在不改变算法结构的情况下重定义某些步骤。模板方法是代码复用的基本技术。模板方法调用的操作有具体操作、AbstractClass操作、原语操作(抽象操作)、Factory Method和钩子操作。实现上,尽量少用原语操作,在C++中原语操作定义为纯虚函数,实现子类必须重写它。需要重定义的操作越多程序就越冗长。命名上,尽量给被重定义的操作加上前缀以便识别它们。 

VISITOR(访问者) 

用于表示一个作用于某个结构中的各元素的操作。它使你在不改变各元素的类的前提下定义作用于这些元素的操作。使用Visitor能很容易的增加新的操作,并简化了结构的接口,因为Visitor能分离相关操作。但当提供给Visitor的接口功能过于强大时,可能会破坏封装性。 

转载请注明原文地址: https://www.6miu.com/read-2450153.html

最新回复(0)