设计模式总结

发现这文章总结得不错,就转载了过来。


首先讨论一下构造型模式

Factory Method
描述
是最基本的构造型模式。它将构造从单纯的构造函数中分离出来。
动机
用于封装复杂的或者是易于变化的构造操作。
适用性/作用
适用于创建那些构造时需要初始化的对象,或者需要按一定次序构造对象的时候。该模式结构很简单,额外的代码量也很少,因此适合在开发的开始阶段就可以使用,它通常不会有那些“过度设计”的麻烦。它的另外一个实际作用,是可以统一程序的对象构造风格,增强可读性。

Abstract Factory
适用性/作用:
维护一个对象系列的一致性。当存在多个并行继承体系,并且每一组继承都较为独立,且容易错误的混用时候,建议使用该模式来构造对象系列。

Builder
适用性/作用:
当对象构造步骤复杂,并且每个步骤都使用同样的接口,但是它们具备不同实现的时候,可以选择该模式。常用于创建 构造逻辑接近、但其构造实现差异很大 的一系列对象,如构造不同格式的文本文档(GOF的示例),它们的构造逻辑接近,但是实现却是与平台相关的。

Prototype
适用性/作用:
这个模式的本意是从一个对象构造另外一个对象。在一个应用系统中,由于我们通常也能很方面的使用其它的构造方法;或者我们只需要一个按照默认初始化方式构造的对象,这样并不适合使用原型。当我们希望从不是由客户代码控制的对象中复制出一个相同的但是被客户代码管理的对象(管理权移交);或者是对象需要深拷贝语义;或者是两个子系统协作时,一个系统无法方便的访问到另外一个系统的构造函数,但是确能很方面的获得它的实例时(往往是由参数传递进来的),便需要使用这样一个模式存在。

Singleton
适用性/作用:
提供一个全局唯一的描述。还可以用来取代一些难以避免的全局变量。

有关于构造型的一些个人看法:
在C++中,由于构造函数不能多态调用虚成员函数,因此很多时候Factory Method成为了维持构造-初始化的一致性的逼不得已的选择;但是在C#一类的语言中,由于构造函数本身是虚函数,因此一些Factory Method的作用可以通过将Template Method运用到构造函数中来避免一部分Factory Method的使用;Factory Method更加强调于封装可能的构造方法变化,以及统一设计风格,而不是隐藏不必要的构造步骤。

然后是结构型模式

Adapter
描述:
转换接口的格式(也可以说转换接口的signature)。
适用性/作用:
通常是为了统一程序风格。例如在开发系统时,发现某一个系统所使用的程序库接口不符合系统约定,便需要使用Adapter模式适配接口,使风格统一。该模式也可用于使两个系统接合到一起工作。

Bridge
描述:
分离接口与实现。
适用性/作用:
Bridge的Interface与其Implementation通常在程序的架构中是垂直关系,即上层调用下层。在基础设置,即Impl发生变化的时候,可以避免使用Interface的客户代码发生变化。将实现部分的变化消灭在接口上。
在一些使用beta版的lib开发软件,或者是开发所依赖的库有较大的可能性在软件的生命期内发生改变时,通常回使用该模式。

Composite
适用性/作用:
描述嵌套的结构,类似于程序设计里的递归,当每一个对象既是一个有独立功能的对象,也是一个容器时,通常会选择该模式,典型的例子就是XML Document。每一个节点都可能包含了叶节点。
需要注意的是,这些被包容和对象,和容器都具有及其类似的逻辑;如果从接口的层面上,可以认为这些对象是自包含的。

Decorator
描述:
动态、透明的为对象添加职责。
适用性/作用:
该模式有两个主要参与者,Decoratee和Decorator。前者为原有的对象,而后者则是在前者的基础上添加了新的功能。在GOF的描述中,Decorator和Decoratee使用了相同的接口。同时大部分的相关材料都侧重于这一模式“添加职责”的功能。但是有一点个人认为,在该模式的使用中还有两点值得考虑
首先,Decorator和Decoratee不仅在继承体系中属于同一层,在整个系统的功能体系中也应当是平级的。Decoratee自身也需要有相当的可能性被独立的使用到,否则在两个对象之间建立Decorator-Decoratee的关系就不那么必要,也许将Decoratee在功能体系中下移是个更好的选择,因为它会避开“相同接口”这样一个限制;
其次,Decorator之间的功能划分最好是正交的。一旦Decorator之间出现了不一致性,那么Decorator的装饰次序将会影响到实际的结果,或者对某个Decorator而言Decoratee存在着限制。而这些运行时才有的特性很难在静态设计中体现出来,会造成对系统功能的理解造成障碍。

Facade
描述:
为子系统中的一组接口提供一个统一的界面
适用性/作用:
由于子系统间的类联系比较紧密,他们通常会一一种固定的协作方式完成某项任务。当客户代码多以这种粗粒度的方式与子系统打交道时,Facade模式便有必要起来。
Facade模式可以用来解决以下两个问题
第一,如果某种粗粒度任务都是由一系列的细粒度组件以一定的语义关系耦合起来。而客户代码只关心这种粗粒度的任务,但不关心这个任务究竟怎样由子系统的组件合作完成的。那么这是,可以使用Facade模式将细粒度之间的语义耦合封装起来,并且这种封装可以防止子系统组件间合作的改变对客户代码造成影响。
第二,也就是GOF所描述的,简化客户使用子系统的复杂度。
Facade本身的设计在实践过程中会遇到以下几种情况
第一种情况下,Facade本身具有很高的内聚,同时它完全封装了子系统(说明子系统内聚度高),客户代码不需要知道子系统的细节便可以用Facade完成全部的工作。这一点是最理想的情况。
更多的可能是,Facade不能包办一切,程序既要与子系统打交道,也要与Facade打交道。这种情况下,如果Facade使用不当,便会破坏系统的层次结构。
个人的经验是,
如果一些子任务可以由Facade封装而完全不需要与子系统打交道,便对子系统的部分粗粒度任务使用Facade模式,保持局部的内聚性和抽象能力;
如果一个功能,客户通常都直接调用Facade,但是在少数情况下不可避免的要与细粒度组件打交道,那么通常我会将Facade旁置为一些Help Function,让它们与子系统组件处于差不多同样的地位,并以文档显示地注明其实现原理。
当然,能通过修改子系统的设计避免这种情况的出现是最完美的结局了。

Flyweight
适用性/作用:
呃,关于这个我想GOF已经描述的非常好了,我也就不说废话了。注意Flyweight在其它模式中的应用就行了。我经常就会在该用它的时候忘了用了。Flyweights通常由pool手法实现。

Proxy
适用性/作用:
Proxy的目的很明确,在维持原有接口和功能设计情况下,解除原始对象的限制。这最后的一句话是我的理解。通常一个对象实现一个功能直接实现就可以了;但是在一些应用中,会有一些与功能无关的限制和要求,例如对象可能是远程的,或者有内存方面的限制等等。为了解除这些限制,需要一些额外的手法来达到这样的目的。但是它们所附加的内容在功能设计时并没有描述。也就是说,这些不是客户需要关心的,它们应该被抽象掉。Proxy就是为了隐藏这些与代码的核心功能无关的实现,让客户端认为它是个没有限制的对象。


与前两种模式不同,一些行为型模式需要在动态过程中才能被良好的表达,这也给这些模式的理解加深了难度。特别是在对已有系统进行逆向分析的时候,对于一些混合使用了多个设计模式的组件而言,想准确的弄清楚用了哪些模式,更是困难。所以对设计模式使用动机的理解,是很重要的。
这个是题外话。

首先来讨论一下Chain of responsibility 
对于职责链来说,“链”这个词是关键。
它的一个隐含的意义就是,消息的散播有序的。这种序列可以表现为主从关系,或先后关系。
在运行时,消息的传播完全有可能是树状或者呈现DAG这样的结构(例如在composite中)。它适合于消息传播层次化的时候,如果不同对象之间的消息沟通是呈现复杂的网状或者扁平状,那么消息链就完全没有作用了。
如果消息的传递可以是无序的,或者不同消息接受者之间没有明显的主从关系,那么更适合用一些更加灵活的手法,比如signal-slot idioms。不过,signal slot与COR的关注点并不同,前者比后者要更加关注与实现细节和消息通讯的局部情况。
COR和Decorator也有一些相似之处的,只不过COR的关系是在动态过程中体现,而Decorator是在静态过程中体现;并且COR只是强调信息的转发,而Decorator则是强调功能的增强。前者强调模式的协作组件间提供一个机制,以及怎样实现这个机制;后者则强调模式的协作组件之间需要什么样的功能逻辑。

Command是个非常强大的工具,它的强大与难以理解对很多初学者来说都是很可怕的事情。
但是对于Command的基本理解,只需要知道它是C++版的Callback,就足够了。同时,对于Command的使用,我认为也仅限于Callback,如果Command的职责超出了Callback之外的用途,是需要谨慎对待的。Command解决了消息内容与组件之间的耦合问题,但是,它并没有解决交互耦合的问题。
以UI设计为例,使用Command可以将UI元素与逻辑之间互相不知道对方的内容。但是,在仅仅有Commmad的情况下,逻辑必须知道“UI”的基本情况才能正确运行(比方说与UI一起工作,Command需要绑定哪些要素)。实际上我们对它的期望是,逻辑部分不知道UI的存在,无论是Command Line下(此Command非彼Command,哈哈)还是GUI下,甚至作为一个系统的子系统出现,它都能正常的工作。而为与UI协同工作的Command很有可能就具备了UI下的Command实现所特有的特征。这些特征在移植到其它环境下会有一些困难,并需要做出设计上的再度取舍,Command模式本身也成为了一个包袱。
其次,Command模式通常用于多对多的关系中,这使得Command在时间上和逻辑上不连贯,也会给系统理解带来一定的难度。因此,如果将Command与Mediator或者COR、或者Visitor这样的模式搭配使用,将Command理解为实践技术与惯用手法,而将Mediator等作为设计元素来考虑的话,可能更加恰当一些。Command提供了消息的解耦合,而其它的模式则连同通信耦合也一并解除了。

Interpretor恐怕是DP里面所讨论的用途最窄的模式了。
实际上,稍微有点经验的人,对这个模式都不陌生。Composite,Interpretor和Builder之间有着千丝万缕的联系。前者体现了结构上的层次,Builder则适合于构造Composite,而Interpretor,可以作为Builder用于构造Composite的信息源。从更加宽泛的概念讲,Interpretor适合于将一种层次化的信息转换成另外一种表达方式。最典型的例子就是带有子结构的类的I/O实现,例如将对象持久化为XML或者逆持久化,在这里面就少不了它的身影。

ediator/Observer:
描述:将复杂的组件间交互集中到Mediator中/将复杂组件的交互用Observer - Subject相分离。
动机:软件的复杂之处就在于处理各个组分之间的联系。处理联系一般有两种方式,第一种是将相对独立的点进行有效的划分和隔离,减少软件之间的联系总数,降低复杂度。但是如果这些点之间关系太过复杂,那么划分是无法解决根本复杂度的,这时候,一种可选的方案是干脆将所有的联系封装到一点,让那些本该独立的部分不至于被这些藕断丝连拖累到一起。
对于大多数模式而言,显然前者是比较理想的选择。而Mediator几乎就是23.5个设计模式里面的特例,它的指导思想是将联系及其在软件生命周期中的变化集中到一处以方便管理。

用法:用于维护多个状态的一致性。Mediator和Observer通常可换用。

Mediator更加适用的情况:当操作本身出现强关联性而不是在数据上出现关联性,比方说一些消息的连锁触发。当然也可以把这一触发过程建模成事件后看做是状态的更改而启用Observer模式。
如果需要协调的类是不可更改的,且高度复杂或难于扩充,如第三方GUI控件,那么通常也会使用Mediator而不是Observer。
想用Observer?当然也可以,如果你愿意使用Adapter,Decorator或者Proxy等模式将目标控件封装起来,自然也是可以做到的。
更新操作需要高度集中或者需要高度优化时,需要选择Mediator,这也是紧耦合的擅长之处。

Observer的优势在于可以轻松的用单向连接或轮询的方式完成表现层到模型层的交互,因此对于Web一类有特殊限制的应用,Observer要更加适用一些。

Memento:
对于多数系统而言,这一设计几乎是一定可以见到的。Memento的变种颇多,比如序列化/反序列化,持久化/反持久化,实际上都是完成同样的工作。
应用:
Memento自身是需要进行再分层并复用的。
通常的Memento分为上下两层,下层与存储设备接驳,负责实际的存储工作。多数语言中的存储流均可以作为这一层中。在这一层需要为存储的物理介质提供一定程度的抽象,这样我们可以不用关心数据流究竟是被存储到内存中还是数据库或者网络服务器上。上层则提供了数据对象的语义,这样可以为客户对象提供一个友善的接口,同时这一抽象可以用于满足其他的需求,如缓存和代理。
Memento有两种典型的存储方式,一种是为一类对象提供固定的存储模式,这种设计可以最小化数据流量,因而可以获得良好的性能,缺陷在于与对象耦合过于紧密,在面临对象修改的时候灵活性有着很大的不足,另一种设计是基于查询表的理念。这种方法灵活性较高,可以使同样的数据以多个角度展现,缺点在于会遇到类型转换的问题,存储效率上较差。代码的自描述性弱,需要有文档进行约定。
在实际运用中,Memento模式面临的一大问题在于对内嵌引用的存储问题。
如果可以保证存储与读取时被保存对象持有的引用不发生任何可见的变化,那么可以直接保存引用本身(例如C++中的指针地址)。
如果被存储的对象所持有的内嵌引用可以用值进行替换(也就是不共享),那么可以选择将引用也嵌入式的序列化出来。如果需要将被共享的嵌入对象序列化,那么则需要显式的将内嵌的每一个独立的引用标识成一个唯一的句柄。在序列化时,依次的对象进行序列化,并对序列化的对象用句柄标识。在序列化对象时如果遇到了内嵌的引用,则用它的句柄填充到引用的位置上。在反序列化时,第一步是先将各个句柄反序列化成对象,填充上所有的值域,并重建句柄表,再利用句柄表将各个对象中的句柄用实际引用进行替换。
这一方法的典型运用包括运行状态的snapshot(例如游戏的读存盘)。
对于固定初始产生固定序列的系统,如伪随机,则可以保存它的初始值,并使用过程化的方法产生序列以恢复现场。但是这里需要注意,多线程的系统会加大对象恢复的难度,必须要仔细的考虑,例如仅在线程Join的地方,或线程固定的悬挂点上进行序列化工作。
此外,由于Memento经常保存并恢复完整对象,因此可以将Memento与Deepcopy统一设计实现。
例如运用DeepCopy保存当前对象的状态,并在恢复状态时直接用副本替换当前对象,或者将Deepcopy用Memento实现,可以实现对象的远程复制或状态同步。

State/Strategy:
State和Strategy的区别很小,他们的差别往往并非来自与程序上的差异而是在观点上的差异性。
个人认为,和State和Strategy类似的还有Abstract Factory以及Factory Method模式。之所以我将这几个模式绑定在一起进行讨论,本质上是因为他们都将分散在程序各处的分支判断集中到一处进行管理。
如果将这三个模式进行进一步的抽象,可以总结出它们共同的工作方式:获得上下文->获得前置条件并选择分支->执行分支->设置后置条件->重设上下文。
利用这个工作方式,下面简单的分析一下这三个模式各自的异同点以及适用面。
AF工作的理想情况是,上下文的获取、前置条件的设置、分支的选择放在一起完成。也就是说在一个执行期很靠前的地方就完成了产品线的设置,并且在很长一段运行期中不做更改。分支的执行通常比较Lazy,也很分散。后置条件和上下文设置不存在。
在其他工作条件下,这一模式可能会诱发三个负面问题。第一,是如果产品的创建分散,那么便需要仔细的跟踪程序的执行流程以确定到底哪一套产品线被构造出来;其次,如果产品系列间的区分不是依靠类型信息而是某个内含的状态进行区分,会加大对产品线判断的难度。第三,如果产品系列中对应的产品的构造接口存在这某种语义上的不一致性,或者对应产品本身的接口存在不一致性,都会使得程序的可读性大大下降。
因此Abstract Factory需要尽可能早的确定产品系列,同时产品系列在确定以后尽可能少做修改。如果产品线确定的很迟,那么最好在使用前再进行创建,并将Abstract Factory的创建活动放在它的产品的管理层上(例如XXXManager或XXXCollection这样的)。
State和Strategy的情况较为类似,这个工作流程中,各个工序可能很紧凑(例如FSM,有穷状态机),也有可能很分散。同时由于这一工作流程上下关系紧密,因此各道工序的合并或分离存在多种方案。下面将分析一下State模式。
如果将State的调用方看作State Machine,那么State变化有两类主要的触发者,一类来自于State Machine内,一类是来自于State Machine外的客户代码。实际上真正需要再State Machine内完成状态切换操作,基本上都是State之间的相互切换。就是说State本身是状态切换的触发者,这里简写为State-Triggered Mode(STM)。其他的状态切换的原因基本上都来自于外部,这里简写为Client-Triggered Mode(CTM)。
在设计State时,尽可能避免STM和CTM同时存在。一旦同时存在,由于STM基本上是不可以被放置到State Machine之外的,因此建议将STM和CTM同时封装在State Machine中,客户端通过调用State Machine的对应接口实现CTM。这样可以将State Machine的切换逻辑统一到State中,封装了可能的变化并提供了更高层的语义。
如果只存在CTM,那么State Machine大可以将State直接以getter/setter的形式提供给客户代码访问,简化了客户调用,也能明确客户的职责。
同时,在存在STM的时候,后置条件和上下文的设置,即状态切换既可以放在State中,也可以放在State Machine中。在State中直接切换状态很直接,同时可以在State需要经常修改并改变其后继状态的时候很管用,对于很复杂的状态跳转规则它也可以Work Well,和State Machine的依赖较小,仅仅需要通知State Machine切换到哪个状态上就可以了。同时,一旦状态机中添加或者删除了状态,那么也会使得修改扩散到很多地方。
如果在State Machine中进行状态切换,那么便需要State Machine对State有一定的了解,并且做出正确地决策。如果跳转规则很复杂,那么State Machine就会变得很麻烦,甚至可能导致State Machine变成一盘大的spaghetti。但是对于如果需要经常添加/删除State,或者State跳转规则多但简单的时候,在State Machine中维护状态切换会更方便一些。
好吧,我承认这个和Observer/Mediator一样,两者都很Perfect,如果你的运气够好,你的选择又正确的话。
如果问题本身就很困难,那就请不要过多的苛责设计,No Silver Bullet嘛。Good Luck,或者重新进行需求分析。


Template Method
Template Method是一个有点儿违背面向对象原则的模式。它虽然简化了代码结构,但是由于将一致性的维护分散在了子类和父类两个不同的部分,提高了代码的阅读难度。同时这也是一个实现继承而不是接口继承的例子。面向对象原则这个东西究竟只是原则,实际应用的时候,也是会有一定程度的变通。当然这变通也要付出代价的。虽然我不喜欢Template Method,但是仍然会经常的使用它,因为我遵循Kent Beck大牛的懒惰是程序员的美德这一教导,同时也是因为执迷于Occam剃刀的美感。

Visitor
其实我还真不太明白,如果我需要Visitor的时候,还有什么更好的选择。
并且,如果在遍历对象的时候无后效性的话(就是对其它对象的操作和当前对象的操作没什么关系,遍历结果与顺序无关),做起Visitor来,会轻松许多的。


出处:http://www.cppblog.com/lingjingqiu/archive/2008/11/21/67509.aspx


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值