从设计原则到设计模式
说明:这些资料仅仅是对设计模式的一些总结,没有设计模式的相关知识,很难看懂。即使看懂了这些,也仅说明理解了模式的基本思想。想要学好设计模式,还是建议好好看文后所列的参考书籍和推荐书籍。
这些总结有不少是根据自己的理解写成的,或许并不正确。如果您有不同的看法,请告知作者,谢谢!
欢迎传阅,但是请勿随意修改或Copy。
设计模式简介
每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。
-- Christopher Alexander
设计模式描述了软件设计过程中某一类常见问题的一般性的解决方案。面向对象设计模式描述了面向对象设计过程中,特定场景下,类(抽象类之间,抽象类和派生类)之间或者相互通信的对象之间常见的组织关系。
对象是什么?
----从概念层面讲,对象是某种拥有责任的抽象。
----从规格层面讲,对象是一系列可以被其他对象使用的公共接口。
----从语言实现层面来看,对象封装了代码和数据
从设计原则到设计模式
针对接口编程,而不是针对实现编程
---- 客户无需知道所使用对象的特定类型,只需要知道对象拥有客户所期望的接口。
优先使用对象组合,而不是类继承
---- 类继承通常为“白箱复用”,对象组合通常为“黑箱复用”。继承在某种程度上破坏了封装性,子类和父类耦合度高;而对象组合则只要求被组合的对象具有良好定义的接口,耦合度低。
封装变化点
---- 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合。
类设计的五项原则:
- SRP,单一职责原则,一个类应该有且只有一个改变的理由。
- OCP,开放封闭原则,应该能够不用修改原有类就能扩展一个类的行为。类模块应该是可扩展的,但是不可修改(对扩展开放,对修改封闭)。
- LSP,Liskov替换原则,派生类要与其基类自相容。子类必须能够替换它们的基类。
- DIP,依赖倒置原则,依赖于抽象而不是实现。高层模块不应该依赖于低层模块,二者都应该依赖于抽象。抽象不应该依赖于实现细节,实现细节应该依赖于抽象。
- ISP,接口隔离原则,客户只要关注它们所需的接口。不应该强迫客户程序依赖于它们不用的方法。
设计模式融合了上述设计原则,并提供了对常见设计问题的解决方案核心。
OO基础
抽象,封装,多态,继承
其中,抽象和封装是OO的基本。抽象使实际概念得以升华并工程化,从而使设计者能够使用概念视角来考查和设计复杂系统。封装用于封装系统中的各种变化(点),如内部数据和子类差异等,从而简化系统编程。多态和继承则仅仅是实现抽象和封装的必要技术,并非OO本质。
OO原则(同于类设计的五项原则)
1) 封装变化:找出应用中可能的变化点,封装易变代码,隔离稳定代码。
2) 多用组合,少用继承
3) 针对接口编程,不针对实现编程
4) 为交互对象之间的松耦合设计而努力
5) 类应该对扩展开放,对修改关闭
6) 依赖抽象,不依赖具体类. 如工厂方法(Factory Method)
7) 只和朋友交谈(墨忒耳法则):在对象的方法内,只应该调用属于以下范围的方法:a) 该对象本身; b) 被当作方法的参数传递进来的对象; c) 此方法所创建或实例化的任何对象; d) 对象的任何组件(实例变量)。
8) 别找我,我会找你(依赖倒置原则)。父类对外提供公共接口,这些接口调用子类(底层)具体实现接口。这条原则需要通过抽象方法(钩子方法)实现,如模板方法,架构类。
9) 类应该只有一个改变的理由
模式与设计的关系
每个模式都描述了某个特定场景中一个特定问题的约束因素/动机和关系,并为设计者提供一种解决这些问题的优雅方案。换句话说,模式仅仅是描述了特定场景下的关系和约束因素,正因如此,模式本身并不是最重要的,特定场景下的关系和约束因素才是最真实的,而模式仅仅是提供了一组描述这些关系的一组词汇,提供了一套解决这些关系的优雅方式而已。
在软件设计中,模式是随特定场景下的关系和约束因素而定的。也就是说,对所要解决问题的分析和理解是使用模式的必要条件。只有清楚了特定场景下的关系和约束因素,才能很好地选择解决问题的方法和适用的模式。
特定模式描述的是问题中概念(抽象类)之间的关系,比如所有的行为模式,bridge模式等;或者是抽象类和派生类之间的关系,比如Proxy,Composite,Decorator等;抑或是新类和原有类之间的关系,如Adaptor,Facade等。这些关系未必是显而易见的,它们都是问题分析的结果。没有从概念视角对问题的分析,这些关系是不能凸现出来的。因此,使用模式应该建立在共性与可变分析、分析矩阵等方法的基础上。在概念视角层次上的分析,往往考虑的是抽象类之间的关系,因此这时所采用的模式一般是行为模式、bridge模式等。描述抽象类和派生类之间关系的模式一般应该等到实现层次时才考虑引入。这种分层设计有助于简化设计过程,忽略不必要的次要因素,产生更好的高层设计。
根据上述观点,虽然模式经常是组合使用,但模式的使用顺序是有先后之分的。在高层设计会采用一些高层的主模式,在代码实现级别上,也会再次引入其他合适的模式。个人觉得行为模式、Bridge、Facade等模式是最常用的主模式。
模式与继承
(注:这里仅针对静态语言,在动态语言中,多态并非通过继承实现)
封装/继承/多态都是面向对象的基本概念。前面已经讲过,封装变化是模式的核心思想之一。同时,从抽象类和派生类的角度看,继承和多态使得抽象类能够封装派生类的类型。显然,继承和多态使类封装变化成为可能。因此,在设计模式中,继承是随处可见的。
从模式角度看,模式是面向对象复用的基础元素。采用模式可使设计的软件更加灵活,更易扩展,更易维护,这也正是OCP(开放封闭原则)所强调的。要实现这个目标,继承也是必不可少的。继承使派生类之间可互相替换,因此也就封装了抽象类背后的变化。
一般来说,使用模式是有代价的。模式虽有其自身的优越性,但只有在问题本身有一定的复杂性时,采用模式来简化这些复杂性才是有意义的。然而,不能忽略的是:模式本身(所涉及的相互交互的类)是有一定复杂性的。每个模式中相互交互的类之间,可以说是紧密耦合在一起的。这些类基本上是不可分的,它们本身就是作为一个整体而存在。同时这些类背后的继承关系在使模式灵活的同时,也增加了复杂性。这都是使用模式时需要考虑的因素。切记:模式中的类是作为一个整体而存在,它们是紧密耦合的关系;模式描述了这些类之间的关系和约束因素,丰富设计词汇,使我们容易交流,但同时要清楚实现时是对这些类和这些交互关系的再现。
设计模式与封装变化
设计模式可以确保系统能以特定方式变化(这很大程度是一种预测),从而帮助设计者避免重新设计系统。每一个设计模式允许系统结构的某个部分的变化独立于其他部分,这样产生的系统对于某一种特殊变化将更健壮。
下面阐述一些导致重新设计的一般原因,以及解决这些问题的常用设计模式:
1) 通过显式地指定一个类来创建对象。在创建对象时指定类名将使设计者受特定实现的约束, 而不是特定接口的约束。这会使未来的变化更复杂。要避免这种情况,应该间接地创建对象。
设计模式:Abstract Factory,Factory Method,Prototype。
2) 对特殊操作的依赖。 当设计者为请求指定一个特殊操作时,完成该请求的方式就固定了。避免把请求代码写死,可在编译时刻或运行时刻方便地改变响应请求的方法。
设计模式:Chain of Responsibility,Command。
3) 对硬件和软件平台的依赖。 外部的操作系统接口和应用编程接口(API)在不同的软硬件平台上是不同的。依赖于特定平台的软件将很难移植到其他平台上,甚至都很难跟上本地平台的更新。所以设计系统时限制其平台相关性就很重要了。
设计模式:Abstract Factory,Bridge。
4) 对对象表示或实现的依赖。 知道对象怎样表示、保存、定位或实现的客户在对象发生变化时可能也需要变化。对客户隐藏这些信息能阻止连锁变化。
设计模式:Abstract Factory,Bridge,Memento,Proxy
5) 算法依赖。 算法在开发和复用时常常被扩展、优化和替代。依赖于某个特定算法的对象在算法发生变化时不得不变化。因此有可能发生变化的算法应该被孤立起来。
设计模式:Builder,Iterator,Strategy,Template Method,Visitor
6) 紧耦合的类很难独立地被复用,因为它们是互相依赖的。紧耦合产生单块的系统,要改变或删掉一个类,必须理解和改变其他类。这样的系统是一个很难学习、移植和维护的密集体。
松散耦合提高了一个类本身被复用的可能性,并且系统更易于学习、移植、修改和扩展。设计模式使用抽象耦合和分层技术来提高系统的松散耦合性。
设计模式:Abstract Factory,Command,Facade,Mediator,Observer,Chain of Responsibility
7) 通过生成子类来扩充功能。 通常很难通过定义子类来定制对象。每一个新类都有固定的实现开销(初始化、终止处理等)。定义子类还需要对父类有深入的了解。如,重定义一个操作可能需要重定义其他操作。一个被重定义的操作可能需要调用继承下来的操作。并且子类方法会导致类爆炸,因为即使对于一个简单的扩充,也不得不引入许多新的子类。(本质: 继承应该仅仅封装一个变化点)
一般的对象组合技术和具体的委托技术,是继承之外组合对象行为的另一种灵活方法。新的功能可以通过以新的方式组合已有对象,而不是通过定义已存在类的子类的方式加到应用中去。另一方面,过多使用对象组合会使设计难于理解。许多设计模式产生的设计中,设计者可定义一个子类,且将它的实例和已存在实例进行组合来引入定制的功能。
设计模式:Bridge,Chain of Responsibility,Composite,Decorator,Observer,Strategy
8) 不能方便地对类进行修改。 有时设计者不得不改变一个难以修改的类。也许你需要源代码而又没有(对于商业类库就有这种情况),或者可能对类的任何改变会要求修改许多已存在的其他子类。设计模式提供在这些情况下对类进行修改的方法。
设计模式:Adapter,Decorator,Visitor
设计模式与其封装的变化点
| 设计模式 | 变化点 |
创建型 | Abstract Factory | 产品对象家族 |
Builder | 组合创建组合(复杂) 对象 | |
Factory Method | 被实例化的子类(相关类) | |
Prototype | 被实例化的类 | |
Singleton | 一个类的唯一实例 | |
Object Factory | 被实例化的类,Factory Method的变种 | |
Object Pool | 对象管理和重用,Factory Method的变种 | |
Creation Method | 类的构造函数,Factory Method的变种 | |
结构型 | Adapter | 对象的接口(可变API) |
Bridge | 对象实现(实现逻辑) | |
Composite | 对象的结构和组成 | |
Decorator | 对象的职责(职责组合) | |
Façade | 子系统的接口(子系统的变化) | |
Flyweight | 对象的存储 | |
Proxy | 对象的访问和对象的位置 | |
行为型 | Chain of Responsibility | 满足某个请求的对象(请求的实际处理对象) |
Command | 何时、如何实现某个请求 | |
Interpreter | 一个语言的文法和解释 | |
Iterator | 如何遍历、访问一个聚合的各元素 | |
Mediator | 对象间的交互逻辑 | |
Memento | 对象信息的存储管理 | |
Observer | 对象间的依赖和通讯 | |
State | 对象状态 | |
Strategy | 算法 | |
Template Method | 算法某些步骤 | |
Visitor | 作用于对象上的操作 |
---------------------------------------------------
模式的分类
模式依据其目的可分为创建型(Creational)、结构型(Structural)、或行为型(Behavioral)三种。创建型模式与对象的创建有关;结构型模式处理类或对象的组合;行为型模式描述类或对象之间的职责分配和交互。
根据范围准则,模式可分为类模式和对象模式。类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,由编译时确定。对象模式处理对象间的关系,这些关系在运行时刻是可变化的,更具动态性。从某种意义上来说,几乎所有模式都使用继承机制,所以“类模式”专指那些集中于处理类间关系的模式,而大部分模式都属于对象模式的范畴。
分类如下:
| 创建型 | 结构型 | 行为型 |
类 | Factory Method | Adapter(类) | Interpreter; Template Method |
对象 | Abstract Factory; Builder; Prototype; Singleton ----------------------- Object Factory Object Pool Creation Method | Adapter(对象); Bridge; Composite; Decorator; Façade; Flyweight; Proxy | Chain of Responsibility; Command; Iterator; Mediator; Memento; Observer; State; Strategy; Visitor |
说明:
创建型类模式将对象的部分创建工作延迟到子类,而创建型对象模式则将它延迟到另一个对象中。结构型类模式使用继承机制来组合类,而结构型对象模式则描述了对象的组合方式。行为型类模式使用继承描述算法和控制流,而行为型对象模式则描述一组对象如何协作完成单个对象所无法完成的任务。
还有其他组织模式的方式。有些模式经常会被绑在一起使用,例如,Composite常和Iterato r或Visitor一起使用;有些模式是可替代的,例如,Prototype常用来替代Abstract Factory;有些模式尽管使用意图不同,但产生的设计结果是很相似的,例如,Composite和Decorator的结构图是相似的。
创建型模式
创建型模式抽象了实例化过程。它们帮助一个系统独立于如何创建、组合、管理和表示它的那些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。
随着系统演化得越来越依赖于对象复合而不是类继承,创建型模式变得更为重要。当这种情况发生时,重心从对一组固定行为的硬编码(hard-coding)转移为定义一个较小的基本行为集,这些行为可以被组合成任意数目的更复杂的行为。这样创建有特定行为的对象要求的不仅仅是实例化一个类,而可能是一组复杂的对象组合。
在这些模式中有两个不断出现的主旋律。第一,它们都将关于该系统使用哪些具体的类的信息封装起来。第二,它们隐藏了这些类的实例是如何被创建和放在一起的。整个系统关于这些对象所知道的是由抽象类所定义的接口。因此,创建型模式在什么对象被创建,谁来创建,如何创建,以及何时创建这些方面给予设计者很大的灵活性。它们允许你用结构和功能差别很大的“产品对象”配置一个系统。配置可以是静态的(即在编译时指定),也可以是动态的(在运行时)。
Abstract Factory
意图: 提供一个创建一系列相关或相互依赖对象的接口,用于创建相关或依赖的对象家族,而无需明确指定具体类。
动机: 封装一系列相关或相互依赖对象的创建,隐藏了相关对象创建过程的选取组合问题,减少错误组合的可能性。
适用性: 常用于不同软件构建环境(平台,不同的软件需求配置等)下相关对象的创建。
结构: 一般使用继承来封装不同软件构建环境。父类定义所有的一系列创建接口,由具体类实现。本质上抽象工厂由多个工厂方法组成(每个子类均为对象工厂)。
优点: 减少对象错误组合使用的可能性。
Factory Method
意图:定义一个用于创建对象的接口,让子类决定将哪一个类实例化。FactoryMethod使一个类的实例化延迟到其子类。
动机: 让子类自己决定所需创建的对象,这里的对象一般并非子类本身,而是子类所需要的其他对象。本质:通过子类来创建对象,使客户仅依赖父类的抽象接口。
适用性: 常用于调用统一的抽象类接口,而让子类自己决定(实现)所需创建的对象,由此,封装了子类所创建对象的类型。
结构: 一般内嵌于具有继承关系的子类中,很少单独使用。抽象类提供创建接口,子类实现。典型例子: STL中迭代器的创建(end(),begin())。常用抽象接口:MakeObj();CreateObj()。
优点: 封装了子类所创建对象的类型。
组合模式: 经常用于TemplateMethod中的子类中,使创建过程成为TemplateMethod的一个步骤。
Note:实际中,设计者常常使用专门的工厂类负责相关对象的创建,这不同于FactoryMethod,但都属于对象工厂(负责创建对象的类)。
Builder
意图: 将一个复杂对象(一般内部含有多个其他对象)的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
动机: 封装复杂对象或具有多种组合的对象的创建过程。比如提供不同的接口表示同一个对象不同的构建过程。
适用性: 常用于封装具有可变构建过程的对象。
结构: 提供不同的接口封装不同的构建过程;提供较小粒度的接口,封装构建过程中对象间的交互关系,让客户通过提供的接口自己合成创建过程。
优点: 要么封装整个对象的构建,要么封装了创建过程中对象间的交互关系。
组合模式: 经常用于封装Composite的创建过程(提供小粒度的对外接口)。
Prototype
意图:用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。
动机: 拷贝已有对象(包括状态).
适用性: 常用于包含状态的对象拷贝,简化状态的同步过程。典型例子:CAD。
结构: 对象既有自拷贝的对外接口。注意区分浅拷贝和深拷贝,多线程也需要注意。
优点: 简化了对象状态的重新构建。向客户隐藏制造新实例的复杂性;提供让客户能够产生未知类型对象的途径;有时复制对象更高效。
组合模式: 有时与工厂Factory一同使用,通过单一的接口来创建所需的对象。(Map+clone)
Singleton
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
动机: 保证一个类仅有一个实例。
适用性: 方便对象的全局访问;或在当对象因存在多个副本引发性能问题时,才考虑使用Singleton来限制对象实例化。
结构: 常常和静态变量等相关.
优点: 方便全局访问,限制对象实例化,解决系统的性能问题。
缺点: Singleton本质上是一个全局变量,全局变量并不是什么好东西,不好管理,且容易引发其他问题。一般并不推荐使用Singleton.
ObjectPool
意图: 在创建对象比较昂贵,或者所能创建对象(File/socket)数目有限制时,管理对象的重用。
动机: 封装对重用对象的管理。
适用性: 当对象的创建和/或管理必须遵循一组定义明确的规则集, 并且这些规则都与如何创建对象、创建多少对象和在已有对象完成当前任务时如何重用等相关时,使用Object Pool来创建和管理对象。
结构: 类似于简单的Factory,即使用单独的类。
优点: 封装对重用对象的管理。
CreationMethod
意图: 基于类的不同构造函数,提供多种不同的对象构建过程
动机: 使用不同的Creation函数(语义明确)来区别所创建对象的用途(意图)。
适用性: 类中存在多种不同的构造函数,并且每种构造函数所构建的类有不同的用途/意图。
结构: 一般Creation函数需要是static函数,根据所使用的语言而定。类的构造函数为私有。
优点: 根据不同的Creation函数名,可以很清楚的描述构建对象的用途。
变化: 有时直接把派生类的构建直接放入抽象类中,并使用Creation Method。
---------------------------------------------------
结构型模式涉及到如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口或实现。结构型对象模式不是对接口和实现进行组合,而是描述了如何对一些对象进行组合,从而实现新功能的一些方法。因为可在运行时改变对象组合关系,所以对象组合方式具有更大的灵活性,而这种机制用静态类组合是不可能实现的。
Adapter
意图:将一个类的接口转换成客户希望的另外一个接口(转换接口)。Adapter模式使得原本由于接口不兼容而不能一起工作的类可以一起工作。
动机: 重用已有的类。
适用性: 重用已有的类去完成正在编写的类中所需的功能。一般原有的类的功能应该和所需类的功能相近,或者更多。
结构: 使用对象组合。不一定就用于子类的编写,也可以是普通类的编写,因此继承关系并不是必须的。典型例子:ACE对系统调用函数的封装。
优点: 重用代码,简化类的编写和测试。
Facade
意图:为子系统中的一组接口提供一个一致的界面(一致的接口), Facade模式定义了一个高层接口,这个接口使得子系统更加容易使用(简化接口)。
动机: 封装复杂的子系统,使客户更容易使用。同时也封装了原系统变化所引起的客户代码变化。
适用性: 存在功能复杂或易变的子系统,使用Facade增加一个封装层,方便用户使用。
结构: 使用一个类为子系统提供一组易用的对外接口。
优点: 简化编程,隔离变化。
Bridge
意图:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
动机: 分离对外接口和内部实现。本质上就是分离两个变化点,并单独封装其变化。
适用性: 用于具有两个变化点的抽象概念的分离。
结构: 使用两个抽象封装不同的变化点。
优点: 简化类之间的继承关系(理论上,一个抽象一个变化点)。隐藏实现的变化对客户的影响。适用于跨越多个平台的图形和窗口系统。
Composite
意图:将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得客户对单个对象和复合对象的使用具有一致性。
动机: 隐藏“一对多”的关系,让客户一致的对待不同的对象。
适用性: 常用于结构上具有显式树型结构的对象关系。当使用一致的方式对待列表中的每个对象的情况时,可以使用Composite来封装对对象列表的管理和遍历,简化客户代码对对象的管理,使客户能够使用一致的代码来对待所有对象。
结构: 常常在Composite的一个派生类中,使用列表来管理其他的派生对象;或者基于递归组合来组织可变数目的对象
优点: 封装了“一对多”的对象关系,使客户能够统一的对待所有的派生对象。省去客户自行管理对象的行为和代码(省略大量if语句)。
组合模式: 可以和Command对象一起使用,封装“一对多”的关系,当然如果Command对象需要考虑顺序或者其他情况的时候,应该使用Builder来构建。常常使用Builder来构建Composite,用Iterator来遍历对象。
Decorator
意图:动态地给一个对象添加一些额外的职责。就扩展功能而言,Decorator模式比生成子类方式(继承)更为灵活。
动机: 复用现有的类,在不破坏原来的条件下, 添加一些额外的职责/功能。
适用性: 存在一个具备核心功能的类(组件),但是同时需要一些附加的功能。使用Decorator可在不破坏原类的条件下,动态添加所需的附加功能。
结构: 如果需要动态的替换,继承关系是必须的,在继承关系下,只需在复用原类接口下改写部分接口即可。但是如果没有替换要求,没必要一定使用继承关系,只需简单的适配所需的接口即可。一般,每个装饰者都“有一个”(包装一个)组件,即保存某个组件的实例。如果需要多次修饰,那么可用多个装饰者包装核心组件(多层次继承)。
优点: 代码复用;容易在核心功能代码的基础上增加额外的功能。
缺点:装饰者会导致设计中出现许多小对象,如果过度使用,会使程序变得更加复杂。
Flyweight
意图:运用共享技术有效地支持大量细粒度的对象。让某个类的一个实例提供许多“虚拟实例”
动机: 对象共享,优化性能
适用性:当一个类有许多细小实例,且实例能被同一个方法控制,可使用绳量来存储细小对象,即将所有原来的实例状态存储在一个类中,统一管理。
优点:减少运行时对象实例个数,节约内存;将许多“虚拟”对象的状态集中管理。
Proxy
意图:为其他对象提供一个代理以控制对这个对象的访问。
动机: 对某个对象的访问有特殊的要求。比如需要跨越网络障碍等。
适用性:远程代理控制访问远程对象;虚拟代理控制访问创建开销大的资源;保护代理基于权限控制对资源的访问;缓存代理控制对缓存对象的访问。
结构:常用代理包装实际对象,在必要时可通过工厂来实例化对象。
优点: Proxy是一个重型模式(需要实现原对象的大部分接口),可使用Facade代替。
区别:1)装饰者为对象增加行为,代理是控制对象的访问。2)适配器会改变对象适配的接口,而代理则实现相同的接口。
-------------------下面两个可能谈不上模式,更像代码重构惯用法----------------------------
Compose Method
意图: 将一个比较复杂(逻辑或步骤多)的方法(接口实现)转化成为一系列意图自明的更小方法(接口)。
动机: 提供代码的可读性。
适用性: 所有的复杂的接口实现。该模式经常用于重构现有的代码。
结构: 用更小的接口封装代码行。
优点: 能够大大改善代码的可读性,如果接口名字取的好的话。
组合模式: 可与任何模式相结合,改善代码的质量。
Collecting Parameter
意图: 使用对象(结构体)在不同的地方进行参数收集。
动机: 所需的参数分散在系统的不同地方,使用该模式简化参数的收集和传递。
适用性: 用于类之间的数据收集和传递,也适用于类内部的参数收集和传递。
结构: 一般使用结构体来存放所需参数。
优点: 有利于接口之间和对象之间的参数传递。
组合模式: 常和ComposeMethod方法一起使用。
行为模式涉及到算法和对象间职责的分配。行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。这些模式刻画了在运行时难以跟踪的复杂的控制流。它们将你的注意力从控制流转移到对象间的联系方式上。
行为型模式的三个典型特点:
封装变化
对象作为参数
对发送者和接收者解耦
Chain of Responsibility
意图:为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。
动机: 解除请求的发送者和接收者之间耦合,在对象间传递待处理的请求。
适用性:用于请求的跨层传递,解除请求发送者和请求的最终处理者之间的耦合。统一地处理客户请求。常用于窗口系统,处理鼠标或键盘事件,典型应用:wxWidget的事件系统。
结构: 客户请求一般采用Command封装,使易于传递。一般不同对象采用统一的接口来处理请求。如果请求处理者对象存放在列表中,一般要求使用继承实现。
优点: 易于请求的跨层传递;解除对象耦合(封装请求的真实处理者);统一请求处理(封装请求的变化)。
组合模式: 经常采用Command来封装不同类型的请求.
Command
意图:将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化(传递请求);对请求排队或记录请求日志,以及支持可取消的操作。
动机: 封装请求的类型;让请求具有对象的特性(具有状态的实体),这样就可以传递、保存或者采用不同的方式来处理请求对象。
结构: 用继承来封装请求的类型;除了不一样的构造和初始化函数外,提供一致的核心接口。
优点: 让请求具有对象的特性,使客户能够采用不同的方式来处理请求对象,比如可以解除对象构造和对象使用的耦合,即实体解耦和时间解耦。
用途: 经常用于数据库事务操作,设备控制,多线程核心(Active Object)以及GUI的do/undo管理等。或者用于消除过多的条件分派。
组合模式: 经常用于Chain ofResponsibility中的请求封装;经常和Composite组合使用,提供统一的对待Command的途径,封装“一对多”的关系。
Iterator
意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
动机: 封装对对象的访问规则或者算法。本质:提供一致的遍历接口
适用性: 适用于为多个不同聚合类提供一致的遍历接口。
结构:抽象迭代器提供类似hasNext(),Next(), Remove()等接口,并由具体聚合类通过createIterator()创建具体的迭代器。
优点: 封装聚合类内部实现,提供一致对外接口
Interpreter
意图:给定一个语言, 定义它的文法的一种表示,并定义一个解释器, 该解释器使用自身定义表示来解释语言中的句子。
动机: 采用不同的对象来表示不同的文法,使文法易于组合使用。提供一种可选择的方式,将易变的组合逻辑推给客户代码。
结构:将每一个语法规则表示成一个类,方便于实现语言。
适用性:方便实现简单语言的解释器(常用yacc和lex工具来编写语言的解释器原型)
Mediator
意图:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可独立地改变它们之间的交互。
动机: 封装多个对象间的交互关系,解除耦合。
适用性: 适用于多个对象交互关系复杂且易变的情况。常用于协调GUI组件。
结构: 提供一个幕后类来统一管理不同对象间的交互关系。
优点: 封装多个对象间的交互关系,使客户更加容易编程。
Memento
意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。
动机: 保存对象状态,用于对象状态的回滚或重新构造。
适用性: 用于对象内部状态易变,且对象状态具有某种价值的场合。
结构: 使用简单的结构体即可完成任务。
优点: 提供保存对象的另一种选择。
缺点:存储耗时,常用语言自带的序列化(serialization)机制存储系统状态。
Observer
意图:定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。
动机: 封装对象间一对多的依赖关系,提供统一的管理点。
适用性: 所有具有一对多依赖关系且需要传递状态信息的对象管理。
结构: 使用双抽象结构(主题和观察者),一个抽象管理主题状态的通知行为(简化主题派生类的行为),一个抽象用于封装不同的状态观察者。
优点: 在具有多个观察者时,可简化状态通知部分的Hard-coding,并且易于扩展。
变化: 在状态比较复杂的情况下,一般采用某种约定的参数来提示观察者状态发生了什么样的变化,简化对象更新过程。
State
意图:允许一个对象在其内部状态改变时改变它的行为(外部行为)。对象看起来似乎修改了它所属的类。
动机: 分离状态机的逻辑和动作;或者是分离状态和行为(动作)。
结构:state定义所用具体状态的共同接口(事件接口);任何具体状态实现该相同接口。Context拥有所有状态对象。根据不同事件,context在不同状态对象中切换,从而改变自身行为。
优点:避免用户直接和状态交互;去除掉大量的条件语句;使系统更加易于扩展和维护。
变化:1)一般来讲,当状态改变是固定的,状态转换逻辑适合放在Context中;当转换更动态的时候,通常将状态转换逻辑放在State中,但这会使状态类之间产生依赖。总之,该决策决定了究竟哪个类是对修改封闭的(context或state)。2)如有多个Context,则可考虑共享所有的状态类。
补充:有限状态自动机(FSM)
有限状态自动机的两种表示方式: 状态迁移图(STD)和状态迁移表(STT)
状态迁移图(STD)至少由4部分组成。圆形表示状态;连接状态的箭头被称为迁移;迁移被用一个后面跟着动作名的时间做了标记,组成事件/动作对。类似数字电子中的状态转移图,本质上就是一个东西。
状态迁移表(STT)用一个表的形式来描述系统中状态的转移。表中的每一列表示一个状态迁移的完整过程。由下面4个部分组成一列:
起始状态 触发迁移的事件 终止状态 所执行的动作
使用状态迁移图(STD)和状态迁移表(STT)来描述自动状态机是非常有效的,并且非常容易检测那么未知的以及没有处理的状态转移情况。这对编程是很有帮助的,因为在实际编码中,非常容易遗漏非正常的状态转移,而这些遗漏往往是错误的根源。
实现有限状态自动机(FSM)的技术:
嵌套的switch/case语句,解释迁移表和State模式。
在简单其状态迁移中,使用嵌套的switch/case语句就足够的。在复杂情况下,使用State模式比较好。解释迁移表也容易实现,但不是这里讨论的目标,最主要的就是表的查找。
State的标准结构图:
State模式彻底的分离了状态机的逻辑和动作。动作是在Context类中实现的,而逻辑则是分布在State类的派生类中。这使得二者可以非常容易的独立变化,互补影响。例如,只要使用State的另一个派生类。就可以非常容易地在一个不同的状态逻辑中重用Context类的动作。此外,我们也可以在不影响State派生类逻辑的情况下创建Context的派生类来更改或者替换动作的实现。
可以使用状态机的地方:作为GUI中高层应用策略;GUI交互控制器;分布式处理等。几乎凡有状态存在的地方均可以考虑采用有限状态机。
Strategy
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。该模式使得算法的变化可独立于使用它的客户。
动机: 封装不同的算法。定制一组可以互换的算法族。
适用性: 同一个问题存在多种不同的解决方案。
结构: 为了满足算法之间的互换性,必须使用继承,并且遵循Liskov原则。
优点: 封装算法的变化。
组合模式: 经常在使用算法的基类Context中使用Template Method。如果Context的派生类中要求所使用的算法动态改变,还常常把Factory Method内嵌到派生类中来创建不同的算法类。
区别:策略模式和状态模式具有相同类图。策略模式是围绕可互换的算法来创建业务的,由Client自行决定具体策略。状态模式则通过改变对象内部状态帮助对象控制自己的行为。
Template Method
意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
动机: 消除子类中的重复代码,简化子类代码。
适用性: 当各个子类中,混杂着不变和可变的行为时,就可以使用该模式。将不变的行为放入父类中,子类只需定制可变的行为。这里不变还包括行为的执行顺序。
结构: 必须使用继承关系。
优点: 消除子类的重复行为。
组合模式: 经常和FactoryMethod、Strategy一起使用。在分解不变和可变行为时,还常可借助组合方法(ComposeMethod)和Collecting Parameter模式。
Visitor
意图:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
动机: 在不改变原类(一般是特定的数据结构)的情况下,增加新的功能接口。
适用性: 原有的类结构不能或者不容易改变;或者新加的功能不属于原类的职责范畴。
结构: 双重分派。增加一个Visitor类,并针对所有要访问的派生类增加单独的访问接口(在Visitor中)。本质上,Visitor模式中的两次分派形成一个功能矩阵。Visitor的接口名字和其接受的派生类类型分别是功能矩阵的两个变化轴。
优点: 使用Visitor模式,使程序中的数据结构(原类)独立于它的用途。
用途: 一般如果应用程序中存在有需要以多种不同方式进行解释的数据结构,就可以使用Visitor模式。比如使用Visitor模式来遍历所有的配置数据来初始化不同的应用程序子系统。最常见的应用:遍历大量的数据结构并产生不同类型的报表。
缺点:会破环组合类的封装。
Null Object
意图: 提供一个没有任何行为的对象。
动机: 消除代码中四处存在的无效对象判断
适用性: 只要对无效对象的判断逻辑多次出现时,就有引入Null Object的必要。
结构: Null Object肯定是作为派生类的一个种类出现,并用于取代没有合适派生类可用的情形。比如,对象放在Map中,查找可能无效。
优点: 消除对无效对象的判断逻辑,提供系统的可靠性。