7.对象性能
- 面向对象很好地解决了“抽象”的问题,但是必不可免地要付出一定的代价。对于通常情况来讲,面向对象的成本大都可以忽略不计。但是某些情况,面向对象所带来的成本必须谨慎处理。
- 典型模式
- Singleton
- Flyweight
7.1 Singleton
动机
- 在如那件系统中,经常有这些一些特殊的类,必须保证它们在系统中只存在一个实例,才能确保它们的逻辑正确性、以及良好的效率。
- 如何绕过常规的构造器,保证一个类只有一个实例。
- 这应该是类设计者的责任,而不是使用者的责任。
写法
- 常规写法,线程不安全
Singleton* Singleton::getInstance(){ if(m_instance == nullptr){ m_instance = new Singleton; } return m_instance; }
- 加锁,但是代价太大
Singleton* Singleton::getInstance(){ Lock lock; if(m_instance == nullptr){ m_instance = new Singleton; } return m_instance; }
- 双检查锁,但由于内存读写reorder不安全
Singleton* Singleton::getInstance(){ if(m_instance == nullptr){ Lock lock; if(m_instance == nullptr){ m_instance = new Singleton; } } return m_instance; }
- 在new的过程中,理论上是如下的三个步骤:
- 分配内存
- 执行构造函数
- 将指针赋值给m_instance
- 但是如果发生reorder,有可能颠倒顺序:
- 分配内存
- 将指针赋值给m_instance
- 执行构造函数
- 此时如果thread A 按此步骤执行,将指针赋值给了m_instance,而恰恰thread B 来获取m_instance,由于指针已经赋值给m_instance,那么thread B就会得到m_instance,但是此时thread A还未调用构造函数,m_instance指向的对象还不能用,如果这个时候thread B去使用他得到的m_instance,就会出问题。
- 在new的过程中,理论上是如下的三个步骤:
- C++ 11 版本之后的跨平台实现
std::atomic<Singleton*> Singleton::m_instance; std::mutex Singleton::m_mutex; Singleton* Singleton::getInstance(){ Singleton* tmp = m_instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire); //获取内存fence if(tmp == nullptr){ std::lock_guard<std::mutex> lock(m_mutex); if(tmp == nullptr){ tmp = new Singleton; m_instance = new Singleton(); std::atomic_thread_fence(std::memory_order_release); //释放内存fence m_instance.store(tmp, std::memory_order_relaxed); } } return tmp; }
模式定义
- 保证一个类只有一个实例,并提供一个该实例的全局访问点。
结构
要点总结
- Singleton模式中的实例构造器可以设置为protected以允许子类派生。
- Singleton模式一般不要支持拷贝构造函数和Clone接口,这会导致多个对象,违背了Singleton模式的初衷。
- 多线程安全的Singleton,应该注意双检查锁的正确实现。
7.2 Flyweight
动机
- 在软件系统采用纯粹对象方案的问题在于大量细粒度的对象会很快充斥在系统中,从而带来很高的运行时代价————主要指内存需求方面的代价。
- 如何在避免大量细粒度对象问题的同时,让外部客户程序仍然能够透明地使用面向对象的方式来进行操作?
模式定义
- 运用共享技术(解决效率问题的常用手段)有效地支持大量细粒度的对象。
结构
- GetFlyweight(key) 通过这个方法得到对象,函数通过key进行判断,如果存在,直接返回,如果不存在,则创建,并加入pool中,再返回。
要点总结
- 面向对象很好地解决了抽象性的问题,但是作为一个运行在机器中的程序实体,我们需要考虑对象的代价问题。Flyweight主要用来解决代价问题,一般不触及面向对象的抽象性问题。
- Flyweight采用对象共享的做法来降低系统中对象的个数,从而降低细粒度对象给系统带来的内存压力。在具体实现方面,要注意对象状态的处理。
- 对象数量太大从而导致对象内存开销加大————什么样的数量才算大?这需要我们根据具体的应用情况去评估。
8.状态变化
- 在组建构建过程中,某些对象的状态经常面临变化,如何对这些变化进行有效的管理?同时又维持高层模块的稳定? 状态变化模式为这一问题提供了一种解决方案。
- 典型模式
- State
- Memento
8.1 State
动机
- 在软件构建过程中,某些对象的状态如果改变,其行为也会随之而发生改变,比如文档处于只读状态,其支持的行为和读写状态支持的行为就可能完全不同。
模式定义
- 允许一个对象在内部状态改变时改变它的行为,从而使对象看起来似乎修改了其行为。
结构
要点总结
- State模式将所有与一个特定状态相关的行为都放入一个State的子类对象中,在对象状态切换时、切换相应的对象;但同时维持State的接口,这样实现了具体操作与状态装换之间的解耦。
- 为不同的状态引入不同的对象使得状态转换变得更加明确,而且可以保证不会出现状态不一致的情况,因为转换时原子性的----即要么彻底转换过来,要么不转换。
- 如果Stat对象没有实例变量,那么各个上下文可以共享同一个State对象,从容节省对象开销。
8.2 Memento
动机
- 在软件构建过程中,某些对象的状态在转换过程中,可能由于某种需要,要求程序能够回溯到对象之前处于某个点时的状态。如果使用一些共有接口来让其他对象得到对象的状态,便会暴露对象的实现细节。
模式定义
- 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。
结构
要点总结
- 备忘录(Memento)存储原发器(Originator)对象的内部状态,在需要时恢复原发器状态。
- Memento模式的核心是信息隐藏,即Originator需要向外接隐藏信息,保持其封装性。但同时又需要将状态保持到外界(Memento)。
- 由于现代语言运行时(如C#、Java等)都具有相当的对象序列化支持,因此往往采用效率高、又容易正确实现的序列化方案来实现Memento模式。
9.数据结构
- 常常有一些组件在内部具有特定的数据结构,如果让客户程序依赖这些特定的数据结构,将极大的破坏组件的复用。这时候,将这些特定数据结构封装在内部,在外部提供统一的接口,来实现与特定数据结构无关的访问。
- 典型模式
- Composite
- Iterator
- Chain Of Resposibility
9.1 Composite
动机
- 客户代码过多地依赖于对象容器复杂的内部实现结构,对象容器内部实现结构(而非抽象接口)的变化将引起客户代码的频繁变化,带来了代码的维护性、扩展性等弊端。
模式定义
- 将对象组合成树形结构以表示部分-整体的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性(稳定)。
结构
要点总结
- Composite模式采用树形结构来实现普遍存在的对象容器,从而将一对多的关系转换成一对一的关系,使得客户程序可以一致地处理对象和对象容器,无需关心处理的是单个的对象,还是组合的对象容器。
- 将客户代码与复杂的对象容器结构解耦是Composite的核心思想,解耦之后,客户代码将与纯粹的抽象接口----而非对象容器的内部实现结构----发生依赖,从而更能应对变化。
- Composite模式在具体实现中,可以让父对象中的子对象反向追溯;如果父对象有频繁的遍历要求,可使用缓存技巧来改善效率。
9.2 Iterator
动机
- 集合对象内部结构常常变化各异。但对于这些集合对象,我们希望在不暴露其内部结构的同事,可以让外部客户代码透明地访问其中包含的元素。同时这种透明遍历也为同一种算法在多种集合对象上进行操作提供了可能。
模式定义
- 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露(隔离变化,稳定)该对象的内部表示。
结构
要点总结
- 迭代抽象:访问一个聚合对象的内容而无需暴露它的内部表示。
- 迭代多态:为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
- 迭代器的健壮性考虑:遍历的同时更改迭代器所在的集合结构,会导致问题。
9.3 Chain Of Resposibility
动机
- 一个请求可能被多个对象处理,但是每个请求在运行时只能有一个接受者,如果显示指定,将必不可少地带来请求发送者与接受者的紧耦合。
模式定义
- 使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
结构
要点总结
- Chain Of Responsibility 模式的应用场合在于一个请求可能有多个接受者,但是最后真正的接受者只有一个,这个时候请求发送者与接受者的耦合有可能出现变化脆弱的症状,职责链的目的就是讲二者解耦,从而更好地应对变化。
- 应用了Chain Of Responsibility模式后,对象的责任分派将更具灵活定,我们可以在运行时添加/修改请求的处理职责。
- 如果请求传递到职责链的末尾扔得不到处理,应该有一个合理的缺省机制,这也是每一个接受对象的责任,而不是发出请求的对象的责任。
9.4 回顾与总结
- 以上三个模式的特征很明显,都是和数据结构有关。
- 归到一起,能够从认知上很快的辨析出来其共性,但是Iterator模式和Chain Of Responsibility模式在现有软件环境下已经过时。
10.行为变化
- 在组件的构建过程中,组件行为的变化经常导致组件本身剧烈的变化,行为变化模式将组件的行为和组件本身进行解耦,从而支持组件的行为变化,实现两者之间的松耦合。
- 典型模式
- Command
- Visitor
10.1 Command
动机
- 行为请求者与行为实现者通常呈现一种紧耦合。但在某些场合----比如需要对行为进行记录、撤销/重做、事务等处理,这种无法抵御变化的紧耦合使不合适的。
模式定义
- 将一个请求(行为)封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。
结构
要点总结
- Command模式的根本弟弟在于将行为请求者与行为实现者解耦,在面向对象语言中,常见的手段是将行为抽象为对象。
- 实现Command接口的具体命令对象ConcreteCommand有时候根据需要可能会保存一些额外的信息。通过使用Composite模式,可以讲多个命令封装为一个符合命令MacroCommand。
- Command模式与C++中的函数对象有些类似,但两者定义行为接口的规范有所区别:Command以面向对象中的接口--实现来定义行为接口说明,更严格,但有性能损失;C++函数对象以函数名来定义行为接口规范,更灵活,性能更高。
10.2 Visitor
动机
- 在软件构建过程中,由于需求的改变,某些类层次结构中常常需要增加新的行为(方法),如果直接在积累中做这样的改变,将会给予子类带来很繁重的变更负担,甚至破坏原有设计。
模式定义
- 表示一个作用于某种对象结构中的各元素的操作。使得可以在不改变(稳定)各元素的类的前提下定义(扩展)作用于这些元素的新操作(变化)。
结构
要点总结
- Visitor模式通过所谓的双重分发(double dispatch)来实现在不更改(不添加新的操作--编译时)Element类层次结构的前提下,在运行时透明地为类层次结构上的各个类动态添加新的操作(支持变化)。
- 所谓双重分发即Visitor模式中间包括了两个多态分发(注意其中的多态机制):第一个为accept方法的多态解析;第二个为visitElementX方法的多态辨析。
- Visitor模式的最大缺点在于扩展类层次结构(添加新的Element子类),会导致Visitor类的改变。因此Visitor模式适用于Element类层次结构稳定,而其中的操作却经常面临频繁改动。
11.领域规则
- 在特定领域中,某些变化虽然频繁,但可以抽象为某种规则。这时候,结合特定领域,将问题抽象为语法规则,从而给出在该领域下的一般性解决方案。
- 典型模式
- Interpreter
Interpreter
动机
- 在软件构建过程中,如果某一特定领域的问题比较复杂,类似的结构不断重复出现,如果使用普通的编程方式来实现将面临非常频繁的变化。
模式定义
- 给定一个语言,定义它的文法的一种表示,并定义一种解释器,这个监视器使用该表示来解释语言中的句子。
结构
要点总结
- Interpreter模式的应用场合是Interprete模式应用中的难点,只有满足业务规则频繁变化,且类似的结构不断重复出现,并且容易抽象为语法规则的问题,才适合使用Interpreter模式。
- 使用Interpreter模式来表示文法规则,从而可以使用面向对象技巧来方便地扩展文法。
- Interpreter模式比较适合简单的文法表示,对于复杂的文发表示,Interpreter模式会产生比较大的类层次结构,需要求助于语法分析生成器这样的标准工具。