目录
什么是设计模式?
设计模式怎样解决设计问题?
复用的机制
运行时刻和编译时刻的结构
设计应支持变化
怎样选择设计模式
本书讲述了一些通用的设计模式,但是没有收录涉及分布式或者并发、数据库相关的设计模式。本书中的设计模式是对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述。
什么是设计模式?
一个模式有4个基本要素:①模式名称、②问题(要解决的问题及场合)、③解决方案(设计模式的组成)、④效果(该设计模式对可复用性、对时间/控件效率)
理解设计模式,重点关注所含类和实例的角色、协作方式、职责分配,以及“在什么时候使用”、“在另外一些约束下是否还可使用”。
设计模式怎样解决设计问题?
①寻找适合的对象②决定对象的粒度③指定对象的接口④描述对象的实现
对象接口(interface)描述了该对象所能接受的全部请求的集合。类型type是用来标识特定接口(interface)的一个名字。如果一个对象接受“Window”接口所定义的所有操作请求,那么我们就说该对象具有“Window”类型。
类型type只与接口有关。一个对象可以有多个类型,不同类的对象可以有相同的类型。
类继承与接口继承(又称为子类型化)有哪些不同?类继承根据一个对象的实现定义了另一个对象。简而言之,它是代码和表示的共享机制。然而,接口继承(或子类型化)描述了一个对象什么时候能被用来替代另一个对象。
C++中的纯接口继承近似于共有继承纯抽象类,纯实现继承或纯类继承类似于私有继承。
实现的复用(实现的继承)是成功的一半,接口的复用也同等重要。
只根据抽象类中定义的接口来操纵对象有以下两个好处:①客户无须知道他们使用对象的特定类型,只须对象有客户所期望的接口。②客户无须知道他们所使用的对象是什么类型来实现的,他们只须要定义接口的抽象类。
这就意味着,很多时候,我们需要针对接口编程,而不是针对实现编程(面向对象设计的第一个原则)。不将变量声明为某个特定的具体类的实例对象,而是让它遵从抽象类所定义的接口。当我们不得不在某个地方实例化具体的类(即一个特定的实现)时,创建型模式(Abstract Factory,builder,Factory Method,Prototype,Singleton)可以帮助我们。这些模型提供不同的方式,在实例化时建立接口和实现的透明链接,使得我们是针对接口进行编程。
复用的机制
1. 组合
| 继承 | 组合 |
|---|---|
| 通过生成子类实现复用 | 通过组装或组合对象来获得(通常是通过获得对其他对象的引用) |
| “白箱”复用:继承方式中,父类内部的细节对子类可见。 | “黑箱”复用:对象内部的细节不可见,只有定义良好的接口 |
| 在编译时刻就定义好了,是一种静态的方式,可直接使用 | 通过获得对其他对象的引用而在运行时刻动态绑定 |
| 好处: ①可以方便地改变复用的实现。子类可以重新定义部分操作;这些操作可能会影响所继承的操作。 |
好处: ①不破坏“封装性”,有助于保持每个类被封装,并被集中在单个任务上。这样可以减少类的继承层次,减少增长成为不可控制的庞然大物的可能。 ②只要类型一致,运行时刻可以用一个对象来替代另一个对象 ③对象的实现基于接口,实现上存在较少的依赖关系 ④基于对象组合的设计会有更多的对象(而有较少的类),系统的行为将依赖于对象之间的关系,而不是被定义在某个类中。 |
| 不足之处: ①编译时刻就定义好了,无法在运行时刻改变从父类继承的实现。 ②继承对子类揭示了其父类的实现细节,“破坏了封装性”。父类中的任何变化必然导致子类发生变化。 ③实现上的依赖会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须被重写或者被其他更合适的类替换。这种依赖关系限制了灵活性,并最终限制了复用性。 |
|
| 解决办法: 针对这些缺点,一个可用的解决办法就是只继承抽象类,因为抽象通常提供较少的实现。 |
上表导出了面向对象设计的第二个原则,优先使用对象组合,而不是类继承。
理想情况下,我们不应该为了获得复用而去创建新的构件。你应该能够只使用对象组合技术,通过组装已有的构建就能获得你所想要的功能。但是事实很少如此,因为可用构建的集合实际上并不足够丰富。使用继承的复用使得创建新的构建要比组装旧的构建来得容易。这样,继承和对象组合常一起使用。然而,作者的经验表明,设计人员通常过度使用了继承这种复用技术。但依赖于对象组合技术的设计却有更好的复用性(或更简单)。
用“窗口”和“矩形”的关系来形象说明:一个窗口拥有一个矩形,就是“组合”;一个窗口就是一个矩形,就是“继承”。
2. 委托
“委托”是一种组合方法,使组合具有与继承相同的复用能力。委托方式中,接受请求的对象将自己转给被委托者(代理人),使被委托的对象可以引用接受请求的对象。
| 优点 | 缺点 |
|---|---|
| 便于在运行时刻组合对象的操作以及改变这些操作的组合方式 | 动态的、高度参数化的软件比较难于理解。但长远看,人的低效才是更主要的。 |
| 适用情形:符合特定程式的情形,即标准模式的情形。 |
|
3.参数化类型
参数化类型,类似C++中的模板template
类继承、对象组合、参数化类型,是组合面向对象系统行为的三种方法,有很大不同,使用是需要根据实际情况选择。
运行时刻和编译时刻的结构
两个结构实际上是彼此独立的。系统的运行时刻结构更多地受到设计者的影响,而不是编程语言。
| 聚合 | 相识 |
|---|---|
| 一个对象拥有另一个对象或对另一个对象负责 | 一个对象仅仅知道另一个对象;相识的对象可能请求彼此的操作,但不为对方负责 |
| 对象和其所有者有相同的生命周期 | 相识的两个对象生命周期可能不同 |
| 弱耦合,更具动态性 |
设计应支持变化
获得最大限度复用的关键在于对新需求和已有需求发生变化时的可预见性,要求我们的系统设计要能够相应地改进。因此,我们须要考虑生命周期内会发生怎样的变化。
| 序号 | 原因 | 具体情况 | 解决问题的设计模式 |
|---|---|---|---|
| 1 | 通过显示地指定一个类来创建对象 | 在创建对象时指定类名,将使我们接受特定实现的约束,而不是特定接口的约束。这会使未来的变化更复杂;应该间接地创建对象。 | Abstract Factory、Factory Method、Prototype |
| 2 | 对特殊操作的依赖 | 当你为请求指定一个特殊的操作时,完成该请求的方式就固定下来了。如果避免把请求代码写死,我们就可以在编译时刻或者运行时刻很方便地改变响应请求的方法。 | Chains of Responsibility、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 Responsibility、Composite、Decorator、Observer、Strategy |
| 8 | 不能方便地对类进行修改 | 有时候后我们不得不改变一个难以修改的类。也许你需要源代码而又没有(对于商业类库就是这种情况),或者可能对类的改变会要求修改许多已经存在的其他子类。设计模式提供在这些情况下对类进行修改的方法。 | Adapter、Decorator、VIsitor |
| 应用程序 | 工具箱 | 框架 |
|---|---|---|
| 提高内部复用性、可维护性、可扩展性 | 提高工具箱的适用性和效率 | 提高更高层次的设计复用和代码复用,提高松散耦合的特性 |
| 区别点 | 细节 |
|---|---|
| 设计模式更加抽象 | |
| 设计模式是比框架更小的体系结构元素 | 一个典型的框架包含了很多设计模式,反之绝非如此。 |
| 框架比设计模式更加特列话 | 框架总是针对一个特定的应用领域。 |
怎样选择设计模式
①考虑设计模式是怎样解决设计问题的;②留言模式的意图部分;③研究模式怎样相互关联;④研究目的相似的模式;⑤检查重新设计的原因;⑥考虑你的设计中哪些是可变的
| 目的 | 设计模式 | 可变的方面 |
|---|---|---|
| 创建 | Abstract Factory | 产品对象家族 |
| Builder | 如何创建一个组合对象 | |
| Factory Mathod | 被实例化的子类 | |
| Prototype | 被实例化的类 | |
| Singleton | 一个类的唯一实例 | |
| 结构 | Adapter | 对象的接口 |
| Bridge | 对象的实现 | |
| Composite | 一个对象的结构和组成 | |
| Decorator | 对象的职责,不生成子类 | |
| Facade | 一个子系统的接口 | |
| Flyweight | 对象的存储开销 | |
| Proxy | 如何访问一个对象;该对象的位置 | |
| 行为 | Chain of Responsibility | 满足一个请求的对象 |
| Command | 何时、怎样满足一个请求 | |
| Interpreter | 一个语言的文法和解释 | |
| Iterator | 如何遍历、访问一个聚合的各元素 | |
| Mediator | 对象间怎样交互、和谁交互 | |
| Memento | 一个对象中哪些私有信息存放在该对象之外、以及在什么时候进行存储 | |
| Observer | 多个对象依赖于另外一个对象,而这些对象又如何保持一致 | |
| State | 对象的状态 | |
| Strategy | 算法 | |
| Template Method | 算法中的某些步骤 | |
| Visitor | 某些可以作用于一个(组)对象上的操作,但不修改这些对象的类 |
| 序号 | 方式 | 意义 |
|---|---|---|
| 1 | 大致浏览一遍模式 | 特别注意其适用性部分和效果部分,确定它适合我们的问题 |
| 2 | 回头研究结构部分、参与者部分和协作部分 | 确保我们理解这个模式的类和对象,以及他们是怎么联系的 |
| 3 | 看代码示例部分,看看具体例子 | 研究代码有助于我们实现模式 |
| 4 | 选择模式参与者的名字,使其在上下文中有意义 | 将参与者的名字和应用中出现的名字合并起来是有用过的,将有助于在实现中更显式地体现出模式来。 |
| 5 | 定义类 | |
| 6 | 定义模式中专用于应用的操作名称 | |
| 7 | 实现执行模式中责任和协作的操作 |