设计模式篇
《设计模式——可复用面向对象软件的基础》学习笔记,该书是我们熟知的23中设计模式的提出者,是最经典的设计模式方面书籍之一,非常值得任何想要提高自己程序设计思维的程序设计者阅读,原书pdf版见这里。
1. 什么是设计模式
《设计模式——可复用面向对象软件的基础》原文中指出:“本书中的设计模式是对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述。”
“设计模式确定了所包含的类和示例,它们的角色、协作方式以及职责分配。每一种设计模式都集中于一个特定的面向对象设计问题或设计要点,描述了什么时候使用它,在另一些设计约束条件下是否还能使用以及使用的效果和如何取舍。”
因此设计模式很大程度上是为达到设计复用、避免重复劳动的目的。
2. 描述设计模式
为了有助于使用者更容易学习、理解、比较和使用设计模式,采用了统一的格式来描述设计模式,每一个模式根据以下模板被分为若干部分。
模式名和分类
模式名简洁地描述了模式的本质。一个好的名字可以让使用者更快地理解。
意图
它描述了某种设计模式是为了解决什么样的问题
别名
模式的其他名称
动机
用以说明一个设计问题以及如何用模式中的类、对象来解决该问题的特定情景。
适用性
结构
采用了对象建模技术的表示法对模式中的类进行图形描述。
参与者
指的是设计模式中的类和/或对象以及它们各自的职责。
协作
模式的参与者怎样协作以实现它们的职责。
效果
模式怎样支持它的目标?使用模式的效果和所需做的权衡取舍?系统结构的哪些方面可以独立改变?
实现
实现模式时需要知道的一些提示、技术要点及应避免的缺陷,以及是否存在某些特定于实现语言的问题。
3. 设计模式的编目
- Abstract Factory:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
- Adapter:将一个类的接口转换成客户希望的另一个接口。Adapter模式使得原本由于接口不相容而不能一起工作的那些类可以一起工作。
- Bridge:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
- Builder:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
- Chain of Responsibility:为解除请求的发送者和接收者之间耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。
- Command:将一个请求封装为一个对象,从而使你可用不同的请求参数对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。
- Composite:将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得客户对单个对象和复合对象的使用具有一致性。
- Decorator:动态地给一个对象添加一些额外的职责。就扩展功能而言,Decorator模式比生成子类方式更为灵活。
- Facade:为子系统中的一组接口提供一个一致的界面,Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
- Factory Method:定义一个用于创建对象的接口,让子类决定将哪个类实例化。Factory Method使一个类的实例化延迟到其子类。
- Flyweight:运用共享技术有效地支持大量细粒度的对象。
- Interpreter:给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
- Iterator:提供一种方法顺序访问一个聚合对象中各个元素,而又不需要暴露该对象的内部表示。
- Mediator:用一个中介对象来封装一系列的对象交互。中介者使各个对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
- Memento:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到保存的状态。
- Observer:定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。
- Prototype:用原型实例创建对象的种类,并且通过拷贝这个原型来创建新的对象。
- Proxy:为其他对象提供一个代理以控制这个对象的访问。
- Singleton:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- State:允许一个对象在它的内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。
- Strategy:定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法的变化独立于使用它的客户。
- Template Method:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
- Visitor:表示一个作用于某对象结构中的各元素的操作。它使你在不改变各元素的类的前提下定义作用于这些元素的新操作。
4. 设计模式的分类
根据目的和范围两条准则来进行分类。
目的准则,即模式是用来完成什么工作的。模式依据目的可分为以下3种:
创建型(Creational):与对象的创建有关。
结构型(Structural):处理类或对象的组合。
行为型(Behavioral):描述类或对象怎样交互和怎样分配职责。
范围准则:指定模式主要是用于类还是用于对象。
类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,在编译时刻确定。
对象模式处理对象间的关系,这些关系在运行时刻是可变化的,更具动态性。
创建型类模式将对象的部分创建工作延迟到子类,而创建型对象模式则将它延迟到另一个对象中。
结构型类模式使用继承机制来组合类,而结构型对象模式则描述了对象的组装方式。
行为型类模式使用继承描述算法和控制流,而行为型对象模式则描述一组对象怎样协作完成单个对象所无法完成的任务。
下表给出了所有的设计模式分类:
5. 设计模式之间的关系
6. 设计模式怎样解决设计问题
设计模式采用多种方法解决面向对象设计者经常碰到的问题。
5.1寻找合适的对象
面向对象程序由对象组成,对象包括数据和对数据进行操作的过程,过程通常也被称为方法或操作。对象在收到客户的请求或消息后,执行相应的操作。客户请求是让对象执行操作的唯一方法,操作又是改变内部数据的唯一方法,因此对象的内部状态是封装的,不能被直接访问,且它的表示对外部是不可见的。
面向对象设计最困难的部分是将系统分解成对象集合。因为要考虑许多因素:封装、粒度、依赖关系、灵活性、性能、演化、复用等,它们都影响着系统的分解,且这些因素通常是互相冲突的。
设计模式帮助我们确定并不明显的抽象和描述这些抽象的对象。
5.2 决定对象的粒度
对象在大小和数目上变化极大。它们可以表示下自硬件上至整个应用的任何事物。那我们如何决定对象的粒度呢?设计模式很好的讲述了这个问题。比如,Faca模式描述了如何用对象表示完整的子系统;Flyweight模式描述了如何支持大量的最小粒度的对象;Abstract Facory和Builder产生那些专门负责生成其他对象的对象;Vistor和Command生成的对象专门负责实现对其他对象或对象组的请求。
5.3 指定对象接口
对象声明的每一个操作指定操作名、作为参数的对象和返回值,这也称为操作的型构(signture)。**对象操作所定义的所有操作型构的集合被称为该对象的接口。**对象接口描述了该对象所能接受的全部请求的集合,任何匹配对象接口中型构的请求都可以发送给该对象。
在面向对象系统中,接口是基本的组成部分。对象只有通过它们的接口才能与外部交流。对象接口与其功能实现分离,不同对象可对请求做不同的实现。
当给对象发送请求时,所引起的具体操作既与请求本身有关,又与接受对象有关。发送给对象的请求和它的相应操作在运行时刻的连接就称之为动态绑定(dynamic binding)。
动态绑定是指发送的请求直到运行时刻才受你的具体实现的约束。**动态绑定允许你在运行时刻彼此替换有相同接口的对象。这种可替换性称为多态(polymorphism)。**多态简化了客户的定义,使得对象间彼此独立,并可以在运行时刻动态改变它们相互的关系。
**设计模式通过确定接口的主要组成成分及经接口发送的数据类型,来帮助你定义接口。**设计模式也指定了接口之间的关系。
5.4 描述对象的实现
对象通过实例化类来创建,此对象被称为该类的实例对象。当实例化类时,要给对象的内部数据(由实例变量组成)分配存储空间,并将操作与这些数据联系起来。
新的类可以由已存在的类通过类继承(class inheritance)来定义。当子类(subclass)继承父类(parent class)时,子类包含了父类定义的所有数据和操作。子类的实例对象包含所有子类和父类定义的数据,且它们能完成子类和父类定义的所有操作。
抽象类(abstract class)的主要目的是为它的子类定义公共接口。一个抽象类把它的部分或者全部操作延迟到子类实现,因此,抽象类不能被实例化。
类能够重定义,也称为重写(override)父类定义的操作,重定义使得子类能够接管父类对请求的处理操作。
类继承与接口继承的比较
类继承根据一个对象的实现定义了另一个对象的实现,即它是代码和表示的共享机制。而接口继承描述了一个对象什么时候能被用来替代另一个对象。
类继承是一个通过复用父类功能而扩展应用功能的基本机制。它允许你根据旧对象快速定义新的对象,允许从已存在的类中继承所需要的绝大部分功能,从而几乎无需任何代价就可以获得新的实现。
5.5 运用复用机制
面向对象系统中功能复用有3种方式:类继承、对象组合和参数化类型,其中最常用的是继承和组合。
继承和组合的比较
类继承是根据其他类的实现来定义一个类的实现。在继承方式中,父类的细节对子类可见,因此称之为“白箱复用”。
对象组合是指通过组装或组合对象来实现更复杂的功能。因为对象的内部细节是不可见的,因此被称为“黑箱复用”。
类继承的优缺点:
优点:类继承是在编译时刻静态定义的,且可直接使用。类继承可以比较方便地改变被复用的实现。
缺点:因为类继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。因为继承揭示了其父类的实现细节,所以继承被认为破坏了封装性。父类实现中的任何变化必然会导致子类发送变化。
当需要复用子类时,实现上的依赖性就会产生一些问题。如何继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决办法是只继承抽象类。
对象组合的优缺点
优点:对象组合是通过获得其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此间的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍将一个对象和其他对象一起使用。而且由于对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象;由于对象的实现是基于接口写的,所以实现上存在较少的依赖关系,减小了耦合。
对象组合在系统中还有另一个作用,即优先使用对象组合有助于保持每个类被封装,并集中在单个任务上。
因此,面向对象设计的原则之一是:优先使用对象组合,而不是类继承。
缺点:因为可用构件的集合实际上并不足够丰富。使用继承的复用使得创建新的构件要比组装旧的构件容易。这样,继承和组合经常一起使用。
*委托
委托(delegation)是一种组合方法,它使组合具有继承同样的复用能力。
参数化类型
参数化类型(parameterized type)是另一种功能复用技术,也就是模板(templates)(C++)。它允许在定义一个类型时并不指定该类型所用到的其他所有类型。为经指定的类型在使用时以参数形式提供。
5.6 关联运行时刻和编译时刻的结构
一个面向对象程序运行时刻的结构通常与它的代码结构相差较大。代码结构在编译时刻就被确定下来了,它由继承关系固定的类组成。而程序的运行时刻结构是由快速变化的通信对象网络组成。
系统的运行时刻结构更多地受到设计者的影响,而不是编程语言。对象和它们的类型之间的关系必须更加仔细地设计,因为它们决定了运行时刻程序结构的好坏。
**设计模式显式地记述了编译时刻和运行时刻结构的差别。**只有理解了模式,才能弄清楚代码中的运行时刻结构。
5.7 设计应支持变化
获得最大限度复用的关键在于对新需求和已有需求发生变化时的预见性,要求你的系统能够相应的改进。
设计模式可以确保系统能以特定方式变化,从而帮助开发者避免重新设计系统。每一个设计模式运行系统结构的某个方面的变化独立于其他方面,这样产生的系统对于某一种特殊变化将更加健壮。
以下总结了一些导致重新设计的一般原因,以及解决这些问题的设计模式:
1)通过显式地指定一个类来创建对象。在创建对象时指定类名将受特定实现的约束,而不是特定接口的约束。这会使未来的变化更复杂。要避免这种情况,应该间接地创建对象。
设计模式:Abstract Factory, Factory Method, Prototype
2)对特殊操作的依赖。当为请求指定一个特殊的操作时,完成该请求的方式就固定下来了。未来避免把请求代码写死,可以在编译时刻或运行时刻很方便地改变相应请求的方法。
设计模式:Chain Resposibility, 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 Responsiblity, Composite, Decorator, Observer, Strategy
8)不能方便地对类进行修改。 有时不得不修改一个难以改变的类。也许你需要源代码而又没有(对于商业类库就有这种情况),或者对类的改变可能要求修改许多已存在的其他子类。。设计模式提供在这些情况下对类进行修改的方法。
设计模式:Adapter, Decorator, Visitor### ###
7. 怎样选择设计模式
1)考虑设计模式是怎样解决设计问题的
2)浏览模式的意图部分
3)研究模式怎样互相关联
4)研究目的相似的模式
5)检查重新设计的原因
6)考虑你的设计中哪些是可变的
8. 怎样使用设计模式
1)大致浏览一遍模式,特别注意其适用性和效果部分,确定它适合你的问题。
2)回头研究结构部分、参与者和协作部分,确定你理解这个模式的类和对象以及它们是怎样关联的。
3)看代码示例部分
4)选择模式参与者的名字,使它们在应用上下文中有意义。
5)定义类,声明它们的接口,建立它们的继承关系,定义代表数据和对象引用的实例变量。
6)定义模式中专用于应用的操作名称。
7)实现执行模式中负责和协作的操作。