Loading

GoF设计模式——构建型设计模式

前言

最近在拜读GoF设计模式这本书。理解起来还是有点费劲的,不知道是中文版翻译的原因还是啥。所以,这里我尽量把书里的话说的简单一点,但说实话,这本书不适合作为设计模式初学者的第一本书,它适合已经了解了设计模式并且有过一些在实际项目中对设计模式的体验,想要复习或更加精进的掌握设计模式的人。

设计模式本身只是一种解决在设计中重复出现的问题的通用模式,所以解决问题就行,千万不要把设计模式看得太死,比如看到Builder模式就想起链式调用,没有链式调用你就不认识那是Builder模式,而却忘了Builder模式解决的主要问题是复杂对象的创建。如果你读此书的时候(包括本笔记)怀着XXX模式就应该是什么样的偏见来看的话,那你可能越看越迷惑,然后问出“这跟我知道的XXX模式怎么看起来大相径庭”这种话。

我之所以说上面那段话是因为我在读此书的时候不断在犯上述错误。书中大量使用了接口,甚至连简单的工厂方法模式也都是基于接口的,接口促进了面向对象编程中组件间的解耦——一个组件并不需要知道为它服务的组件的具体实现,服务组件对它来说是透明的。所以,即使最简单的设计模式看起来也稍显复杂,从外观上看和你所知道的设计模式可能差得远,遇到这种情况,坐下来,好好想想,你所知道的XXX模式和书中介绍的XXX模式是不是在解决同样的问题,别被复杂的表象所迷惑。

而你在使用设计模式时,只要解决了你的问题,符合了你的需求,你也可以在不破坏重用性的前提下把它写的和所有人都不一样。你可以去掉一些东西,加上一些东西......

废话太多,开始。

抽象工厂模式(Abstract Factory)

当你要向程序中的其它部分提供一系列相关的组件,并且你可能需要替换到另一批相关组件时,抽象工厂模式就正好适用。

动机

假设你有一个GUI应用程序,你需要利用特定的GUI库来创建窗口和滚动条。

普通工厂模式很方便的就能处理这种需求,比如你基于MotifGUI库来构建你的应用,你可能创建如下的工厂:

class MotifWidgetFactory {
    MotifWindow createWindow() {}
    MotifScrollbar createScrollbar() {}
}

由于不同GUI库的性能、设计风格都不同,你可能希望在后期更换其它的GUI库或支持动态更换GUI库。想象你最初使用一款称作Motif的GUI库来构建窗口和滚动条,并且你预料到后期你可能会使用其他GUI库,如何才能快捷透明的切换到另一种GUI库上?

你肯定要建立接口,你要向程序的其它部分隐藏所有与特定GUI库相关的细节,像MotifWidgetFactoryMotifWindowMotifScrollbar这些东西不可以被程序的其它部分所知。一旦程序的其它部分直接引用了特定于某种实现的相关的API,在切换到其它API时,这些部分都会变成你的麻烦,你必须一处处的修改它们。

所以,我们首先要把特定于MotifGUI库的工厂MotifWidgetFactory给抽象了,我们需要建立一种与特定实现无关的工厂——WidgetFactory,这也就是抽象工厂。其次,我们还要创建与特定实现无关的WindowScrollbar接口。下面是使用抽象工厂模式来实现这个需求的UML图:

img

这样,程序只知道WidgetFactoryWindowScrollbar,并不关心当前使用的实现是基于Motif的还是PM的,所以当你需要切换到另一种GUI库时,你几乎不用改动什么,直接使用另一种WidgetFactory的实现替换之前的实现即可。

适用性

当你要向程序中的其它部分提供一系列相关的组件,并且你可能需要替换到另一批相关组件时,抽象工厂模式就正好适用。

结构

img

参与者

在此部分,给出该设计模式中的关键组件,为了便于练习,我不会将这里所述的组件与上面示例中的组件一一对应,你需要自己思考并对号入座。如果不确定,再往下一点就是答案。

  • AbstractFactory: 抽象工厂接口,用于隐藏具体使用哪种产品系列
  • ConcreteFactory:抽象工厂的实现类,用于构建具体的产品系列
  • AbstractProduct:抽象产品接口,用于隐藏具体使用的是哪种产品系列中的产品
  • ConcreteProduct:抽象产品的实现类,代表具体的产品系列中的具体产品类型
  • Client:使用抽象工厂的代码
AbstractFactory: WidgetFactory
ConcreteFactory: MotifWidgetFactory, PMWidgetFactory
AbstractProduct: Window, Scrollbar
ConcreteProduct: MotifWindow, MotifScrollbar, PMWindow, PMScrollbar

构建者模式(Builder)

构建者(Builder)提供一种构造对象的方式,你可以调用构建者中的方法对对象进行各种灵活的配置,并最后通过一个方法来获得最终构造好的对象

动机

假设你在设计一个RTF(Rich Text Format)阅读器,其中的一个需求是将RTF格式的文档转换成各种其它格式、比如纯文本格式、TeX格式、甚至是转换成一个能以交互方式编辑的正文窗口组件。

我们先假设格式转换的过程是先用RTFReader类来读出RTF文档中的每一个token(文法解析中的token,可以理解为单词,但并不是我们平时所说的word),然后根据目标格式的不同,来对这个token做不同的处理,比如如果目标格式是文本格式,那它可能忽略所有的样式token,比如段落啊、字体更改啊...当单词都读完并转换完毕后,目标格式的文件也就生成了。

首先可以确定的是,每种格式转换的代码不可能固化在RTFReader中,因为随着阅读器的开发,你可能希望它能够转换越来越多的格式,你不希望每添加一种格式就要大幅修改RTFReader的代码。所以,RTFReader一定是不知道格式转换的细节的(即到底以什么方式转换成哪种目标格式)。

我们可以抽象出一个TextConverter接口,并传递给RTFReaderRTFReader读取每个token并交给TextConverter进行转换,最终得到对应格式的文档。UML图如下:

img

  1. ASCIIConverter用于将RTF文档转换为纯文本,它只实现了对于CHAR类型token的转换,对于其它类型的token,它都忽略了。因为文本文件本来就没有样式。
  2. TeXConverter用于将RTF文档转换为TeX文档,它实现了TextConverter的所有方法,也就是对于所有的token,不管是文字还是样式改动,都会反映到转换后的TeX文档中。
  3. TextWidgetConverter用于将RTF文档转换为一个可编辑的文档窗口,并且保留所有样式。

由于RTFReader并不关心要转换的目标格式以及具体的逻辑,它只是规规矩矩的调用TextConverter,所以你可以很轻松的添加HTMLConverterDOCXConverterMarkdownConverter......

适用性

不像抽象工厂模式,我们在动机里几乎没怎么介绍构建者模式的适用情况,只是拿出来了一个示例来讲解。而且上面的示例其实有点复杂,构建者模式并不需要这么复杂。

我们平常在开发中看到的构建者都是这样的:

Product p = new XXXBuilder()
    .addXXX()
    .setXXX()
    .build();

简单来说就是创建一个对象需要很多步骤,而且这个对象的配置很灵活,甚至有些时候是动态的,如果用构造器,工厂方法等来构造它会很笨拙甚至根本不可能。然后构建者(Builder)提供一种构造该对象的方式,你可以调用构建者中的方法对对象进行各种灵活的配置,并最后通过一个方法来获得最终构造好的对象大部分构建者模式都提供了链式调用

上面的任何一个Converter单独拿出来都可以称为一个构建者,RTFReader会动态的调用它来构建转换后的文档,并且每一个构建器都提供了一个额外的方法来获得最终转换后的文档。

而上面的庞大的示例,我们不妨把它称作抽象构建者模式,就和抽象工厂模式和工厂模式的关系一样,只不过把构建器类又抽象了一层,向代码中的其它部分隐藏当前使用的是什么构建器。

所以,构建者模式,或者是抽象构建者模式的适用性我们可以总结如下:

  1. 当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时
  2. 当构造过程必须允许被构造的对象有不同的表示时

结构

img

参与者

在此部分,给出该设计模式中的关键组件,为了便于练习,我不会将这里所述的组件与上面示例中的组件一一对应,你需要自己思考并对号入座。如果不确定,再往下一点就是答案。

  • Builder:抽象的构建者类,用于向其它组件隐藏当前正在构建的对象的具体实现细节
  • ConcreteBuilder:具体的构建者类,用于构建具体的对象
  • Director:使用构建者来构造目标对象的组件,也就是Builder的调用者
  • Product:Builder所最终构建出的产物,也就是上面所说的目标对象
Builder: TextConverter
ConcreteBuilder: ASCIIConverter, TeXConverter, TextWidgetConverter
Director: RTFReader
Product: ASCIIText, TeXText, TextWidget

工厂方法模式(Factory Method)

创建某个组件的具体实现的方法就叫工厂方法,它向应用的其它部分隐藏了到底创建哪个具体实现

动机

想象,有这样一个应用程序框架,它提供Application抽象类,代表一个应用。如果你使用这个框架开发,你需要继承Application抽象类。一个Application的职能就是,管理多个DocumentDocument代表某种类型的文档,Application可以对Document进行创建、打开等操作。

一个该框架的用例就是一个画图应用程序,你可能需要编写自己的DrawingApplicationDrawingDocument

现在考虑抽象类Application中创建Document的方法,因为具体创建哪个Document和具体的Application实现类有关,抽象类中不可能知道应该创建什么Document

这时,可以在Application中提供一个抽象方法,这个抽象方法由具体的Application实现类来编写,它的作用是创建具体的Document,因为只有实现类才知道该创建什么Document。这个创建某个组件的具体实现的方法就叫工厂方法,它向应用的其它部分隐藏了到底创建哪个具体实现

img

适用性

  1. 当一个类不知道它所必须创建的对象的类的时候。
  2. 当一个类希望由它的子类来指定它所创建的对象的时候。
  3. 当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。

结构

img

参与者

在此部分,给出该设计模式中的关键组件,为了便于练习,我不会将这里所述的组件与上面示例中的组件一一对应,你需要自己思考并对号入座。如果不确定,再往下一点就是答案。

  • Product:要创建的具体组件的接口,用于向应用中其它部分屏蔽创建出的组件的具体实现
  • ConcreteProduct:实际创建的具体组件实现
  • Creator:创建者,声明工厂方法,返回一个具体组件接口,等待子类实现并返回具体的组件。也可以返回一个缺省的具体组件实现
  • ConcreteCreator:具体创建者,实现工厂方法,返回具体的组件实现。
Product: Document
ConcreateProduct: MyDocument
Creator: Application
ConcreteCreator: MyApplication

原型模式(Prototype)

提供一个已经创建好的对象作为“原型”,然后通过对原型对象进行克隆来构建新的对象,这就是原型模式。

动机

现在有这么一个绘图框架。它提供如下接口:

  1. Graphic:代表一个图形,其中的draw(position)方法将自己绘制到指定位置
  2. Tool: 代表某种操作图形的工具

然后,它还预定义了一个用于创建某种图形对象并插入到文档中的工具GraphicTool

GraphicTool显然不知道它要创建的Graphic的具体类型,它显然不知道如何创建它。具体的Graphic实现才最了解如何创建自己。我们可以创建一批Graphic的原型,然后在GraphicTool创建一个图形时,传递一个原型给它,GraphicTool通过拷贝或者说克隆该原型来创建一个具体图形的实例。

所以,作为原型的那些类(也就是Graphic)一定要支持克隆,并且克隆的逻辑由具体实现类来实现。下面的UML图中,StaffMusicalNote等都是一个个图形,它们都有一个Clone方法来克隆自己。

img

提供一个已经创建好的对象作为“原型”,然后通过对原型对象进行克隆来构建新的对象,这就是原型模式

适用性

  • 当一个系统应该独立于它的产品创建、构成和表示时,要使用 P r o t o t y p e模式
  • 当要实例化的类是在运行时刻指定时,例如,通过动态装载
  • 为了避免创建一个与产品类层次平行的工厂类层次时
  • 当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们 可能比每次用合适的状态手工实例化该类更方便一些。

结构

img

参与者

在此部分,给出该设计模式中的关键组件,为了便于练习,我不会将这里所述的组件与上面示例中的组件一一对应,你需要自己思考并对号入座。如果不确定,再往下一点就是答案。

  • Prototype: 原型接口,实现者可以克隆出一个自己并向外界发布
  • ConcretePrototype:原型实现
  • Client:使用原型来得到克隆后的对象的组件
Prototype: Graphic
ConcretePrototype: Staff, WholeNote, HalfNote
Client: GraphicTool

单例模式(Singleton)

当一个类需要保证在全局只有一个实例时使用

动机

当一个类需要保证在全局只有一个实例时使用。

适用性

  • 当类只能有一个实例而且客户可以从一个众所周知的访问点访问它时。
  • 当这个唯一实例应该是通过子类化可扩展的,并且客户应该无需更改代码就能使用一个 扩展的实例时。

结构

img

参与者

  • Singleton
posted @ 2022-06-24 12:19  yudoge  阅读(271)  评论(0编辑  收藏  举报