什么是设计模式
每一个模式描述了一个在周围不断重复发生的问题,以及该文件的解决方案的核心。这样呢就能一次又一次地使用该方案而不必做重复劳动。设计模式是对被用来在特定场景下解决一般设计问题的类和互相通信的对象的描述。
模式的四个要素:
- 模式的名称。
- 问题。描述应该在何时使用模式。解释了设计问题和问题存在的前因后果,可能描述了特定的设计问题,包含使用模式必须满足的一系列先决条件。
- 解决方案。描述了设计的组成部分,它们间的相互关系以及格子的职责和协作方式。
- 效果。描述了模式应用的效果以及使用模式应该权衡的问题。包括对时间、空间的衡量,对系统灵活性、扩展性、可移植性的影响。
描述设计模式
- 模式名和分类
- 意图。设计模式是做什么的;它的基本原理和意图是什么;它解决的是什么样的特定设计问题
- 别名
- 动机。说明一个设计问题以及如何用模式中的类、对象来解决该问题的特定情境。帮助理解随后对模式更抽象的描述
- 适用性。
- 什么情况下可以使用该设计模式
- 适用案例
- 该模式可以用来改进哪些不良设计
- 怎样识别这些情况
- 结构。
- 参与者。设计模式中的类或对象以及他们各自的职责
- 协作。模式的参与者怎样协作已实现他们的职责
- 效果。模式怎样支持它的目标;使用模式的效果和所需要做的权衡取舍;系统结构哪些方面可以独立改;
- 实现。实现模式时需要知道的一些提示、技术要点以及代码示例
- 已知应用。实际系统中发现的模式的例子,每个模式至少包括了两个不同领域的实例
- 相关模式、与这个模式紧密相关的模式有哪些,其间重要的不同之处是什么,这个模式应该与哪些其他模式一起使用
设计模式的编目
详见:设计模式:目录
设计模式怎样解决设计问题
寻找合适的对象
客户请求是使对象执行操作的唯一方法,操作又是对象改变内部数据的唯一方法。对象的内部状态是被封装的,不能被直接访问,它的表示对于对象外部是不可见的。
面向对象设计最困难的部分是将系统分解成对象集合,因为要考虑许多因素:封装、粒度、依赖关系、灵活性、性能、演化、复用等。它们都影响系统的分解,并且这些因素通常还是互相冲突的。
设计方法
面向对象设计方法学支持很多设计方法
- 可以写出一个问题描述,挑出名词、动词进而创建相应的类和操作;
- 关注系统的协作与职责关系
- 对现实世界进行建模,再将分析时发现的对象转化至设计中等等
设计的许多对象来源于现实世界的分析模型,但是设计结果所得到的的类通常在现实世界中并不存在。设计模式帮助确定并不明显的抽象和描述这些抽象的对象。
决定对象的粒度
对象在大小和数目上变化极大。它们能表示下自硬件,上自整个应用的任何事物,如何决定一个对象应该是什么呢?
Facade描述了怎样用对象表示完整的子系统
Flyweight描述了如何支持大量的最小粒度的对象
visitor和command生成的对象专门负责实现对其他对象或对象组的请求
指定对象接口
对象声明的每一个操作指定操作名、作为参数的对象和返回值,构成了操作的型构。对象操作所定义的所有操作型构的集合被称为该对象的接口。
设计模式通过确定接口的主要组成成分以及经接口发送的数据类型,帮助定义接口。设计模式也指定了接口间的关系,如要求一些类具有相似的接口或对一些类的接口做了限制。
运用复用机制
继承和组合的比较
复用最常用的技巧即继承与组合。
- 继承允许根据其他类的实现来定义一个类的实现,这种通过生成子类的复用通常被称为白箱(父类的内部细节对子类可见)复用
- 编译时静态定义,可直接使用。较方便地改变被复用的实现
- 无法再运行时刻改变从父类继承的实现。父类至少定义了部分子类的具体表示,并且破坏了封装性。他们之间具有强依赖关系。一定程度上限制了灵活性与复用性
- 新的复杂功能通过组装或组合对象来获得,要求被组合的对象具有良好定义的接口。称为黑箱复用(内部不可见)
- 运行时刻动态确定。要求对象遵守彼此的接口约定,要求更仔细定义接口。通过接口访问,依赖关系较少。有助于保持每个类被封装
委托
委托是一种组合方法,使得组合具有与继承同样的复用能力。委托方式下,有两个对象参与处理一个请求,接受请求的对象将操作委托给它的代理者。
即类似于子类将请求交给它的父类进行处理。在使用继承时,被继承的操作总能够引用接受请求的对象,则接受请求的对象将自己传给被委托者(代理者),使得被委托的操作可以引用接受请求的对象。
缺陷
- 动态的、高度参数化的软件比静态软件更难于理解。
- 当委托使得设计比较简单而不是复杂时,才是更好地选择
设计模式:
- state,一个对象将请求委托给一个描述当前状态的state进行处理
- strategy,一个对象将一个特定的请求委托给一个描述请求执行策略的对象,一个对象只有一个状态,但对不同的请求是有很多策略
- visitor,对象结构的每个元素上的操作都总是被委托到visitor
关联运行时刻和编译时刻的结构
代码结构在编译时刻就被确定下来了,它由继承关系固定的类组成。程序的运行时刻结构是由快速变化的通信对象组成。
聚合意味着一个对象拥有另一个对象或对一个另对象负责,意味着聚合对象和其所有者具有相同的生命周期。关联意味着一个对象仅仅知道另一个对象,关联的对象可能请求彼此的操作,但是不为对方负责,表示较松散的耦合。
设计应支持变化
获得最大限度复用的关键在于对新需求和已有需求发生变化时的预见性,要求系统设计能够相应地改进。因此要考虑在系统的生命周期内会发生怎样的变化。
设计模式可以确保系统能以特定方式变化,从而帮助避免重新设计系统,每一个设计模式允许系统结构的某个人方面的变化独立于其他方面
导致系统重新设计的一般原因
显式指定一个类来创建对象。
- 在创建对象时指定类名将使得你受特定实现的约束而不是特定接口的约束,使得未来变化更复杂,应该间接创建对象
- factory、prototype
对特殊操作的依赖
- 当为请求指定一个特殊的操作时,完成该请求的方式就固定下来了,为了避免将代码写死。你将可以在编译期或运行期很方便改变响应请求的方法
- chainof resposibility 、command
对硬件和软件平台的依赖
- 外部的操作系统接口和API在不同的平台上是不同的,依赖于特定平台的软件将很难移植,因此设计系统时限制其平台相关性就很重要
- factory、bridge
对对象表示或实现的依赖
- 知道对象怎样表示、保存、定位或实现的客户在对象发生变化时可能也需要变化。对客户隐藏这些信息能阻止连锁变化
- factory、bridge、memento、proxy
算法依赖
- 算法在开发和复用时常常被扩展、优化、代替。依赖于某个特定算法的对象在算法发生变化时不得不变化。因此有可能发生变化的算法应该被孤立起来。
- Builder、Iterator、Starategy、Template Method、Visitor
紧耦合
- 紧耦合的类很难独立地被复用,因为他们是相互依赖的。紧耦合产生的系统,要改变或删掉一个类必须理解 和改变其他很多类。这样系统很难维护。实现松耦合很必要
- factory、command、facade、mediator、observer、chain of responsibility
通过生成子类来扩展功能
- 通常很难通过子类来定制对象,每一个新类都有固定的实现开销。定义子类需要对父类有深入的理解。重定义一个操作可能需要重定义其他操作,即使一个简单的扩充也不得不引入许多新的子类
- 使用组合、委托是继承外的灵活方式。但是过多使用对象组合会使得设计难以理解。可以去定义一个子类,将它的实例和已经存在的实例进行组合来引进定制的功能
- bridge、chain of responsibility、composite、decorator、observer、strategy
不能方便地对类进行修改
- 有时不得不改变一个难以修改的类,对于商业类库(没有源代码),或可能对类的任何修改会要求修改许多已经存在的其他子类。
- adapter、Decorator、Visitor
如何选择设计模式
考虑设计模式是怎样解决问题的
从上一节:设计模式怎样解决设计问题研究
浏览模式的意图部分
- 目的准则,即模式用来完成什么工作
- 创建型模式与对象创建有关
- 结构型模式处理类或对象的组合
- 行为型模式对类或对象怎么样交互和怎样分配职责进行描述
- 范围准则,指定模式主要是用于类还是用于对象。
- 类模式处理类和子类间的关系,通过继承建立,是静态的,编译期确定
- 对象模式处理对象间关系,是动态的。
- 创建型类模式将对象的部分创建工作延迟到子类
- 创建型对象模式将它延迟到另一个对象中
- 结构型类模式使用继承机制来组合类
- 结构型对象模式描述了对象的组装方式
- 行为型类模式使用继承描述算法和控制流
- 行为型对象模式描述一组对象怎样协作完成单个对象无法完成的任务
研究模式怎样相互关联
研究以获得合适的模式或模式组
研究目的相似的模式
创建型、行为型、结构型模式
检查重新设计的原因
从设计应当支持变化研究引起重新设计的原因,再观察问题是否与它们有关,然后再找出哪些模式可以帮助避免这些会导致重新设计的因素
考虑设计中哪些是可变的
- 考虑想要什么变化却又不会引起重新设计,即封装变化的概念。
- 下表是设计模式允许独立变化的方面,改变它们而不会导致需要重新设计。
怎样使用设计模式
- 大致浏览一遍模式。特别注意其适用性部分和效果部分,确定适合你的问题
- 研究结构部分、参与者部分和协作部分。确保理解这个模式的类和对象以及它们是怎样关联的
- 看代码示例部分。看模式代码如何具体实现
- 选择模式参与者的名字。使他们在应用上下文当中有意义。并且将模式作为后缀是有意义的
- 定义类。声明他们的接口,建立继承关系,定义代表数据和对象引用的实例变量
- 定义模式中专用于应用的操作名称。
- 实现执行模式中的责任和协作的操作。