1-设计模式概论

定义和组成要素

什么是模式:每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。

模式的组成要素

  • 模式名
  • 问题:何时使用该模式
  • 解决方案:描述设计的组成成分、相互关系和协作方式
  • 影响:描述模式应当权衡的问题,对系统的灵活性、扩充性或可移植性的影响。如时空复杂度、语言和实现

由 MVC, Model-View-Controller 理解“模式”:

  • 什么是 MVC:
    • 模型:应用对象
    • 视图:对象在屏幕上的显示
    • 控制器:定义用户界面对用户输入的响应方式
    • MVC 通过建立订阅-通知协议来分离视图和模型。一旦模型的数据发生变化,模型将通知有关的视图,每个视图相应地得到刷新自己的机会。允许为一个模型提供不同的多个视图表现形式
  • 推广 1:模型和视图分离→订阅对象和发布对象分离
    • 将对象分离,使得一个对象的改变能够影响另一些对象,而这个对象并不需要知道那些被影响的对象的细节。这个更一般的设计被描述成 Observer
  • 推广 2:MVC 允许视图嵌套,将组合 View 与单个 View 平等对待→将对象组视为一般对象
    • 将一些对象划为一组,并将该组对象当作一个对象来使用。这个设计被描述为 Composite

设计模式的组成要素

  • 模式名、别名和分类
  • 意图:解决什么问题
  • 动机:说明一个设计问题以及如何用模式中的类、对象来解决该问题的特定情景
  • 适用性
  • 结构:类图和交互图(描述对象之间的请求序列和协作关系)
  • 参与者:涉及的类和/或对象以及它们各自的职责。
  • 协作:模式的参与者怎样协作以实现它们的职责。
  • 影响
  • 实现:实现时的注意事项
  • 实际应用案例
  • 相关模式

设计模式目录

  • 创建型模式与对象的创建有关;

  • 结构型模式处理类或对象的组合;

  • 行为型模式对类或对象怎样交互和怎样分配职责进行描述。

  • 类模式:处理类和子类的关系

  • 对象模式:处理对象间的关系

设计模式中可变的部分

  1. Abstract Factory(3.1):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
  2. Adapter(4.1):将一个类的接口转换成客户希望的另外一个接口。Adapter 模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
  3. Bridge(4.2):将抽象部分与它的实现部分分离,使它们都可以独立地变化。
  4. Builder(3.2):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
  5. Chain of Responsibility(5.1):解除请求的发送者和接收者之间的耦合,使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。Command(5.2):将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。
  6. Composite(4.3):将对象组合成树形结构以表示“部分–整体”的层次结构。Composite 使得客户对单个对象和组合对象的使用具有一致性。
  7. Decorator(4.4):动态地给一个对象添加一些额外的职责。就扩展功能而言,Decorator 模式比生成子类方式更为灵活。
  8. Facade(4.5):为子系统中的一组接口提供一个一致的界面, Facade 模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
  9. Factory Method(3.3):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。Factory Method 使一个类的实例化延迟到其子类。
  10. Flyweight(4.6):运用共享技术有效地支持大量细粒度的对象。
  11. Interpreter(5.3):给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。Iterator(5.4):提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
  12. Mediator(5.5):用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
  13. Memento(5.6):在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到保存的状态。
  14. Observer(5.7):定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。
  15. Prototype(3.4):用原型实例指定创建对象的种类,并且通过拷贝这个原型来创建新的对象。
  16. Proxy(4.7):为其他对象提供一个代理以控制对这个对象的访问。
  17. Singleton(3.5):保证一个类仅有一个实例,并提供一个访问它的全局访问点。State(5.8):允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。
  18. Strategy(5.9):定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法的变化可独立于使用它的客户。
  19. Template Method(5.10):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。Template Method 使得子类不改变一个算法的结构即可重定义该算法的某些特定步骤。
  20. Visitor(5.11):表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

面向对象设计的步骤

设计原则

对接口编程,而不是对实现编程。这可以极大地减少子系统实现之间的相互依赖关系:

  • 客户无须知道他们使用对象的特定类型,只需要知道对象有客户所期望的接口。
  • 客户无须知道他们使用的对象是用什么类来实现的,只需要知道定义接口的抽象类。

优先使用对象组合,而不是类继承:对象组合耦合度低,不破坏封装性

设计步骤与基本概念

  1. 选择合适对象:
    1. 对象在收到客户请求后执行相应动作
    2. 对象的内部状态是被封装的。客户请求是使对象执行操作的唯一方法,操作又是对象改变内部数据的唯一方法。
  2. 决定对象的粒度
  3. 指定对象的接口:
    1. 操作的型构 (signature):操作名、作为参数的对象和返回值 3 者唯一确定一个操作
    2. 接口 (interface):对象操作所定义的所有操作型构的集合被称为该对象的~
    3. 类型 type:即接口名。如果一个对象接受“Window”接口所定义的所有操作请求,那么我们就说该对象具有“Window”类型。
      1. 对象和类型是多对多的关系
      2. 子类型和超类型:当一个类型的接口包含另一个类型的接口时,我们就说它是另一个类型的子类型(subtype),而称另一个类型为它的超类型(supertype)。我们常说子类型继承了它的超类型的接口。
      3. 动态绑定(dynamic binding):发送给对象的请求和它的相应操作在运行时的连接就称为动态绑定
      4. 多态(polymorphism):动态绑定允许你在运行时彼此替换有相同接口的对象。这种可替换性就称为多态

比较类(class)与类型(type)

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

class 是 type 的超集,class=type+内部状态和实现

类继承和接口继承

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

类和对象关系图

类:类名、类方法、类变量

L

实例化 :用虚箭头从类指向实例

由类 A 创建类 B L

继承:三角箭头

从子类指向父类 抽象类(abstract class)的主要目的是为它的子类定义公共接口。抽象类将把它的部分或全部操作的实现延迟到子类中,不能被实例化。

  • 在抽象类中定义却没有实现的操作被称为抽象操作 (abstract operation)
  • 抽象类的类名以斜体表示,以与具体类相区别。抽象操作也用斜体表示。 重载 override:子类能够改进和重新定义它们父类的操作

L 混入类(mixin class)是给其他类提供可选择的接口或功能的类。它与抽象类一样不能实例化。混入类要求多继承 L

聚合尾部菱形箭头、相识普通箭头

聚合尾部菱形箭头:称一个对象包含另一个对象或者是另一个对象的一部分。聚合意味着聚合对象和其所有者具有相同的生命周期。 相识、关联、引用普通箭头:相识意味着一个对象仅仅知道另一个对象。相识的对象可能请求彼此的操作,但是它们不为对方负责。

伪代码实现:虚线引出

L

3 种复用方式

类继承、对象组合、参数化类型

类继承和对象组合

类继承和对象组合:这是面向对象系统中最常用的功能复用技术

  • 类继承:又称白箱复用(white-box reuse):在继承方式中,父类的内部细节对子类可见。
    • 优点:编译时静态定义且可以直接使用;程序设计语言直接支持类继承;易于改变实现;
    • 缺点:无法在运行时改变从父类继承的实现;父子类依赖紧密(继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。)
  • 对象组合:又称黑箱复用(black-box reuse):新的更复杂的功能可以通过组装或组合对象来获得。
    • 优点:依赖少,耦合度低;只通过接口访问不影响封装性
  • 不应为获得复用而去创建新的构件,应该只使用对象组合技术,通过组装已有的构件就能获得你需要的功能。

委托(Sp 对象组合)

委托 delegation:一种组合方法,它使组合具有与继承同样的复用能力

  • 工作模式:
    • 有两个对象参与处理一个请求,接受请求的对象将操作委托给它的代理者 delegate(类似于子类将请求交给它的父类处理)
    • 接受请求的对象将自己传给被委托者(代理者),使被委托的操作可以引用接受请求的对象。
  • 优点:便于运行时组合对象操作以及改变这些操作的组合方式。
  • 缺点:动态的、高度参数化的软件比静态软件更难于理解;运行相对低效

Eg:Window 无需继承 Rectangle 而能够使用后者的功能

参数化类型

参数化类型即 C++中的模板 template:允许你在定义一个类型时不用指定该类型所用到的其他所有类型。未经指定的类型在使用时以参数形式提供。 参数化类型在不执行编译时类型检查的语言中式完全不必要的

例如实现一个以元素比较操作为可变元的排序例程:

  1. 通过子类实现该操作(Template Method(5.10)的一个应用)。
  2. 实现要传给排序例程的对象的职责(Strategy(5.9))。
  3. 提供作为 C++模板的参数,以指定元素比较操作的名称 3 者比较:
  • 对象组合技术允许你在运行时改变被组合的行为,但是它存在间接性,比较低效。
  • 继承允许你提供操作的默认实现,并通过子类重定义这些操作。
  • 参数化类型允许你改变类所用到的类型。但是继承和参数化类型都不能在运行时改变。

失败的设计

必须考虑系统在它的生命周期内会发生怎样的变化:例如类的重新定义和实现,修改客户和重新测试

导致重新设计的一般原因和解决问题的相关设计模式:

  • 通过显式地指定一个类来创建对象
    • 危害:受特定实现的约束而不是特定接口的约束
    • 方法:应该间接地创建对象
    • 模式:Abstract Factory(3.1),Factory Method(3.3), Prototype(3.4)
  • 对特殊操作的依赖
    • 危害:完成该请求的方式就固定下来了
    • 方法:在编译时或运行时很方便地改变响应请求的方法
    • 模式:Chain of Resposibility(5.1),Command(5.2)
  • 对软硬件平台的依赖
    • 模式:Abstract Factory(3.1),Bridge(4.2)
  • 对对象实现的依赖
    • 危害:知道对象怎样表示、保存、定位或实现的客户在对象发生变化时可能也需要变化
    • 方法:对客户隐藏这些信息能阻止连锁变化
    • 模式:Abstract Factory(3.1),Bridge(4.2), Memento(5.6),Proxy(4.7)
  • 算法依赖
    • 危害:算法在开发和复用时常常被扩展、优化和替代。
    • 方法:有可能发生变化的算法应该被孤立起来
    • 模式:Builder(3.2),Iterator(5.4),Strategy(5.9), Template Method(5.10),Visitor(5.11)
  • 紧耦合
    • 危害:紧耦合的类很难独立地被复用
    • 方法:设计模式使用抽象耦合和分层技术来提高系统的松散耦合性。
    • 模式:Abstract Factory(3.1),Command(5.2), Facade(4.5),Mediator(5.5),Observer(5.7),Chain of Responsibility(5.1)
  • 通过生成子类来扩充功能
    • 危害:每一个新类都有固定的实现开销(初始化、终止处理等)。定义子类还需要对父类有深入的了解。
    • 方法:多用对象组合
    • 模式:Bridge(4.2),Chain of Responsibility(5.1), Composite(4.3),Decorator(4.4),Observer(5.7),Strategy(5.9)
  • 不能方便地对类进行修改(例如你需要源代码而又没有,或者可能对类的任何改变会要求修改许多已存在的其他子类)
    • 模式:Adapter(4.1),Decorator(4.4),Visitor(5.11)

设计模式在软件开发中的作用

应用程序:内部复用性、可维护性和可扩充性是要优先考虑的。

  • 内部复用性确保你不会做多余的设计和实现。设计模式通过减少依赖性来提高内部复用性。
  • 当设计模式被用来对系统分层和限制对平台的依赖性时,它们还会使一个应用更具可维护性。
  • 如果一个类不过多地依赖其他类,扩充这个孤立的类还是很容易的。

工具箱:工具箱是一组相关的、可复用的类的集合,这些类提供了通用的功能。

  • 避免假设和依赖就变得很重要,否则会限制工具箱的灵活性,进而影响它的适用性和效率

框架:框架规定了你的应用的体系结构。它定义了整体结构,类和对象的划分,各部分的主要责任,类和对象怎么协作,以及控制流程。

  • 框架更强调设计复用,尽管框架常包括具体的立即可用的子类。
  • 当使用工具箱(或传统的子程序库)时需要写应用软件的主体并且调用你想复用的代码。
  • 使用框架时,你应该复用应用的主体,写主体调用的代码。

如果说应用程序难以设计,那么工具箱就更难了,而框架则是最难的。

  • 当框架演化时,应用不得不随之演化。这使得松散耦合更加重要,否则框架的一个细微变化都将引起强烈反应。设计的框架必须尽可能地灵活、可扩充,并避免对已有内容的实质性修改。