参考资料
程杰.《大话设计模式》
《Head First 设计模式》
《设计模式:可复用面向对象软件的基础》
基本知识
基本概念
一个成熟的设计方法不仅要有设计模式,还可有其他类型的模式,如分析模式,用户界面设计模式,或者性能调节模式等等,但是设计模式是最主要的部分。
设计模式可以帮助你重新组织一个设计,同时还能减少以后的重构工作。
设计模式不是代码,而是讲述一些通用设计问题的解决思路。
库和框架提供了我们某些特定的实现,让我们的代码可以轻易的进行引用或调用,但这并不算是设计模式。
设计模式比库和框架的等级更高,告诉我们如何组织类和对象以解决某种问题。
模式是被“发现的”,而不是被创建的。
模式就是在某种情境下,针对某问题的某种解决方案。情境就是应用某个模式的情况,这应该是会不断出现的情况。问题就是你想在某情境下达到的目标,但也可以是在某情境下的约束。解决方案就是你所追求的一个通用的设计,用来解决约束、达到目标。
模式组合,模式通常被一起使用,并被组合在同一个设计解决方案中。
复合模式,是指一群模式被结合起来使用,以解决一般性问题,如MVC模式。
模式类目
模式类目定义
模式类目描述某个模式的意图、动机、可能应用该模式的地方,解决方案的设计以及使用后果。
模式类目内容
模式类目中的所有模式都是以一个“名称”开始的。
“名称”是模式中很重要的一部分,如果没有很好的名称,该模式就无法成为你和其他开发人员之间共享词汇的一部分。
“意图”简短地描述该模式的作用,你也可以把它看作是模式的定义。
“动机”给出了问题以及如何解决这个问题的具体场景。
“适用性”描述模式可以被应用在什么场合。
“结构”提供了图示,显示参与此模式的类之间的关系。
“参与者”描述在此类中所涉及到的类和对象在模式中责任和角色。
“协作”告诉我们参与者如何在此模式中合作。
“结果”描述采用此模式之后可能产生的效果,好与不好。
“实现”提供了在实现该模式时需要使用的技巧,以及你应该小心面对的问题。
“范例代码”提供代码片断。
“已知应用”描述应经在真实系统中发现的模式例子。
“相关模式”描述了此模式和其他模式之间的关系。
模式类目使用
首先,试着让自己熟悉所有的模式以及他们之间的关系。
其次,当需要一个模式的时候,就能大概知道是什么模式,然后参考模式类目中描述“动机”和“适用性”的小节,确认你的想法没错。
第三,浏览这个模式的“结果” 小节,确保该模式不会给你的设计带来意外的影响。
第四,一旦确定该模式可以被采用。你应该阅读“结构”小节,以了解类图,然后查看“参与者”小节,确定了解每一个类的角色,接下来,就可以开始进行设计,做出符合需求的一些更改。
第五,继续阅读“实例代码”小节,以确认你知道可能会遇到的所有较好的实现技巧。
模式分类
分类准则
(1)、目的准则,即模式是用来完成什么工作的。
分类 | 说明 | 备注 |
构建模式 | 考虑的是对象的创建。 | 此类模式都提供一个方法,将客户从所需要实例化的对象中解耦。 |
结构模式 | 处理的是类或对象的组合。 | 此类模式描述类和对象如何被组合已建立新的结构或新的功能。 |
行为模式 | 描述有关类或对象如何交互及分配职责。 | 此类模式主要关心对象之间的沟通与互联。 |
(2)、范围准则,指定模式主要是用于类还是用于对象。
分类 | 说明 | 备注 |
类模式 | 描述类之间的关系如何通过继承来定义。 | 类模式的关系是在编译时建立的。 |
对象模式 | 描述对象之间的关系,而且主要是利用组合定义。 | 对象模式的关系通常在运行时建立,而且更加动态、更有弹性。 |
从某种意义上来说,几乎所有模式都使用继承机制,所以“类模式”只指那些集中于处理类间关系的模式,而大部分模式都属于对象模式的范畴。
模式类型
分类 | 类模式 | 对象模式 |
构建模式 | 使用继承机制将对象创建工作放到子类中。 | 将对象的创建工作委托给另一个对象。 |
结构模式 | 使用继承机制来组合类。 | 描述了对象的组装方式。 |
行为模式 | 使用继承描述算法和控制流。 | 描述一组对象怎样协作完成单个对象所无法完成的任务。 |
分类 | 类模式 | 对象模式 |
创建型模式 | 工厂方法 | 抽象工厂、生成器、原型、单件 |
结构性模式 | 适配器 | 适配器、桥接、组合、装饰者、外观、享元、代理 |
行为模式 | 解释器、模板方法 | 职责链、命令、迭代、中介者、备忘录、观察者、状态、策略、访问者 |
类型说明
(1)、构建模式
构建模式抽象了对象实例化过程。该模式允许你用结构和功能差别很大的“产品”对象配置一个系统,配置可以是静态的(即在编译时指定),也可以是动态的(在运行时)。
构建模式有以下特点:
<1>、第一,将关于该系统使用哪些具体的类的信息封装起来。
<2>、第二,隐藏了这些类的实例是如何被创建和放在一起的。整个系统关于这些对象所知道的是由抽象类所定义的接口。
构建模式常用方法:
<1>、使用继承机制,生成创建对象的类的子类。
主要缺点是,为了改变产品类就可能需要创建一个新的子类,这样的改变可能是级联的(cascade)。例如,如果产品的创建者本身是由一个工厂方法创建的,那么你也必须重定义它的创建者。
<2>、使用组合机制,根据传入的参数创建对象。
定义一个负责产品对象创建的类,并将它作为系统的参数。所有该类模式都涉及到创建一个新的负责产品对象创建的“工厂类”。
(2)、结构模式
结构模式涉及到如何组合类和对象以获得更大的结构。
结构模式常用方法:
<1>、使用继承机制,对接口和实现进行组合。
这一模式有助于多个独立开发的类库协同工作。
第一,采用多重继承方法将两个以上的类组合成一个类,这个组合类包含所有父类的性质。
第二,采用类形式的适配器模式。适配器使得一个接口与其他接口兼容,从而给出多个不同接口的统一抽象。
<2>、使用组合机制,对一些对象进行组合。
因为可以在运行时刻改变对象组合关系,所以对象组合方式具有更大的灵活性,而这种机制用静态类组合是不可能实现的。
(3)、行为模式
行为模式涉及到算法和对象间职责的分配,该模式不仅描述对象或类的模式,还描述它们之间的通信模式。
行为模式常用方法:
<1>、使用继承机制,在类间分派行为。
<2>、使用组合机制,在对象间分派行为。
一些行为对象模式描述了一组对等的对象怎样相互协作以完成其中任一个对象都无法单独完成的任务。其他的行为对象模式常将行为封装在一个对象中并将请求指派给它。
模式定义
分类 | 模式 | 简述 |
构建(creational) | 抽象工厂 (Abstract Factory) | 提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。 |
生成器 (Builder) | 将一个复杂对象的构建流程与它的构建细节分离分离,使得同样的构流过程可以创建不同的对象。 | |
工厂方法 (Factory Method) | 定义一个用于创建对象的接口,让子类决定将哪一个类实例化。该模式使一个类的实例化延迟到其子类。 | |
原型 (Prototype) | 用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。 | |
单件 (Singleton) | 保证一个类仅有一个实例,并提供一个访问它的全局访问点。 | |
结构(structural) | 适配器 (Adapter) | 将一个类的接口转换成客户希望的另外一个接口。该模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 |
桥接 (Bridge) | 将抽象部分与它的实现部分分离,使它们都可以独立地变化。 | |
组合 (Composite) | 将对象组合成树形结构以表示“部分-整体”的层次结构。该模式使得客户对单个对象和复合对象的使用具有一致性。 | |
装饰者 (Decorator) | 动态地给一个对象添加一些额外的职责。就扩展功能而言,装饰者模式比生成子类方式更为灵活。 | |
外观 (Facade) | 为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。 | |
享元 (Flyweight) | 运用共享技术有效地支持大量细粒度的对象。 | |
代理 (Proxy) | 为其他对象提供一个代理以控制对这个对象的访问。 | |
行为(behavioral) | 职责链 (Chain of Responsibility) | 为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。 |
命令 (Command) | 将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。 | |
解释器 (Interpreter) | 给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。 | |
迭代 (Iterator) | 提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。 | |
中介者 (Mediator) | 用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。 | |
备忘录 (Memento) | 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。 | |
观察者 (Observer) | 定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。 | |
状态 (State) | 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。 | |
策略 (Strategy) | 定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法的变化可独立于使用它的客户。 | |
模板方法 (Template Method) | 定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。 | |
访问者 (Visitor) | 表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。 |
简单工厂模式:
优点是在工厂类中包含了必要的逻辑判断,根据客户端的选择条件动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。缺点是当增加新功能时,除了编写新功能类,还必须修改原有的工厂类,这就等于说,我们不但对扩展开放了,对修改也开放了,这就违背了开放-封闭原则。
工厂方法模式:
客户端需要决定实例化哪一个工厂类来创建产品对象,选择判断的问题还是存在的,也就是说,工厂方法模式把简单的内部逻辑判断移到客户端来进行。你想要加功能,本来是要修改工厂类的,但现在只需要修改客户端。
在工厂方法模式中,工厂类和产品类是两个平行类层次。两个类层次为什么是平行的,因为他们都有抽象类,而抽象类都有许多具体的子类,每个子类都有自己特定的实现。
抽象工厂模式:
最大的好处便是易于交换产品系列,由于具体工厂类在一个应用中只需要在初始化的时候出现一次,这就使得改变一个应用的具体工厂变得非常容易,他只要改变具体工厂即可使用不同的产品配置。
静态工厂:
利用静态方法定义一个简单的工厂,这是很常见的技巧,常被称为静态工厂。为何使用静态方法?因为不需要对该工厂进行实例化,但该技巧也有缺点,即不能通过继承来改变创建方法的行为。
原型模式:
一般在初始化的信息不发生变化的情况下,克隆是最好的方法。这就隐藏了对象创建的细节,又对性能是大大的提高。
浅复制:复制对象的所有变量都含有与原来对象的变量相同的值,而所有的对其他对象的引用都仍然指向原有的被引用对象。
深复制:复制对象的所有变量都含有与原来对象的变量相同的值,而所有的对其他对象的引用都重新指向被复制出的引用对象,而不是原有的被引用对象。
在一些特定场合,会经常涉及深复制或浅复制,比如说,数据集对象DataSet,它就有clone方法和Copy方法,clone方法用于复制DataSet的结构,但不复制DataSet的数据,实现了原型模式的浅复制。Copy方法不但复制结构,也复制数据,其实就是实现了原型模式的深复制。
单件模式:
用来管理共享的资源,有一些对象其实我们只需要一个,比如线程池、缓存、日志对象等。单件模式也给了我们一个全局访问点,和全局变量一样方便,又没有全局变量的缺点。
全局变量的缺点,如果将对象赋值给一个全局变量,那么你必须在程序一开始就创建好对象,万一这个对象非常耗费资源,而程序在这次的执行过程中又一直没用到它,不就形成浪费了吗。全局变量可以提供全局访问,但是不能确保类只有一个实例,而且使用全局变量也会变相鼓励开发人员创建更多的全局变量,从而造成命名空间的污染。
在多线程中使用单件模式时,注意同步处理,以防止生成多个实例。你应当适度的优化代码,以防止同步一个方法造成的程序执行效率下降的情况。
装饰模式:
为已有功能动态地添加更多功能的一种方式。装饰模式是的优点就是把类中的装饰功能从类中搬移出去,这样可以简化原有的类。更大的好处就是有效的把类的核心职责和装饰功能区分开了,而且可以去除相关类中重复的装饰逻辑。
当系统需要新功的时候,是向旧的类中添加新的代码。这些新加的代码通常装饰了原有类的核心职责或主要行为,他们在主类中加入了新的字段、新的方法和新的逻辑,从而增加了主类的复杂性,而有时候这些新加的东西可能仅仅是为了满足一些只在某种特定情况下才会执行的功能需求。
外观模式:
外观模式没有封装子系统的类,外观模式只提供简化的接口,所以如果客户觉得有必要,依然可以使用子系统的类,这是外观模式的一个很好的特征:提供简化接口的同时,依然将系统完整的功能暴露出来,以供需要的人使用。在以下情况时应考虑使用外观模式。
第一,在设计初期阶段,应该有意识的将不同的两个层分离,比如经典的三层架构,就需要考虑在数据访问层、业务逻辑层和表示层的层与层之间建立外观类,这样可以为复杂的子系统提供一个简单的接口,使得耦合大大降低。
第二,子系统往往因为不断的重构演化而变得越来越复杂,大多说的模式使用时也会产生很多很小的类,这本是好事,但也给外部调用它们的用户程序带来了使用上的困难,增加外观类可以提供一个简单的接口,减少它们之间的依赖。
第三,在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了,但因为它包含非常重要的功能,新的需求开发必须要依赖于它。此时你可以为新系统开发一个外观类,来提供设计粗燥或高度复杂的遗留代码的比较清晰简单的接口,让新系统与外观对象交互,而外观对象与遗留代码交互所有复杂的工作。
代理模式:为其他对象提供一种代理以控制对这个对象的访问。
第一,远程代理。使用远程代理对象就像使用实际对象一样,但其实际幕后是它利用网络和一个真实的处理对象进行沟通。
第二,虚拟代理。控制访问创建开销大的资源。比如说打开一个很大的HTML网页时,里面可能有很多的文字和图片。但你还是可以很快打开它,此时你所看到的是所有的文字,但图片却是一张一张的下载以后才能看到,那些未打开的图片框,就是通过虚拟代理来代替了真实的图片,此时代理存储了真实图片的路径和尺寸。
第三,保护代理。基于权限控制对资源的访问。
第四,智能指引。指当调用真实的对象时,代理处理另外一些事。如计算真实对象的引用次数,这样当该对象没有引用时,可以自动释放它。
通过工厂方法返回包装真实对象的代理,此时客户调用时不知道也不在乎他使用的是代理对象还是实际对象。
策略模式:
是用来封装算法的,但在实践中,我们可以用它来封装几乎任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。
模板方法模式:
当我们要完成在某一细节层次上步骤序列或方法过程保持一致,而在其中的个别步骤则在更详细的层次上的实现有所不同,则通常考虑用模板方法模式来处理。当不变的行为和可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复出现。我们通过模板方法模式把这些行为搬移到单一的地方,这样就帮助子类摆脱重复的不便行为的纠缠。
在模板方法模式中,我们也可以有“默认不做事的方法”,我们称这种方法为“Hook”(钩子),子类可以视情况决定是不是要覆盖它们。钩子是一种被声明在抽象类中的方法,但只有空的或者默认的实现。钩子的存在可以让子类有能力对算法的不同点进行挂钩,要不要挂钩,由子类决定。当子类必须实现算法中的某个步骤时,使用抽象方法声明该步骤。如果算法中的该步骤是可选的,就用钩子。
为了达到“抽象方法数目尽量少”的目的,可以让算法不要切割的太细,但如果步骤太少的话,会比较没有弹性,所以要看情况折衷。
观察者模式:
将一个系统分割成一系列相互协作的类有一个很不好的副作用,那就是需要维护相关对象的一致性。我们不希望为了维持一致性而使各类紧密耦合,这要会给维护、扩展和重用都带来不便。观察者模式所作的工作其实就是解除耦合,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使各自的变化都不会影响另一边的变化。
观察者模式的缺点是如果无法继承抽象观察者这样的接口,则通知功能就无法实现。此时可以使用事件委托机制,数据对象通过维护一个事件方法列表的方式来实现通知功能,从而兼容不能使用观察者接口的对象。
状态模式:
将每个状态的相关行为都局部化到它自己的类中,那么每个状态只要实现它自己的动作就可以了。此时调用方只需要将请求委托给代表当前状态的状态对象。由于所有与状态相关的代码都存在于某个具体状态类中,所以通过定义新的子类可以很容易地增加新的状态和转换。
状态模式的目的是“防止在多个操作中出现状态检查”,具体来说就是为了消除庞大或复杂的条件分支语句(尤其是该条件语句在多处被调用时),大的分支判断会使程序难以修改和扩展,任何改动和变化都是致命的。如果这个状态判断很简单并且只在一处声明,则没必要使用“状态模式”了。
状态模式增加了类的数目,这就是为了获取弹性而付出的代价。一般使用时会将这些额外的状态类全部隐藏起来。
相似的状态要不要合并。若果你将相似状态合并,那么你等于将两个状态用一个状态类来代表,这样做你牺牲了状态类的清晰易懂换来一些重复代码的减少。但这违背了“一个类,一个责任”的设计原则。假如一个状态为临时的,你又该怎么办呢。所以,有时也必须折衷考虑。
适配器模式:
主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况。通常是在软件开发后期或维护期使用,并且双方都不太容易修改的时候使用。如果能事先预防接口不同的问题,不匹配问题就不会发生;在有小的接口不统一问题发生时,及时重构,问题不止于扩大;只有碰到无法改变原有设计和代码的情况时,才考虑适配。事后控制不如事中控制,事中控制不如事前控制。
桥接模式:
传统意义上讲,父类是抽象的集合,子类为具体实现。如“车-〉轿车、JEEP、商务车”等。但是,我们常常遇到的情况是对象有多种实现方式,如“车-〉轿车、JEEP、商务车”、“车-〉大众、QQ、奔驰、宝马”,“大众-〉轿车、JEEP”等。
由于实现的方式有多种,而每一种实现又都有可能变化,桥接模式的核心意图就是把这些实现独立出来,让它们各自独立地变化,减少它们之间的耦合。这就使得每种实现的变化不会影响其他实现。从而达到应对变化的目的。
桥接模式适合使用在需要跨越多个平台的图形和窗口系统上。
命令模式:
发送者发送命令给调度者,调度者通知接收者执行命令,接收者处理命令具体内容。
职责链模式:
接收者和发送者都没有对方的明确信息,且链中的对象自己也并不知道链的结构,结果是职责链可简化对象的相互连接,他们仅需要保持一个指向其后继者的引用,而不需要它所有的候选者。例如,邮寄一封信。
职责链模式经常被使用在窗口系统中,处理鼠标和键盘之类的事件。
中介者模式:
尽管将一个系统分割成许多对象通常可以增加其可复用性,但是对象间相互连接的激增又会降低其可复用性。因为大量的连接使得一个对象不可能没有其他对象的支持下工作,系统表现为一个不可分割的整体,所以,对系统的行为进行任何较大的改动就困难了。
通过中介者对象,可以将系统的网状结构变成以中介者为中心的星形结构,每个具体对象不再通过直接的联系与另一个对象发生相互作用,而是通过“中介者”对象与另一个对象发生相互作用。中介者对象的设计,使得系统的结构不会因为新对象的引入造成大量的修改工作。中介者也仅是用于真正需要解耦的对象集合,一般用于不同模块间的通信。
由于把对象如何协作进行了抽象,将中介作为一个独立的概念并将其封装在一个对象中,这样关注的对象就从对象各自本身的行为转移到它们之间的交互上来,也就是站在一个更宏观的角度去看待系统。
由于中介者控制了集中化,于是就把交互复杂性变为了中介者的复杂性,这就使得中介者变得比任何一个对象都复杂。中介者模式的优点来自集中控制,其缺点也是它。
中介者常常被用来协调相关的GUI组件。Windows应用程序的form就是典型的中介者,比如计算器程序,它上面有菜单、文本、按钮等,这些控件之间的交互都是通过Form这个中介者来实现。
解释器模式:
可以理解为解释并执行。通常当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树时,可使用解释器模式。解释器模式为文法中的每一规则至少定义一个类,当语法规则的数目太大时,解释器模式可能会变得非常复杂,且难以管理和维护,此时使用其他的技术如解析器/编译器可能更合适。
访问者模式:
适用于数据结构相对稳定的系统,它把数据结构和作用于结构上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化。
很多系统可以按照算法和数据结构分开,如果这样的系统有比较稳定的数据结构,又有益于变化的算法的话,使用访问者模式可以使算法操作的增加变得容易。
访问者模式的优点就是增加新的操作很容易;缺点是增加新的数据结构很困难。
命令模式:
命令模式中包括客户(具体请求者)、调用者、接收者(具体执行者)等角色。最简单的情况下,客户直接要求接收者执行某些操作,此时客户、接收者高度绑定,增加一个调用者角色以便将客户和接收者解耦。具体流程如下:
(1)、客户负责创建命令对象。命令对象包含了接收者的一组动作,动作和接收者在命令对象中被绑在一起。命令对象提供了一个方法,这个方法封装了具体动作,调用这个方法就会调用接收者的这些操作。
(2)、客户将命令发送给调用者。
(3)、客户要求调用者执行命令。
注意:一旦命令被加载到调用者,该命令可以被使用或丢弃,或者可以被保留以便多次使用。
迭代器模式:
把游走的任务放在迭代器上,而不是数据集合(或聚合)对象上。这样简化了集合的接口和实现,也让责任各得其所。
有一个共同的创建迭代器对象接口供所有集合对象使用,这对客户是很方便的,将客户代码从集合对象的实现进行解耦。
外部迭代器必须维护它在遍历中的位置,以便外部客户可以通过调用HasNext和Next来驱动遍历。对于组合模式中使用的迭代器,可以使所用堆栈来维护组合递归结构的位置。
组合模式:
以图形用户界面为例,你经常会看到一个顶层组件(如Frame、Form、Panel等)包含很多其他组件(如文本框、按钮、滚动条等),但是当你显示它的时候,你认为它是一个整体。你告诉顶层组件显示,然后就可以放手不管,由顶层组件负责显示所有相关部分。
如果一个组合结构很复杂或则遍历的代价太高,那么实现组合结点的缓存很有帮助。
模式比较
状态模式,一个对象将请求委托给一个描述当前状态的状态对象来处理。
策略模式,一个对象将一个特定的请求委托给一个描述请求执行策略的对象。
访问者模式,对象结构的每个元素上的操作总是被委托到访问者对象。
装饰者模式,包装一个对象,并提供额外的行为。
外观模式,包装一组对象,以简化他们的接口。
代理模式,包装一个对象,并控制对它的访问。
适配器模式,包装一个对象,并提供不同的接口。
生成器模式,封装一种对象生成策略。
迭代模式,封装一种集合遍历策略。
策略模式,封装一系列可互换的算法或执行策略。
模板方法模式,封装一个算法大纲。
访问者模式,封装一个数据访问策略。
职责链模式,封装具有相同职责的对象。
命令模式,封装一个请求或操作。
解释器模式,封装一种文法解释方式。
中介者模式,封装对象间的通信协议。
备忘录模式,封装一个对象在某个特定时刻的内部状态。
状态模式,封装一个对象状态相关的所有行为。
访问者模式,封装分布于多个元素之间的访问操作。
抽象工厂模式,将对象调用者与具体对象解耦。
命令模式,将请求对象与执行对象解耦。
外观模式,将子系统与调用者解耦。
中介者模式,将相互交互或引用的对象解耦。
观察者模式,将数据对象与数据访问对象解耦。
职责链模式,将请求对象与具体执行对象解耦。
策略模式,将算法类与算法使用类解耦。
装饰者模式,将功能解耦。每个装饰对象只关心自己的功能。
桥接模式,将抽象对象和实现对象组合起来提高设计可维护性、可扩充性。
职责链模式,将多个处理对象组合起来完成一个请求操作。提供在数目可变的对象间进行通信的机制。
组合模式,将组合对象与基元对象组合起来作为一个整体来访问。
装饰者模式,将具体对象与装饰对象组合起来实现新功能。其焦点在于如何动态的组合对象以获取功能。
观察者模式,将被观察对象与观察对象组合起来实现数据传递。
策略模式,将多个策略组合起来以实现新策略。
抽象工厂模式,通过对象工厂创建具体对象。可以产生多个类的对象
生成器模式, 通过对象生成器创建具体对象。对象生成器使用一个相对复杂的协议,逐步创建一个复杂对象。
工厂方法模式,通过子类创建具体对象。
原型模式,通过原型拷贝创建具体对象。因为原型对象负责返回产品对象,所以工厂对象和原型对象实际是同一个对象。
享元模式,说明了如何生成很多较小的对象。
外观模式,描述了如何用单个对象表示整个子系统。外观对象的职责是将消息转发给它所表示的对象。
外观模式,定义一个新的接口。
适配器模式,复用一个原有的接口。
装饰者模式,不改变接口,仅添加一些额外的职责。
享元模式,简化对象接口,以便重复利用对象。
适配器模式,将一个接口转成另一个接口。
桥接模式,分离抽象接口与实现接口。
组合模式,设计一个组合接口。
代理模式,使用代理接口控制对具体对象的访问。
模板方法模式:由子类决定如何实现算法中的步骤。
工厂方法模式:由子类决定实例化哪个具体类。
工厂方法模式是模板方法模式的一种特殊版本。
装饰者模式和代理模式很像,但它们的目的不一样。装饰者模式为对象增加行为,而代理模式是控制对象的访问。
适配器模式会改变对象的接口,而代理模式则实现相同的接口。
简单工厂模式和工厂方法模式都是集中封装了对象的创建过程,使得要更换对象时,不需要做大的改动就可实现,降低客户程序与具体产品对象的耦合。工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用动态性,工厂方法模式克服了简单工厂违背开放-封闭原则的缺点,又保持了封装对象创建过程的优点。但又带来新的问题,即由于每加一个产品,就需要加一个产品工厂的类,增加了额外的开发量。
适配器模式,解决两个已有接口之间不匹配的问题。一般在类已经设计好后实施。它不考虑这些接口是怎样实现的,也不考虑它们各自可能会如何演化。这种方式不需要对两个独立设计的类中的任一个进行重新设计,就能够使它们协同工作。
桥接模式,对抽象接口与它的(可能是多个)实现部分进行桥接。一般在设计类之前实施,
组合模式和装饰模式基于递归组合来组织可变数目的对象。
组合模式,该模式旨在构造类,使多个相关的对象能够以统一的方式处理,而多重对象可以被当作一个对象来处理。它重点不在于修饰,而在于表示。
装饰模式,该模式旨在使你能够不需要生成子类即可给对象添加职责。这就避免了静态实现所有功能组合,从而导致子类急剧增加。
装饰模式和代理模式描述了怎样为对象提供一定程度上的间接引用,在对象的实现部分保留了指向另一个对象的指针,并向这个对象发送请求。
装饰模式,组件仅提供了部分功能,而一个或多个装饰组件负责完成其他功能。装饰模式适用于编译时不能(至少不方便)确定对象的全部功能的情况。这种开放性使递归组合成为装饰模式中一个必不可少的部分。
代理模式,构建一个对象并为用户提供一致的接口。该模式不能动态地添加或分离职责,它也不是为递归组合而设计的。它的目的是,当直接访问一个实体不方便或不符合需要时,为这个实体提供一个替代者。在代理模式中,实体定义了关键功能,而代理提供(或拒绝)对它的访问。代理模式强调一种关系(代理对象与它的实体之间的关系),这种关系可以静态的表达。
命令模式和备忘录模式定义一些可作为令牌到处传递的对象,这些对象将在稍后被调用。令牌都可以有一个复杂的内部表示,但客户并不会意识到这一点。
命令模式,令牌代表一个请求。
备忘录模式,令牌代表在一个对象在某个特定时刻的内部状态。
模板方法,是一个算法的抽象定义,它逐步地定义该算法。算法的每一步调用一个抽象操作或一个原语操作,子类负责实现抽象操作。
解释器模式,将一个文法表示为一个类层次,并为类层次中的每个类定义一个解释操作(对于脚本执行引擎,每个类的解释操作就是相应代码执行操作;对于脚本格式化引擎,每个类的解释操作就是相应代码格式化操作)。
观察者模式,通过引入Observer和Subject对象来分布通信。在该模式中,不存在封装一个约束的单个对象,而必须是由Observer和Subject对象相互协作来维护这个约束。通信模式由观察者和目标连接的方式决定:一个目标通常有多个观察者,并且有时一个目标的观察者也是另一个观察者的目标。
中介者模式,封装了其他对象间的通信。该模式的目的是集中而不是分布,它将维护一个约束的职责直接放在一个中介者中。
策略模式和模板方法模式都封装算法,一个用组合,一个用继承。
策略模式,定义一个算法家族,并让这些算法可以互换。所以客户可以轻易使用不同的算法。
模板方法模式,定义一个算法大纲,并让子类实现其中的某些步骤。父类对算法有更多的控制权。
状态模式与策略模式很相近,一个是策略切换,一个是状态切换。
策略模式,是除了继承外的一种弹性替代方案,通过组合不同的策略对象来改变行为。客户需要了解当前是哪个策略在工作。
状态模式,是不用在上下文中放置许多条件判断的替代方案,通过简单地改变状态对象来改变行为。客户根本不需要了解当前是哪个状态在工作。
模式关系
除了少数例外情况,各个行为设计模式之间是相互补充和相互加强的关系。例如,一个“职责链”中的类可能包括至少一个“模板方法”的应用,该“模板方法”可使用原语操作确定该对象是否应处理该请求并选择应转发的对象。“职责链”可以使用“命令模式”将请求表示为对象。“解释器”可以使用“状态模式”定义语法分析上下文。“迭代器”可以遍历一个“组合对象”,而访问者可以对“组合”的每一个元素进行一个操作。
行为模式也与能其他模式很好地协同工作。例如,一个使用“组合模式”的系统可以使用一个“访问者”对该复合的各成分进行一些操作。它可以使用“职责链”使得各成分可以通过它们的父类访问某些全局属性。它也可以使用“装饰”对该复合的某些部分的这些属性进行改写。它可以使用“观察者模式”将一个对象结构与另一个对象结构联系起来,可以使用“状态模式”使得一个构件在状态改变时可以改变自身的行为。“组合对象”本身可以使用“生成器模式”中的方法创建,并且它可以被系统中的其他部分当作一个“原型”。
设计良好的面向对象式系统通常有多个模式镶嵌在其中,但其设计者却未必使用这些术语进行思考。然而,在模式级别而不是在类或对象级别上的进行系统组装可以使我们更方便地获取同等的协同性。
模式使用
应对变化
设计系统时应考虑未来可能发生的变化,如新增需求、需求变更、类的重定义和实现等等。一个不考虑系统变化的设计在将来就有可能需要重新设计,而重新设计将会影响软件系统的许多方面,并且未曾料到的变化总是会付出巨大的代价。
下面阐述了一些导致重新设计的问题,以及解决这些问题的设计模式:
(1)、通过显式地指定一个类来创建对象。
在创建对象时指定类名将使你受特定实现的约束而不是特定接口的约束,这会使未来的变化更复杂,要避免这种情况,应该间接地创建对象。
相关模式:抽象工厂,工厂方法,原型。
(2)、对特殊操作的依赖。
当你为请求指定一个特殊的操作时,完成该请求的方式就固定下来了,为避免把请求代码写死,你应该将请求的方法封装起来,这样你将可以在编译时刻或运行时刻很方便地改变响应请求的方法。
相关模式:职责链,命令。
(3)、对硬件和软件平台的依赖。
外部的操作系统接口和应用编程接口(API)在不同的软硬件平台上是不同的,依赖于特定平台的软件将很难移植到其他平台上,甚至都很难跟上本地平台的更新,所以设计系统时限制其平台相关性就很重要了。
相关模式:抽象工厂,桥接。
(4)、对对象表示或实现的依赖。
知道对象怎样表示、保存、定位或实现的客户在对象发生变化时可能也需要变化,对客户隐藏这些信息能阻止连锁变化。
相关模式:抽象工厂,桥接,备忘录,代理。
(5)、算法依赖。
算法在开发和复用时常常被扩展、优化和替代,依赖于某个特定算法的对象在算法发生变化时不得不变化,因此有可能发生变化的算法应该被孤立起来。
相关模式:生成器,迭代,策略,模板方法,访问者。
(6)、紧耦合。
紧耦合的类很难独立地被复用,因为它们是互相依赖的。紧耦合产生单块的系统,要改变或删掉一个类,你必须理解和改变其他许多类。这样的系统是一个很难学习、移植和维护的密集体。
松散耦合提高了一个类本身被复用的可能性,并且系统更易于学习、移植、修改和扩展。设计模式使用抽象耦合和分层技术来提高系统的松散耦合性。
相关模式:抽象工厂,命令,外观,中介者,观察者,职责链。
(7)、通过生成子类来扩充功能。
通常很难通过定义子类来定制对象。每一个新类都有固定的实现开销(初始化、终止处理等)。定义子类还需要对父类有深入的了解。
一般的对象组合技术(如委托)是除类继承之外组合对象行为的另一种灵活方法。新的功能可以通过以新的方式组合已有对象,而不是通过定义已存在类的子类的方式来实现。但是,过多使用对象组合会使设计难于理解。在许多设计模式产生的设计中,你可以定义一个子类,且将它的实例和已存在实例进行组合来引入定制的功能。
相关模式:桥接,职责链,组合,装饰,观察者,策略。
(8)、不能方便地对类进行修改。
有时你不得不改变一个难以修改的类。也许你需要源代码而又没有(对于商业类库就有这种情况),或者可能对类的任何改变会要求修改许多已存在的其他子类。设计模式提供在这些情况下对类进行修改的方法。
相关模式:适配器,装饰,访问者。
注意事项
设计模式只是一种工具,只有在需要的时候才使用这种工具。所谓“用模式思考”,是指能够根据设计,体会在什么地方模式能自然适用,在什么地方模式则不能适用。
1、保持简单。
当你设计时,尽可能地用最简单的方式解决问题。你的目标应该是简单,而不是“如何在这个问题中应用模式”。使用模式是为了要让你的设计简单且有弹性。
2、不要滥用
要使用设计模式,你就必须要考虑模式对你的设计中其他部分所造成的后果。
3、正确使用
何时使用模式?当你在设计的时候,如果确定在你的设计中可以利用某个模式来解决某个问题,那么就是使用这个模式,如果有更简单的解决方案,那么在决定使用模式之前应该先考虑这个方案。
有一种情况,即使有更简单的解决方案,你仍然想要使用模式,这种情况就是:你预期系统在未来会发生改变。但是务必要确定一件事,加入模式是要应对可能发生的改变,而不是假想的改变。
4、抗拒诱惑
开发人员天生就热爱创建漂亮的架构已应对任何方向的改变。要抗拒这样的诱惑。如果你今天在设计中有实际的需要去支持改变,就放手采用模式处理这个改变。然而,如果说理由只是假想的,就不要添加这个模式,因为这只会将你的系统越搞越复杂,而且很可能你永远都不会需要它。
模式可能带来复杂性,如果没有必要,我们绝不需要这样的复杂性。
设计原则
原则:封装导致变化的部分
意义:让系统中某部分的改变不会影响到其它方面。
原则:单一职责原则(SRP),一个类应该只有一个引起变化的原因。
意义:尽量让每个类保持单一责任。
说明:
如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或压制这个类完成其它职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。
遵守该原则的类容易具有很高的凝聚力,并且比承担许多责任的低内聚类更容易维护。当一个模块或一个类被设计成只支持一组相关功能时,我们说它具有高内聚;反之,当被设计成支持一组不相关的功能时,我们说它具有低内聚。
编程时,我们要在类的职责分离上多思考,这样你的代码才是真正的易维护、易扩展、易复用、灵活多样。
原则:针对接口编程,而不是针对实现编程。
意义:隔离掉以后系统可能发生的一大堆改变。
说明:
(1)、不将变量声明为某个特定的具体类的实例对象,而是让它遵从抽象类所定义的接口。
(2)、使用创建型模式,确保你的系统是采用针对接口而不是针对实现的方式书写的。
原则:合成/聚合复用原则,多用组合,少用继承。
意义:让功能的实现具有更大的灵活性。
说明:
该原则的好处是,优先使用对象的合成/聚合将有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。
聚合表示一种弱的“拥有”关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。例如,大雁和雁群。
合成则是一种强的“拥有”关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样。例如,翅膀与大雁。
原则:开放-封闭原则,对扩展开放,对修改关闭。
说明:
在做设计的时候,时刻要考虑,尽量让这个类足够好,写好后就不要去修改了,如果有新需求来,我们增加一些类就完事了,原来的代码能不动则不动。在软件设计模式中,这种不能修改,但可以扩展的思想就是开放-封闭原则的精神所在。
开放-封闭原则使我们面对需求的改变却可以保持相对稳定,从而使得系统可以在第一个版本以后不断推出新的版本。
在我们最初编写代码时,假设变化不会发生,当变化发生时,我们就创建抽象来隔离以后发生的同类变化,即先重构然后再实现功能。
一般没有必要在设计的每个部分都遵守“开放-封闭原则”,这样会浪费大量时间。毕竟无论模块是多么的“封闭”,都会存在一些无法对之封闭的变化。既然不可能完全封闭,设计人员只需要把注意力集中在设计中最有可能改变的地方,然后应用“开放-封闭原则”。
原则:依赖倒置原则,要依赖抽象,不要依赖具体类。
说明:
若对象B的修改必然影响对象A,则对象A依赖于对象B。通过加入对象C使得“对象A-〉对象B”变为“对象A-〉对象C〈-对象B”,即对象B依赖倒置。
原则:里氏代换原则(LSP),子类型必须能够替换掉他们的父类型。
说明:
只有当子类可以替换掉父类,软件的功能不受影响时,父类才能真正被复用,而子类也能够在父类的基础上增加新的行为。正是由于子类型的可替换性才使得使用父类类型的模块在无需修改的情况下就可以扩展。
对于内部来说,依赖无法倒转,父类必须依靠子类来实现具体功能。但对于外部,当父类可以完全表示子类时,依赖倒转成立,即依赖父类。
原则:迪米特法则(也叫最少知识原则),只和你的密友谈话。
意义:尽量减少对象间的交互。
说明:
这个原则告诉我们,在系统设计中,尽量减少对象间的交互,不要让太多的类耦合在一起,免得修改系统中的一部分会影响到其它部分。迪米特法则的根本思想是强调了类之间的松耦合。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。
如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。
附件资料
*MVC模式
MVC模式(Model-View-Controller 模型-视图-控制器)是由数个模式结合起来而形成的新模式,一般被用于解决一些设计问题。
模型是整个系统的核心,封装了数据、状态和处理逻辑。视图用来呈现模型,其通常直接从模型中取得它所需要显示的状态与数据。控制器定义视图对用户输入的响应方式。
对视图的操作会被转到控制器,控制器操作模型,模型通知视图状态的改变。
MVC使用很多设计模式,但其主要关系还是由“观察者模式”、“组合模式”和“策略模式”等三个设计模式给出的。
(1)、视图和控制器实现了经典的策略模式。
MVC允许你在不改变视图外观的情况下改变视图对用户输入的响应方式,而该响应方式被封装在控制器对象中,所以视图使用一个控制器实例来实现一个特定的响应策略,要实现不同的响应策略只需要替换不同种类的控制器实例即可。
视图只关心系统中可视的部分,对于任何界面行为,都委托给控制器处理。使用策略模式可以让让视图和模型之间的关系解耦,因为控制器负责和模型交互来传递用户的请求,对于工作怎么完成,视图毫不知情。
(2)、视图内部使用“组合模式”管理窗口、按钮及标签等显示组件。每个显示组件不是组合节点(如窗口),就是叶结点(如按钮)。当控制器告诉视图更新时,只需要告诉视图最顶层的组件即可,组合会处理其余的事。
(3)、模型利用“观察者模式”让控制器和视图可以随最新的状态改变而更新。使用观察者模式可以让模型完全独立于视图和控制器,同一个模型可以使用不同的视图,甚至可以使用多个视图,并且为模型创建新的视图无须重写模型。
*框架
框架(Framework)是构成一类特定软件可复用设计的一组相互协作的类。你可以定义框架抽象类的应用相关的子类,从而将一个框架定制为特定应用。例如,一个框架能帮助建立适合不同领域的图形编辑器,另一个框架也许能帮助你建立针对不同程序设计语言和目标机器的编译器。
框架规定了你的应用的体系结构。它定义了整体结构、类和对象的分割、各部分的主要责任、类和对象怎么协作,以及控制流程。框架预定义了这些设计参数,以便于应用设计者或实现者能集中精力于应用本身的特定细节。框架记录了其应用领域的共同的设计决策。因而框架更强调设计复用,尽管框架常包括具体的立即可用的子类。
你不仅可以更快地建立应用,而且应用还具有相似的结构。它们很容易维护,且用户看来也更一致。另一方面,你也失去了一些表现创造性的自由,因为许多设计决策无须你来作出。
框架设计者必须冒险决定一个要适应于该领域的所有应用的体系结构。任何对框架设计的实质性修改都会大大降低框架所带来的好处,因为框架对应用的最主要贡献在于它所定义的体系结构。因此设计的框架必须尽可能地灵活、可扩充。
更进一步讲,因为应用的设计如此依赖于框架,所以应用对框架接口的变化是极其敏感的。当框架演化时,应用不得不随之演化。这使得松散耦合更加重要,否则框架的一个细微变化都将引起强烈反应。
一个使用设计模式的框架比不用设计模式的框架更可能获得高层次的设计复用和代码复用。成熟的框架通常使用了多种设计模式。设计模式有助于获得无须重新设计就可适用于多种应用的框架体系结构。
因为模式和框架有些类似,人们常常对它们有怎样的区别和它们是否有区别感到疑惑。它们最主要的不同在于如下三个方面:
1)、设计模式比框架更抽象。
框架能够用代码表示,而设计模式只有其实例才能表示为代码。框架的威力在于它们能够使用程序设计语言写出来,它们不仅能被学习,也能被直接执行和复用。而本书中的设计模式在每一次被复用时,都需要被实现。设计模式还解释了它的
意图、权衡和设计效果。
2)、设计模式是比框架更小的体系结构元素。
一个典型的框架包括了多个设计模式,而反之决非如此。
3)、框架比设计模式更加特例化。
框架总是针对一个特定的应用领域。一个图形编辑器框架可能被用于一个工厂模拟,但它不会被错认为是一个模拟框架。而本书收录的设计模式几乎能被用于任何应用。
框架变得越来越普遍和重要。它们是面向对象系统获得最大复用的方式。较大的面向对象应用将会由多层彼此合作的框架组成。应用的大部分设计和代码将来自于它所使用的框架或受其影响。
*反模式
反模式告诉你如何采用一个不好的解决方案解决一个问题。
反模式看起来总像一个好的解决方案,但是当它真正被采用后,就会带来麻烦。
如果老是有人用某个不好的解决方案处理某个问题,而通过将它归档,可以帮助其他开发人员避免犯同样的错误。毕竟,避免不好的解决方案,就和发现好的解决方案一样有价值。
反模式告诉我们为什么不好的解决方案会有吸引力。
反模式告诉我们为什么这个解决方案从长远看会造成不好的影响。
反模式会为我们指出正确的方向或一些好的建议。
反模式最终的工作之一,就是在于警告你不要陷入某种致命的诱惑。