设计模式解决设计问题的方式

一、寻找合适的对象

面向对象设计最困难的部分是将系统分解成对象集合。

因为要考虑许多因素:封装、粒 度、依赖关系、灵活性、性能、演化、复用等等,它们都影响着系统的分解,并且这些因素 通常还是互相冲突的。

面向对象设计方法学支持许多设计方法。

你可以写出一个问题描述,挑出名词和动词, 进而创建相应的类和操作;或者,你可以关注于系统的协作和职责关系;或者,你可以对现 实世界建模,再将分析时发现的对象转化至设计中。

设计的许多对象来源于现实世界的分析模型。

但是,设计结果所得到的类通常在现实世 界中并不存在,有些是像数组之类的低层类,而另一些则层次较高。

例如,Composite(4.3) 模式引入了统一对待现实世界中并不存在的对象的抽象方法。

严格反映当前现实世界的模型并不能产生也能反映将来世界的系统。

设计中的抽象对于产生灵活的设计是至关重要的。

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

例如,描述过程或算法的对 象现实中并不存在,但它们却是设计的关键部分。

Strategy(5.9) 模式描述了怎样实现可互换的 算法族。

State(5.8)模式将实体的每一个状态描述为一个对象。

这些对象在分析阶段,甚至在 设计阶段的早期都并不存在,后来为使设计更灵活、复用性更好才将它们发掘出来。

二、决定对象的粒度

其他一些设计模式描述了将一个 对象分解成许多小对象的特定方法。

三、指定对象接口

对象接口

对象声明的每一个操作指定操作名、作为参数的对象和返回值,这就是所谓的操作的型构(signature)。

对象操作所定义的所有操作型构的集合被称为该对象的接口(interface)。

对象接口描述了该对象所能接受的全部请求的集合,任何匹配对象接口中型构的请求都可以发送 给该对象。

类型

类型(type)是用来标识特定接口的一个名字。

如果一个对象接受“Window”接口所定义 的所有操作请求,那么我们就说该对象具有“Window”类型。

对象接口 VS 类型

一个对象可以有许多类型,并 且不同的对象可以共享同一个类型。对象接口的某部分可以用某个类型来刻画,而其他部分 则可用其他类型刻画。两个类型相同的对象只需要共享它们的部分接口。接口可以包含其他 接口作为子集。当一个类型的接口包含另一个类型的接口时,我们就说它是另一个类型的子 类型(subtype),另一个类型称之为它的超类型(supertype)。我们常说子类型继承了它的超类型 的接口。

设计模式 VS 接口

设计模式通过确定接口的主要组成成分及经接口发送的数据类型,来帮助你定义接口。

设计模式也许还会告诉你接口中不应包括哪些东西。

Memento(5.6)模式是一个很好的例子, 它描述了怎样封装和保存对象内部的状态,以便一段时间后对象能恢复到这一状态。它规定 了Memento对象必须定义两个接口:一个允许客户保持和复制memento的限制接口,和一个只 有原对象才能使用的用来储存和提取memento中状态的特权接口。

设计模式也指定了接口之间的关系。特别地,它们经常要求一些类具有相似的接口;或 它们对一些类的接口做了限制。

例如,Decorator(4.4)和Proxy(4.7)模式要求Decorator和Proxy 对象的接口与被修饰的对象和受委托的对象一致。而Visitor(5.11)模式中,Visitor接口必须反 映出visitor能访问的对象的所有类。

四、描述对象的实现

对象

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

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

类继承(class inheritance)

新的类可以由已存在的类通过类继承(class inheritance)来定义。

当子类(sub class)继承父类(parent class)时,子类包含了父类定义的所有数据和操作。

子类的实例对象包含所有子类和父类定义的数据,且它们能完成子类和父类定义的所有操作。

抽象类(abstract class)

抽象类(abstract class)的主要目的是为它的子类定义公共接口。

一个抽象类将把它的部分 或全部操作的实现延迟到子类中,因此,一个抽象类不能被实例化。

在抽象类中定义却没有 实现的操作被称为抽象操作(abstract operation)。

非抽象类称为具体类(concrete class)。

重定义(override)

子类能够改进和重新定义它们父类的操作。

更具体地说,类能够重定义(override)父类定 义的操作,重定义使得子类能接管父类对请求的处理操作。

类继承允许你只需简单的扩展其 他类就可以定义新类,从而可以很容易地定义具有相近功能的对象族。

类继承与接口继承的比较

一个对象的类定义了对象是怎样实现的,同时也定义了对象的内部状态和操作的实现。

但是对象的类型只与它的接口有关,接口即对象能响应的请求的集合。

一个对象可以有多个类型,不同类的对象可以有相同的类型。

类继承与接口继承的区别
对接口编程,而不是对实现编程

只根据抽象类中定义的接口来操纵对象有以下两个好处:

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

不将变量声明为某个特定的具体类的实例对象,而是让它遵从抽象类所定义的接口。

五、运用复用机制

1. 继承和组合的比较

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

2. 委托

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

在委托方式下,有两个对象参与处理一个请求,接受请求的对象将操作委托给它的代理者 (delegate)。

这类似于子类将请求交给它的父类处理。

委托的主要优点在于它便于运行时刻组合对象操作以及改变这些操作的组合方式。

3. 继承和参数化类型的比较

另一种功能复用技术(并非严格的面向对象技术)是参数化类型(parameterized type)。

它允许你在定义一个类型时并不指定该类型所用到的其他所有类型。未经指定的类型在使用时以参数形式提供。

4. 比较

哪一种方法最佳,取决于你设计和实现的约束条件。

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

一个面向对象程序运行时刻的结构通常与它的代码结构相差较大:

七、设计应支持变化

获得最大限度复用的关键在于对新需求和已有需求发生变化时的预见性,要求你的系统设计要能够相应地改进。

为了设计适应这种变化、且具有健壮性的系统,你必须考虑系统在它的生命周期内会发生怎样的变化。

设计模式可以确保系统能以特定方式变化,从而帮助你避免重新设计系统。

每一个设计模式允许系统结构的某个方面的变化独立于其他方面,这样产生的系统对于某一种特殊变化将更健壮。

导致重新设计的一般原因

下面阐述了一些导致重新设计的一般原因,以及解决这些问题的设计模式:

  1. 通过显式地指定一个类来创建对象在创建对象时指定类名将使你受特定实现的约束而不是特定接口的约束。这会使未来的变化更复杂。要避免这种情况,应该间接地创建对象。 设计模式:AbstractFactory(3.1),FactoryMethod(3.3),Prototype(3.4)。
  2. 对特殊操作的依赖当你为请求指定一个特殊的操作时,完成该请求的方式就固定下来了。为避免把请求代码写死,你将可以在编译时刻或运行时刻很方便地改变响应请求的方法。
    设计模式:ChainofResposibility(5.1),Command(5.2)。
  3. 对硬件和软件平台的依赖外部的操作系统接口和应用编程接口(API)在不同的软硬件平台上是不同的。依赖于特定平台的软件将很难移植到其他平台上,甚至都很难跟上本地平台的更新。所以设计系统时限制其平台相关性就很重要了。
    设计模式:AbstractFactory(3.1),Bridge(4.2)。
  4. 对对象表示或实现的依赖知道对象怎样表示、保存、定位或实现的客户在对象发生变化时可能也需要变化。对客户隐藏这些信息能阻止连锁变化。
    设计模式:AbstractFactory(3.1),Bridge(4.2),Memento(5.6),Proxy(4.7)
  5. 算法依赖算法在开发和复用时常常被扩展、优化和替代。依赖于某个特定算法的对象在算法发生变化时不得不变化。因此有可能发生变化的算法应该被孤立起来。
    设计模式:Builder(3.2),Iterator(5.4),Strategy(5.9),TemplateMethod(5.10),Visitor(5.11)
  6. 紧耦合紧耦合的类很难独立地被复用,因为它们是互相依赖的。紧耦合产生单块的系统,要改变或删掉一个类,你必须理解和改变其他许多类。这样的系统是一个很难学习、移植和维护的密集体。松散耦合提高了一个类本身被复用的可能性,并且系统更易于学习、移植、修改和扩展。设计模式使用抽象耦合和分层技术来提高系统的松散耦合性。
    设计模式:AbstractFactory(3.1),Command(5.2),Facade(4.5),Mediator(5.5),Observer(5.7),ChainofResponsibility(5.1)。
  7. 通过生成子类来扩充功能通常很难通过定义子类来定制对象。每一个新类都有固定的实现开销(初始化、终止处理等)。定义子类还需要对父类有深入的了解。如,重定义一个操作可能需要重定义其他操作。一个被重定义的操作可能需要调用继承下来的操作。并且子类方法会导致类爆炸,因为即使对于一个简单的扩充,你也不得不引入许多新的子类。一般的对象组合技术和具体的委托技术,是继承之外组合对象行为的另一种灵活方法。新的功能可以通过以新的方式组合已有对象,而不是通过定义已存在类的子类的方式加到应用中去。另一方面,过多使用对象组合会使设计难于理解。许多设计模式产生的设计中,你可以定义一个子类,且将它的实例和已存在实例进行组合来引入定制的功能。
    设计模式:Bridge(4.2),ChainofResponsibility(5.1),Composite(4.3),Decorator(4.4),Observer(5.7),Strategy(5.9)。
  8. 不能方便地对类进行修改有时你不得不改变一个难以修改的类。也许你需要源代码而又没有(对于商业类库就有这种情况),或者可能对类的任何改变会要求修改许多已存在的其他子类。设计模式提供在这些情况下对类进行修改的方法。
    设计模式:Adapter(4.1),Decorator(4.4),Visitor(5.11)。

设计模式在开发中所起的作用

应用程序

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

工具箱

工具箱( Toolkit )的设计比应用设计要难得多,因为它要求对许多应用是可用的和有效的。

再者, 工具箱的设计者并不知道什么应用使用该工具箱及它们有什么特殊需求。

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

框架

框架(Framework)是构成一类特定软件可复用设计的一组相互协作的类。

框架规定了你的应用的体系结构。它定义了整体结构,类和对象的分割,各部分的主要 责任,类和对象怎么协作,以及控制流程。框架预定义了这些设计参数,以便于应用设计者 或实现者能集中精力于应用本身的特定细节。框架记录了其应用领域的共同的设计决策。因而框架更强调设计复用,尽管框架常包括具体的立即可用的子类。

模式和框架的不同