上周讲述了DOF设计模式中的“对象创建”模式和“接口隔离”模式,本周讲述DOF设计模式中的剩下的模式,“对象性能”模式(包括Singleton单件模式和Flyweight享元模式)、“状态变化”模式(包括State状态模式和Memento备忘录)、“数据结构”模式(包括Composite组合模式、Iterator迭代器和Chain of Responsibility职责链)、“行为变化”模式(包括Command命令模式和Visitor访问器)以及“领域规则”模式(包括Interpreter解析器)。
(一)“对象性能”模式
面向对象很好的解决了“抽象”的问题,但是必不可免地要付出一定的代价。对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。
典型模式:Singleton、Flyweight。
1、Singleton单件模式
Singleton单件模式定义:保证一个类仅有一个实例,并提供一个该实例的全局访问点。
Singleton单件模式动机:在软件系统中,经常有这样一个特殊的类,必须保证它们在系统中只存在一个示例,才能确保他们的逻辑正确性、以及良好的效率。
如何绕过常规的构造器,提供一种机制来保证一个类只有一个实例?这个应该类设计者的责任,而不是使用者的责任。
优点:减少了时间和空间的开销(new实例的开销);提高了封装性,使得外部不易改动实例。
缺点:懒汉式是以时间换空间的方式;饿汉式是以空间换时间的方式。
Singleton单件模式的结构如图1所示。
总结:
(1)Singleton模式中的实例构造器可以设置为protected以允许子类派生。
(2)Singleton模式一般不要支持拷贝构造函数和Clone接口,因为这有可能会导致多个对象实例,与Singleton模式的初衷相违背。
(3)实现多线程环境下安全的Singleton,注意对双检查锁的正确实现。
双检查锁,在lock的前后判断m_instance是否为空。因为可能多个线程都走进到m_instance==nullptr分支,所以之后每个线程在获得锁之后要再次判断m_instance==nullptr,来确保m_instance不会被重复实例化。但可能出现内存读写reorder问题,在经过编译器优化后,实例化Singleton可能不是按照分配空间、构造和地址赋值给指针的顺序进行的,而是按照分配空间、指针赋值、构造这三个步骤,当一个线程执行到指针赋值后,如果有另一个线程进来判断m_instance指针不为空,直接返回m_instance,并直接使用这个指针,那就会发生错误,因为第一个线程还没有执行构造器,所以这时双检查锁也就是失效了。解决办法,将变量声明为volatile防止编译器优化代码。
2、Flyweight享元模式
Flyweight享元模式定义:运用共享技术有效地支持大量的细粒度对象。
Flyweight享元模式动机:在软件系统中采用纯粹对象方案的问题 在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价——主要指内存需求方面的代价。
Flyweight享元模式的结构如图2所示。
总结:
(1)面向对象很好的解决了抽相性的问题,但是作为一个运行在机器中的程序实体,我们需要考虑对象的代价问题。Flyweight主要解决面向的代价问题,一般不触及面向对象的抽象性问题。
(2)Flyweight采用对象共享的做法来降低系统中的对象的个数,从而降低细粒度对象给系统带来的内存压力。在具体实现方面,要注意对像状态的处理。
(3)对象的数量太大,从而导致对像内存开销加大——什么样的数量才算大?这需要我们仔细根据具体应用情况进行评估,而不能凭空臆断。
(二)“状态变化”模式
在组建构建过程中,某些对象的状态经常面临变化,“状态变化”模式可以使变化进行有效的管理,同时又维持高层模块的稳定。
典型模式:State、Memento。
3、State状态模式
State状态模式定义:允许一个对象在其内部状态改变是改变它的行为。从而使对像看起来似乎修改其行为。
State状态模式动机:在软件构建过程中,某些对象的状态如果改变,其行为也会随之而发生变化,比如文档处于只读状态,其支持的行为和读写状态支持的行为就可能会完全不同。
如何在运行时根据对象的状态来透明地更改对象的行为?而不会为对象操作和状态转化之间引入紧耦合?
优点:
(1)状态模式将与特定状态相关的行为局部化,并且将不同状态的行为分割开来。
(2)所有状态相关的代码都存在于某个ConcereteState中,所以通过定义新的子类很容易地增加新的状态和转换。
(3)状态模式通过把各种状态转移逻辑分不到State的子类之间,来减少相互间的依赖。
缺点:导致较多的ConcreteState子类。
State状态模式的结构如图3所示。
总结:
(1)通过扩展子类,来添加进状态,解决状态转化问题,当有if...else时,可以转换成此方法。
(2)State模式将所有与一个特定状态相关的行为都放入一个State的子类对象中,在对像状态切换时, 切换相应的对象;但同时维持State的接口,这样实现了具体操作与状态转换之间的解耦。
(3)为不同的状态引入不同的对象使得状态转换变得更加明确,而且可以保证不会出现状态不一致的情况,因为转换是原子性的——即要么彻底转换过来,要么不转换。
(4)如果State对象没有实例变量,那么各个上下文可以共享同一个State对象,从而节省对象开销。
4、Memento备忘录
Memento备忘录模式定义:在不破坏封装性的前提下,不活一个对象的内部状态,并在该对像之外保存这个状态。这样以后就可以将该对像恢复到原想保存的状态。
Memento备忘录模式动机:在软件构建过程中,某些对象的状态在转会过程中,可能由于某种需求,要求程序能够回溯到对像之前处于某个点时的状态。如果使用一些公有借口来让其它对象得到对象的状态,便会暴露对象的实现细节。
如何实现对象状态的良好保存与恢复?但同时又不会因此而破坏对象本身的封装性。
Memento备忘录模式的结构如图4所示。
总结:
(1)在状态转化时,还原到某一个状态,用原发器创建备忘录,保存。
(2)备忘录(Memento)存储原发器(Originator)对象的内部状态,在需要时恢复原发器的状态。
(3)Memento模式的核心是信息隐藏,即Originator需要向外接隐藏信息,保持其封装性。但同时又需要将其状态保持到外界(Memento)
(4)由于现代语言运行时(如C#、java等)都具有相当的对象序列化支持,因此往往采用效率较高、又较容易正确实现的序列化方案来实现Memento模式。
(三)“数据结构”模式
常常有一些组建在内部具有特定的数据结构,如果让客户程序依赖这些特定的数据结构,将极大的破坏组件的复用。这时候,将这些数据结构封装在内部,在外部提供统一的接口,来实现与特定数据结构无关的访问,是一种行之有效的解决方案。
典型模式:Composite、Iterator、Chain of Responsibility。
5、Composite组合模式
Composite组合模式定义:将对象组合成树形结构以表示“部分-整体”的层级结构。Compisite使得用户对单个对象和组合对象的使用具有一致性(稳定)。
Composite组合模式动机:软件在某些情况下,客户代码过多地依赖于对像容器复杂的内部实现结构,对象容器内部实现结构(而非抽象接口)的变化将因其客户代码的频繁变化,带来了代码的维护性、扩展性等弊端。
如何将“客户代码与复杂的对象容器结构”解耦?让对象容器自己来实现自身的复杂结构,从而使得客户代码就像处理简单对象一样来处理复杂的对象容器?
Composite组合模式的结构如图5所示。
总结:
(1)Composite模式采用树形结构来实现普遍存在的对象容器,从而将“一对多”的关系转化为“一对一”的关系,使得客户代码可以一致地(复用)处理对象和对象容器,无需关心处理的是单个对象还是组合的对象容器。
(2)将“客户代码与复杂的对象容器结构”解耦是Composite的核心思想,解耦之后,客户代码将与纯粹的抽象接口——而非对像容器的内部实现结构——发生依赖,从而更能“应对变化”。
(3)Composite模式在具体实现中,可以让父对象中的子对象反向追溯;如果父对象有频繁的遍历需求,可使用缓存技巧来改善效率。
6、Iterator迭代器
Iterator迭代器模式定义:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露(隔离变化,稳定)该对象的内部表示。
Iterator迭代器模式动机:在软件构建过程中,集合对象内部结构常常变化各异。但对于这些集合对象,我们希望在不暴露其内部结构的同时,可以让外部客户代码透明的访问其中包含的元素;同时这种“透明遍历”也为“同一种算法在多种集合对象上进行操作”提供了可能。
使用面向对象技术将这种遍历机制抽象为“迭代器对象”为“因对变化中的集合对象”提供了一种优雅的方式。
Iterator迭代器模式的结构如图6所示。
总结:
(1)迭代抽象:访问一个聚合对象的内容而无需暴露他的内部表示。
(2)迭代多态:为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
(3)迭代器健壮性考虑:遍历的同时更改迭代器所在的集合结构,会导致问题。
7、Chain of Responsibility职责链
Chain of Responsibility职责链模式定义:使多个对像都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对像连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
Chain of Responsibility职责链模式动机:在软件构建的过程中,一个请求可能被多个对象处理,但是每个请求在运行时只能有一个接受者,如果显示指定,将必不可少的带来请求发送者与接受者的耦合。
如何使请求的发送者不需要指定具体的接受者?让请求的接受者自己在运行时决定来处理请求,从而使两者解耦。
Chain of Responsibility职责链模式的结构如图7所示。
总结:
(1)Chain of Responsibility模式的应用场合在于“一个请求可能有多个接受者,但是最后真正接受者只有一个”,这时候请求发送者与接受者的耦合可能出现“变化脆弱”的症状,职责链的目的就是将二者解耦,从而更好的应对变化。
(2)应用了Chain of Responsibility模式后,对象的职责分派将更具灵活性。我们可以在运行时动态添加/修改请求的处理指责。
(3)如果请求传递到职责链的末尾仍得不到处理,应该有一个合理的缺省机制。这也是每一个接受者对象的责任,而不是发出请求的对象的责任。
(四)“行为变化”模式
在组建的构建过程中,组建行为的变化经常导致组建本身剧烈的变化。“行为变化”模式将组建的行为和组建本身进行解耦,从而主持组件的变化,实现两者之间的松耦合。
典型模式:Command、Visitor。
8、Command命令模式
Command命令模式模式定义:将一个请求(行为)封装为对象,从而使你可用不同的请求,对客户进行参数化;对请求排队或记录请求日志以及支持可撤销的操作。
Command命令模式动机:在软件构建构成中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些场合——比如需要对行为进行“记录、撤销(undo)、事务”等处理,这种无法抵御变化的紧耦合是不合适的。
在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
Command命令模式的结构如图8所示。
总结:
(1)Command模式的根本目的在于“行为请求者”与“行为实现者”解耦,在面向对象的语言中,常见的实现手段是“将行为抽象为对象”
(2)实现Command接口的具体命令对象ConcreteCommand有时候根据需要可能会保存一些额外的状态信息。通过使用Composite模式,可以将多个“命令”封装为一个“符合命令”MacroCommand
(3)Command模式与C++中的函数对像有些类似。但两者定义行为接口的规范有所区别:Command以面向对象中的“接口-实现”来定义行为接口规范,更严格,但有性能损失;C++函数对象以函数签名来定义行为接口规范,更灵活,性能能高。
9、Visitor访问器
Visitor访问器模式定义:表示一个作用与某对像结构中的各元素的操作。使得可以在不改变(稳定)各元素的类的前提下定义(扩展)作用于这些元素的新操作(变化)。
Visitor访问器模式动机:在软件构建的过程中,由于需求的改变,某些类层次结构中常常需要增加新的行为(方法)。如果直接在类中做这样的更改,将会给子类带来很繁重的变更负担,甚至破坏原有设计。
如何在不更改类层次结构的前提下,在运行时根据需要透明地为类层次结构上的各个类动态添加新的操作,从而避免上述问题?
Visitor访问器模式的结构如图9所示。
总结:
(1)Vistor模式通过所谓的双重分发(double dispatch)来实现在不更改(不添加新的操作-编译时)Element类层次结构的前提下,在运行时透明地为类层次结构上的各个类动态添加新的操作(支持变化)。
(2)所谓双重分发即Vistor模式中间包括了两个多态分发(注意其中的多态机制):第一个accept方法的多态解析;第二个为visitElementX方法的多态辨析。
(3)Visitor模式最大的缺点在于扩展类层次结构(增添新的Element子类),会导致Visitor类的改变。因此Visitor模式适用于“Element类层次结构稳定,而其中的操作却进场面临频繁改动”。
(五)“领域规则”模式
在特定领域内,某些变化虽然频繁,但可以抽象为某种规则。这时候,结合特定领域,将问题抽象为语法规则,从而给出该领域下的一般性解决方案。
典型模式:Interpreter。
10、Interpreter解析器
Interpreter解析器模式定义:给定一个语言,定义它的文法的一种表示,并定义一种解释器,这个解释器使用该表示来解释语言中的句子。
Interpreter解析器模式动机:在软件构建过程中,如果某一特定领域的问题比较复杂,类似的结构不断的重复出现,如果使用普通的变成方式来实现将面临非常频繁的变化。
在这种情况下,将特定领域的问题表达为某种语法规则下的句子,然后构建一个解析器来解释这样的句子,从而达到解决问题的目的。
Interpreter解析器模式的结构如图10所示。
总结:
(1)Interpreter模式的应用场合是Interpreter模式应用中的难点,只有满足“业务规则频繁变化,且类似的结构不断重复出现,并且容易抽象为语法规则的问题”才适合使用Interpreter模式。
(2)使用Interpreter模式来表示文法规则,从而可以使用面向对象技巧来方便地“扩展”文法。
(3)Interpreter模式比较适合简单的文法表示,对于复杂的文法表示,Interpreter模式会产生比较大的类层次结构,需要求助于语法分析生成器这样的标准工具。
设计模式总结
一个目标:管理变化,提高复用。
两种手段:分解,抽象。
八大原则:依赖倒置原则(DIP)
开放封闭原则(OCP)
单一职责原则(SRP)
Liskov替换原则(LSP)
接口隔离原则(ISP)
优先使用对象组合,而不是类继承
封装变化点
针对接口编程,而不是针对实现编程
重构方法:静态变为动态,早绑定变为晚绑定,继承变为组合,编译时依赖变为运行时依赖,紧耦合变为松耦合。
C++对象模型:
不需要设计模式的情况:
(1)代码可读性很差时;(2)需求理解很浅时;(3)变化没有显现时;(4)不是系统的关键依赖点时;(5)项目没有复用价值时;(6)项目将来要发布时。
设计模式经验:
(1)不要为模式而模式;(2)关注抽象类和接口;(3)理清变化点和稳定点;(4)审视依赖关系;(5)要有Framework和Appliacation的区隔思维;(6)良好的设计是演化的结果。