简介

  • 设计模式:可复用面向对象软件的基础 阅读笔记

第一章 引言

  • 设计面向对象软件比较困难,而设计可复用的面向对象软件就更加困难。你必须找到相关的对象,以适当的粒度将它们归类,再定义类的接口和继承层次,建立对象之间的基本关系

什么是设计模式

  • 一般而言,一个模式有四个基本要素:
    • 模式名称(pattern name),一个助记名,它用一两个词来描述模式的问题,解决方案和效果。
    • 问题(problem),描述了应该在何时使用模式。它解释了设计问题和问题存在的前因后果,它可能描述了特定的设计问题,例如怎样用对象表示算法等。也可能描述了导致了不灵活设计的类或对象结构。
    • 解决方案(solution),描述了设计的组成成分,它们之间的相互关系以及各自的职责和协作方式。因为模式就像一个模板,可应用于多种不同场合,所以解决方案并不描述一个特定而具体的设计或实现,而是提供设计问题的抽象描述和怎样用一个具有一般意义的元素组合(类或对象组合)来解决这个问题
    • 效果(consequences),描述了模式应用的效果以及使用模式应权衡的问题。尽管我们描述设计决策时,并不总提到模式效果,但它们对于评价设计选择和理解使用模式的代价和好处具有重要意义。软件效果大多关注对时间和空间的衡量,它们也表述了语言和实现问题。因为复用是面向对象设计的要素之一,所以模式效果包括它对系统的灵活性,扩充性或者可移植性的映像。
  • 本书中的设计模式是对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述

组织编目

  • 根据两条准则对模式进行分类。

  • 第一是目的准则,即模式是用来完成什么工作的。
    • 模式依据其目的可分为创建型(Creational),结构型(Structural),或行为型(Behavioral)三种。
      • 创建型模式与对象的创建有关;
      • 结构型模式处理类或对象的组合;
      • 行为型模式对类或对象怎样交互和怎样分配职责进行描述
  • 第二是范围准则,指定模式主要用作于类还是用作于对象。
    • 类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,在编译时刻便确定下来了。
    • 对象模式处理对象间的关系,这些关系在运行时刻是可以变化的,更具有动态性。
    • 从某种意义上来说,几乎所有模式都使用继承机制,所以 类模式 只指那些集中于处理类间关系的模式,而大部分模式都属于对象模式的范畴

设计模式怎样解决设计问题

  • 设计模式采用多种方法解决面向对象设计者经常碰到的问题

寻找合适的对象

  • 面向对象程序由对象组成,对象包括数据和对数据进行操作的过程,过程通常称为方法或者操作。对象在收到客户的请求(或消息)后,执行相应的操作

  • 客户请求是使对象执行操作的唯一方法,操作又是对象改变内部数据的唯一方法。由于这些限制,对象的内部状态是被封装的,他不能被直接访问,它的表示对于对象外部是不可见的

  • 面向对象设计最困难的部分是将系统分解成对象集合。因为要考虑许多因素:封装,粒度,依赖关系,灵活性,性能,演化,复用等等,它们都影响着系统的分解,并且这些因素通常还是互相冲突的

  • 设计模式帮你确定并不明显的抽象和描述这些抽象的对象

决定对象的粒度

  • 对象在大小和数目上变化极大。它们能表示下自硬件或者上自整个应用的任何事物

指定对象接口

  • 对象声明的每一个操作指定操作名,作为参数的对象和返回值,这就是所谓的操作的型构(signature)。
  • 对象操作所定义的所有操作型构的集合被称为该对象的接口(interface)。
  • 对象接口描述了该对象所能接受的全部请求的集合,任何匹配对象接口中型构的请求都可以发送给该对象

  • 类型(type)是用来标识特定接口的一个名字。
  • 接口可以包含其他接口作为子集。当一个类型的接口包含另一个类型的接口时,我们就说它是另一个类型的子类型(subtype),另一个类型称之为它的超类型(supertype)。
  • 我们常说子类型继承了它的超类型的接口

  • 当给对象发送请求时,所引起的具体操作即与请求本身有关又与接收对象有关。支持相同请求的不同对象可能对请求激发的操作有不同的实现。发送给对象的请求和它的相应操作在运行时刻的连接就称之为动态绑定(dynamice binding)
  • 动态绑定是指发送的请求直到运行时刻才受到你的具体的实现的约束。
  • 进一步将,动态绑定允许你在运行时刻彼此替换有相同接口的对象。这种可替换性就称为多态(polymorphism),它是面向对象系统中的核心概念之一

描述对象的实现

  • 对象的实现是由它的类决定的,类指定了对象的内部数据和表示,也定义了对象所能够完成的操作。

  • 对象通过实例化类来创建,此时对象被称为该类的实例。当实例化类时,要给对象的内部数据(由实例变量组成)分配存储空间,并将操作与这些数据联系起来。

  • 新的类可以由已存在的类通过类继承(class inheritance)来定义。当子类(subclass)继承父类(parent class)时,子类包含了父类定义的所有数据和操作

  • 抽象类(abstrace class)的主要目的是为它的子类定义公共接口。一个抽象类将把它的部分或者全部操作的实现延迟到子类中,因此,一个抽象类不能被实例化。在抽象类中定义却没有实现的操作被称为抽象操作(abstract operation)。
  • 非抽象类称为具体类

  • 子类能够改进和重新定义它们的父类的操作。更具体的说,类能够重定义(override)父类定义的操作,重定义使得子类能够接管父类对请求的处理操作。

  • 混入类(mixin class)是给其他类提供可选择的接口或者功能的类。它与抽象类一样不能实例化。混入类要求多继承
类继承与接口继承的比较
  • 理解对象的类(class)与对象的类型(type)之间的差别非常重要

  • 一个对象的类定义了对象是怎样实现的,同时也定义了对象的内部状态和操作的实现。
  • 但是对象的类型只与它的接口有关,接口即对象能够响应的请求的集合。
  • 一个对象可以有多个类型,不同类的对象可以有相同的类型。

  • 理解类继承和接口继承(或子类型化)之间的差别也十分重要。

  • 类继承根据一个对象的实现定义了另一个对象的实现。简而言之,它是代码和表示的共享机制。
  • 然而,接口继承(或子类型化)描述了一个对象什么时候能被用来替代另一个对象。

  • 因为许多语言并不显式地区分这两个概念,所以容易被混淆。在C++ 和 Eiffel语言中,继承既指接口的继承又指实现的继承。
  • C++中接口继承的标准方法是公有继承一个含(纯)虚成员函数的类。
  • C++中纯接口继承接近于公有继承纯抽象类,纯实现继承或纯类继承接近于私有继承
对接口编程,而不是对实现编程
  • 类继承是一个通过复用父类功能而扩展应用功能的基本机制。

  • 然而,实现的复用只是成功的一半,继承所拥有的定义具有相同接口的对象族的能力也是很重要的(通常可以从抽象类来继承)。
  • 为什么?因为多态依赖于这种能力

  • 当继承被恰当使用时,所有从抽象类导出的类将共享该抽象类的接口。这意味着子类仅仅添加或重定义操作,而没有隐藏父类的操作。
  • 这时,所有的子类都能响应抽象类接口中的请求,从而子类的类型都是抽象类的子类型。

  • 只根据抽象类中定义的接口来操作对象有以下两个好处:
    • 客户无须知道它们使用对象的特定类型,只须对象有客户所期望的接口
    • 客户无须知道它们使用的对象是用什么类来实现的,它们只须知道定义接口的抽象类。
  • 这将极大地减少子系统实现之间的相互依赖关系,也产生了可复用的面向对象设计的如下原则:
    • 针对接口编程,而不是针对实现编程。
  • 不将变量声明为某个特定的具体类的实例对象,而是让它遵循从抽象类所定义的接口。这是本书设计模式的一个常见主题。
运用复用机制
  • 面向对象系统中功能复用的两种最常用技术是类继承和对象组合(object composition)
  • 类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。
  • 术语 白箱 是相对可视性而言:在继承方式中,父类的内部细节对子类可见

  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。
  • 对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以 黑箱 的形式出现

  • 类继承的优点:
    • 在编译时刻静态定义的,且可直接使用,因为程序设计语言直接支持类继承。
    • 较方便地改变被复用的实现。
    • 当一个子类重定义一些而不是全部操作时,他也能影响他所继承的操作,只要在这些操作中调用了被重定义的操作。
  • 类继承的缺点:
    • 因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现
    • 父类通常至少定义了部分子类的具体表示
    • 因为继承对子类揭示了其父类的实现细节,所以继承常被认为破坏了封装性
    • 子类中的实现与他的父类有紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类的变化
  • 当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。
  • 一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现

  • 对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。
  • 组合要求对象遵守彼此的接口约定,进而要求更仔细的定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。
  • 这会产生良好的结果:
    • 因为对象只能通过接口访问,所以我们并不破坏封装性
    • 只要类型一致,运行时刻还可以用一个对象来替代另一个对象
    • 更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系
  • 对象组合对系统设计还有另一个作用,既优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。
  • 另一方面,基于对象组合的设计会有更多的对象(而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中

  • 这导出了我们的面向对象设计的第二个原则:
    • 优先使用对象组合,而不是类继承。
  • 理想情况下,你不应该为获得复用而去创建新的构件。你应该能够只使用对象组合技术,通过组装已有的构建就能获得你需要的功能。
  • 但是事实很少如此,因为可用构建的集合实际上并不足够丰富。使用继承的复用使得创建新的构建要比组装旧的构建来的容易。
  • 这样,继承和对象组合常一起使用。

  • 委托(delegation)是一种组合方法,他使组合具有与继承同样的复用能力。在委托方式下,有两个对象参与处理一个请求,接受请求的对象将操作委托给他的代理者(delegate)
  • 这类似于子类将请求交给他的父类处理。使用继承时,被继承的操作总能引用接受请求的对象,C++中通过this成员变量
  • 委托是对象组合的特例。它告诉你对象组合作为一个代码复用机制可以代替继承

  • 另一种功能复用技术(并非严格的面向对象技术)是参数化类型(parameterized type),也就是类属(generic)或者模板(templates (C++))。他允许你在定义一个类型时并不指定该类型所用到的其他所有类型。未经指定的类型在使用时以参数形式提供。

  • 参数化类型给我们提供了除了类继承和对象组合外的第三种方法来组合面向对象系统中的行为。许多设计可以使用这三种技术中的任何一种来实现。
  • 实现一个以元素比较操作为可变元的排序例程,可有如下方法:
    • 通过子类实现该操作
    • 实现为传给排序例程的对象的职责
    • 作为C++模板或Ada类属的参数,以指定元素比较操作的名称

关联运行时刻和编译时刻的结构

  • 一个面向对象程序运行时刻的结构通常与他的代码结构相差很大。
  • 代码结构在编译时刻就被确定下来了,他由继承关系固定的类组成。而程序的运行时刻结构是由快速变化的通信对象网格组成。
  • 事实上,两个结构是彼此独立的,试图由一个区理解另一个就好像试图从静态的动植物分类去理解活生生的生态系统的动态性。反之亦然。

  • 聚合(aggregation),意味着一个对象拥有另一个对象或对另一个对象负责。
  • 一般我们称一个对象包含另一个对象或者另一个对象的一部分。聚合意味着聚合对象和其所有者具有相同的生命周期

设计应支持变化

  • 一些导致重新设计的一般原因,以及解决这些问题的设计模式:
    • 通过显示的指定一个类来创建对象 : 在创建对象时指定类名将使你受特定实现的约束而不是特定接口的约束。这会使未来的变化更复杂。要避免这种情况,应该间接地创建对象
      • 设计模式: Abstract Factory, Factory Method, Prototype
    • 对特殊操作的依赖 : 当你为请求指定一个特殊的操作时,完成该请求的方式就固定下来了。为避免把请求代码写死,你将可以在编译时刻或运行时刻很方便地改变响应请求的方式
      • 设计模式: Chain of Resposibility, Command
    • 对硬件和软件平台的依赖 : 外部的操作系统接口和应用编成接口(API)在不同的软硬件平台上是不同的。依赖于特定平台的软件将很难移植到其他平台上,甚至都很难跟上本地平台的更新。所以设计系统时限制其平台相关性就很重要了。
      • 设计模式: Abstract Factory, Bridge
    • 对对象表示或实现的依赖 : 知道对象怎样表示,保存,定位或实现的客户在对象发生变化时可能也需要变化。对客户隐藏这些信息能够阻止连锁变化
      • 设计模式: Abstract Factory, Bridge, Memento, Proxy
    • 算法依赖 : 算法在开发和复用时常常被扩展,优化和替代。依赖于某个特定算法的对象在算法发生变化时不得不变化。因此有可能发生变化的算法应该被孤立起来。
      • 设计模式: Builder, Iterator, Strategy, Template Method, Visitor
    • 紧耦合 : 紧耦合的类很难孤立的被复用,因为它们是互相依赖的。紧耦合产生单块的系统,要改变或者删掉一个类,你必须理解和改变其他许多类。这样的系统是一个很难学习移植和维护的密集体。松散耦合提高了一个类本身被复用的可能性,并且系统更易于学习,移植,修改和扩展。设计模式使用抽象耦合和分层技术来提高系统的松散耦合性。
      • 设计模式: Abstract Factory, Command, Facade, Mediator, Observer, Chain of Responsibility
    • 通过生成子类来扩充功能 : 通常很难通过定义子类来定制对象。每一个新类都有固定的实现开销(初始化,终止处理等)。子类方法会导致类爆炸,因为即使对于一个简单的扩充,你也不得不引入许多新的子类。一般的对象组合技术和具体的委托技术,是继承之外组合对象行为的另一种灵活方法。新的功能可以通过以新的方式组合已有对象,而不是通过定义已存在类的子类的方式加到应用中去。另一方面,过多使用对象组合会使设计难于理解。许多设计模式产生的设计中,你可以定义一个子类,且将它的实例和已存在实例进行组合来引入定制的功能。
      • 设计模式:Bridge, Chain of Responsibility, Composite, Decorator, Observer, Strategy
    • 不能方便地对类进行修改 : 有时你不得不改变一个难以修改的类。
      • 设计模式: Adapter, Decorator, Visitor
  • 设计模式在开发如下三类主要软件中所起作用:
    • 应用程序
    • 工具箱
    • 框架
  • 如果要建造像文档编辑器的应用程序(Application Program),那么它的内部复用性,可维护性和可扩充性是要优先考虑的。
  • 内部复用性确保你不会做多余的设计和实现
  • 设计模式通过减少依赖型来提高内部复用性。
  • 松散耦合也增强了一类对象与其他多个对象协作的可能性。

怎样选择设计模式

  • 考虑设计模式是怎样解决设计问题的

  • 浏览模式的意图部分

  • 研究模式怎样互相关联

  • 研究目的相似的模式

  • 检查重新设计的原因

  • 考虑你的设计中那些是可变的

怎样使用设计模式

  • 大致浏览一遍模式。
    • 特别注意其适用性部分和效果部分,确定它适合你的问题
  • 回头研究结构部分,参与者部分和协作部分
    • 确保你理解这个模式的类和对象以及他们是怎样关联的
  • 选择模式参与者的名字,使他们在应用上下文中有意义

  • 定义类
    • 声明它们的接口,建立他们的继承关系,定义代表数据和对象引用的实例变量。
    • 识别模式会影响的你的应用中存在的类,作出相应的修改
  • 定义模式中专用于应用的操作名称
    • 名字一般依赖于应用。使用与每一个操作相关联的责任和写作作为指导
    • 名字约定要一致
  • 实现执行模式中责任和写作的操作

第二章 实例研究: 设计一个文档编辑器

递归组合

  • 层次结构信息的表述通常是通过一种被称为递归组合(Recursive Composition)的技术来实现的。
  • 递归组合可以由较简单的元素逐渐建立复杂的元素,是我们通过简单图形元素构造文档的方法之一。

第三章 创建型模式

  • 创建型模式抽象了实例化过程。它们帮助一个系统独立于如何创建,组合和表示它的哪些对象。一个类创建型模式使用继承改变被实例化的类,而一个对象创建型模式将实例化委托给另一个对象。

  • 创建型模式在什么被创建,谁创建它,它是怎样被创建的,以及何时创建这方面给予很大的灵活性。它们允许你用结构和功能差别很大的产品对象配置一个系统。配置可以是静态的(即在编译时指定的),也可以是动态的(在运行时)

ABSTRACT FACTORY(抽象工厂) – 对象创建型模式

  • 意图:
    • 提供一个创建一系列相关或相互依赖对象的接口,而无需指定他们具体的类
  • 别名:
    • Kit
  • 动机
    • 考虑一个支持多种视感(look-and-feel)标准的用户界面工具包。不同的视感风格,为诸如滚动条,窗口和按钮等用户界面”窗口组件”定义不同的外观和行为。
    • 为保证视感风格标准间的可移植性,一个应用不应该为一个特定的视感外观硬编码它的窗口组件。在整个应用中实例化特定视感风格的窗口组件类将使得以后很难改变视感风格