java设计模式概述

1.1什么是设计模式

Christopher Alexander说过:“每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动”

一般而言,一个模式有四个基本要素:

1.模式名称(pattern name) 一个助记名,它用一两个词来描述模式的问题、解决方案和效果。模式名可以帮助我们思考,便于我们与其他人交流设计思想及设计结果。找到恰当的模式名也是我们设计模式编目工作的难点之一。

2.问题(problem)描述了应该在何时使用模式。它解释了设计问题和问题存在的前因后果,它可能描述了特定的设计问题,如怎样用对象表示算法等。

3.解决方案(solution)描述了设计的组成成分,它们之间的相互关系及各自的职责和协作方式。因为模式就像一个模板,可应用于多种不同场合,所以解决方案并不描述一个特定而具体的设计或实现,而是提供设计问题的抽象描述和怎样用一个具有一般意义的元素组合

4.效果(consequences)描述了模式应用的效果及使用模式应权衡的问题。尽管我们描述

1.2 Smalltalk MVC中的设计模式

1.3描述设计模式

我们怎样描述设计模式呢?图形符号虽然很重要也很有用,却还远远不够,它们只是将设计过程的结果简单记录为类和对象之间的关系。为了达到设计复用,我们必须同时记录设计产生的决定过程、选择过程和权衡过程。具体的例子也是很重要的,它们让你看到实际的设计。

我们将用统一的格式描述设计模式,每一个模式根据以下的模板被分成若干部分。模板具有统一的信息描述结构,有助于你更容易地学习、比较和使用设计模式。

模式名和分类

模式名简洁地描述了模式的本质。一个好的名字非常重要,因为它将成为你的设计词汇表中的一部分。模式的分类反映了我们将在1 . 5节 介 绍 的 方 案 。

意图

是回答下列问题的简单陈述:设计模式是做什么的?它的基本原理和意图是什么?它解决的是什么样的特定设计问题?

别名

模式的其他名称。

动机

用以说明一个设计问题以及如何用模式中的类、对象来解决该问题的特定情景。该情景会帮助你理解随后对模式更抽象的描述。

适用性

什么情况下可以使用该设计模式?该模式可用来改进哪些不良设计?你怎样识别这些情况?

结构

采用基于对象建模技术(O M T)[ R B P + 9 1 ]的 表 示 法 对 模 式 中 的 类 进 行 图 形 描 述 。 我 们 也使用了交互图[ J C J O 9 2,B O O 9 4 ]来说明对象之间的请求序列和协作关系。附录B详细描述了这些表示法。

参与者

指设计模式中的类和/或对象以及它们各自的职责。
协作
模式的参与者怎样协作以实现它们的职责。
效果模式怎样支持它的目标?使用模式的效果和所需做的权衡取舍?系统结构的哪些方面可

以独立改变?

实现

实现模式时需要知道的一些提示、技术要点及应避免的缺陷,以及是否存在某些特定于实现语言的问题。

代码示例

用来说明怎样用C + +S m a l l t a l k实 现 该 模 式 的 代 码 片 段 。

1.4设计模式的编目

从第3章开始的模式目录中共包含2 3个 设 计 模 式 。 它 们 的 名 字 和 意 图 列 举 如 下 , 以 使 你 有个基本了解。每个模式名后括号中标出模式所在的章节(我们整本书都将遵从这个约定)

A b s t r a c t F a c t o r y( 3 . 1 ):提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。

A d a p t er ( 4 . 1 ):将一个类的接口转换成客户希望的另外一个接口。A d a p t e r模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

B r i d g e( 4 . 2 ):将抽象部分与它的实现部分分离,使它们都可以独立地变化。

B u i l d e r( 3 . 2 ): 将 一 个 复 杂 对 象 的 构 建 与 它 的 表 示 分 离 , 使 得 同 样 的 构 建 过 程 可 以 创 建 不同的表示。

C h a i n o f R e s p o n s i b i l i t y( 5 . 1 ): 为 解 除 请 求 的 发 送 者 和 接 收 者 之 间 耦 合 , 而 使 多 个 对 象 都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它。

C o m m a n d( 5 . 2 ):将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可取消的操作。

C o m p o s i t e( 4 . 3 ): 将 对 象 组 合 成 树 形 结 构 以 表 示 “ 部 分-整 体 ” 的 层 次 结 构 。C o m p o s i t e使得客户对单个对象和复合对象的使用具有一致性。

D e c o r a t o r( 4 . 4 ):动态地给一个对象添加一些额外的职责。就扩展功能而言,D e c o r a t o r模式比生成子类方式更为灵活。

F a c a d e( 4 . 5 ):为子系统中的一组接口提供一个一致的界面,F a c a d e模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

F a c t o r y M e t h o d( 3 . 3 ):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。Factory Method使一个类的实例化延迟到其子类。

F l y w e i g h t( 4 . 6 ):运用共享技术有效地支持大量细粒度的对象。

I n t e r p r e t e r( 5 . 3 ):给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。

I t e r a t o r( 5 . 4 ):提供一种方法顺序访问一个聚合对象中各个元素,而 又 不 需 暴 露 该 对 象 的内部表示。

M e d i a t o r( 5 . 5 ): 用 一 个 中 介 对 象 来 封 装 一 系 列 的 对 象 交 互 。 中 介 者 使 各 对 象 不 需 要 显 式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

M e m e n t o( 5 . 6 ): 在 不 破 坏 封 装 性 的 前 提 下 , 捕 获 一 个 对 象 的 内 部 状 态 , 并 在 该 对 象 之 外保存这个状态。这样以后就可将该对象恢复到保存的状态。

O b s e r v e r( 5 . 7 ):定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。

P r o t o t y p e( 3 . 4 ): 用 原 型 实 例 指 定 创 建 对 象 的 种 类 , 并 且 通 过 拷 贝 这 个 原 型 来 创 建 新 的 对象。

P r o x y( 4 . 7 ):为其他对象提供一个代理以控制对这个对象的访问。
S i n g l e t o n( 3 . 5 ):保证一个类仅有一个实例,并提供一个访问它的全局访问点。
S t a t e( 5 . 8 ): 允 许 一 个 对 象 在 其 内 部 状 态 改 变 时 改 变 它 的 行 为 。 对 象 看 起 来 似 乎 修 改 了 它

所属的类。
S t r a t e g y (5 . 9 ): 定 义 一 系 列 的 算 法,把 它 们 一 个 个 封 装 起 来,并 且 使 它 们 可 相 互 替 换 。 本 模

式使得算法的变化可独立于使用它的客户。
T e m p l a t e M e t h o d( 5 . 1 0 ):定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。

Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

Vi s i t o r( 5 . 11 ): 表 示 一 个 作 用 于 某 对 象 结 构 中 的 各 元 素 的 操 作 。 它 使 你 可 以 在 不 改 变 各 元

素的类的前提下定义作用于这些元素的新操作。

1.5组织编目

设计模式在粒度和抽象层次上各不相同。由于存在众多的设计模式,我们希望用一种方式将它们组织起来。这一节将对设计模式进行分类以便于我们对各族相关的模式进行引用。分类有助于更快地学习目录中的模式,且对发现新的模式也有指导作用,如表1 - 1所示。

我 们 根 据 两 条 准 则(1 - 1 )对 模 式 进 行 分 类 。 第 一 是目的准 则 , 即 模 式 是 用 来 完 成 什 么 工作 的 。 模 式 依 据 其 目 的 可 分 为创 建 型(C r e a t i o n a l)、结 构 型( S t r u c t u r a l )、 或行 为 型( B e h a v i o r a l )三种。创建型模式与对象的创建有关;结构型模式处理类或对象的组合;行为型模式对类或对象怎样交互和怎样分配职责进行描述。

第二是范围准则,指定模式主要是用于类还是用于对象。类模式处理类和子类之间的关系,这些关系通过继承建立,是静态的,在编译时刻便确定下来了。对象模式处理对象间的关系,这些关系在运行时刻是可以变化的,更具动态性。从某种意义上来说,几乎所有模式都使用继承机制,所以“类模式”只指那些集中于处理类间关系的模式,而大部分模式都属于对象模式的范畴。

创建型类模式将对象的部分创建工作延迟到子类,而创建型对象模式则将它延迟到另一个对象中。结构型类模式使用继承机制来组合类,而结构型对象模式则描述了对象的组装方式。行为型类模式使用继承描述算法和控制流,而行为型对象模式则描述一组对象怎样协作完成单个对象所无法完成的任务。

还 有 其 他 组 织 模 式 的 方 式 。 有 些 模 式 经 常 会 被 绑 在 一 起 使 用 , 例 如 ,C o m p o s i t e常和I t e r a t o rV i s i t o r一起使用;有些模式是可替代的,例如,P r o t o t y p e常用来替代A b s t r a c tF a c t o r y;有些模式尽管使用意图不同,但产生的设计结果是很相似的,例如,C o m p o s i t eD e c o r a t o r的结构图是相似的。

还有一种方式是根据模式的“相关模式”部分所描述的它们怎样互相引用来组织设计模式

客户请求是使对象执行操作的唯一方法,操作又是对象改变内部数据的唯一方法。由于这些限制,对象的内部状态是被封装的,它不能被直接访问,它的表示对于对象外部是不可见的。

面向对象设计最困难的部分是将系统分解成对象集合。因为要考虑许多因素:封装、粒度、依赖关系、灵活性、性能、演化、复用等等,它们都影响着系统的分解,并且这些因素通常还是互相冲突的。

面向对象设计方法学支持许多设计方法。你可以写出一个问题描述,挑出名词和动词,进而创建相应的类和操作;或者,你可以关注于系统的协作和职责关系;或者,你可以对现实世界建模,再将分析时发现的对象转化至设计中。至于哪一种方法最好,并无定论。

设计的许多对象来源于现实世界的分析模型。但是,设计结果所得到的类通常在现实世界中并不存在,有些是像数组之类的低层类,而另一些则层次较高。例如,C o m p o s i t e ( 4 . 3 )模式引入了统一对待现实世界中并不存在的对象的抽象方法。严格反映当前现实世界的模型并不能产生也能反映将来世界的系统。设计中的抽象对于产生灵活的设计是至关重要的。

设计模式帮你确定并不明显的抽象和描述这些抽象的对象。例如,描述过程或算法的对象现实中并不存在,但它们却是设计的关键部分。S t r a t e g y ( 5 . 9 )模 式 描 述 了 怎 样 实 现 可 互 换 的算法族。S t a t e ( 5 . 8 )模 式 将 实 体 的 每 一 个 状 态 描 述 为 一 个 对 象 。 这 些 对 象 在 分 析 阶 段 , 甚 至 在设计阶段的早期都并不存在,后来为使设计更灵活、复用性更好才将它们发掘出来。

1.6.2决定对象的粒度

对象在大小和数目上变化极大。它们能表示下自硬件或上自整个应用的任何事物。那么我们怎样决定一个对象应该是什么呢?

设计模式很好地讲述了这个问题。F a c a d e ( 4 . 5 )模式描述了怎样用对象表示完整的子系统,F l y w e i g h t ( 4 . 6 )模 式 描 述 了 如 何 支 持 大 量 的 最 小 粒 度 的 对 象 。 其 他 一 些 设 计 模 式 描 述 了 将 一 个对象分解成许多小对象的特定方法。A b s t r a c t F a c t o r y ( 3 . 1 )B u i l d e r ( 3 . 2 )产生那些专门负责生成 其 他 对 象 的 对 象 。Vi s i t o r ( 5 . 1 0 )C o m m a n d ( 5 . 2 )生 成 的 对 象 专 门 负 责 实 现 对 其 他 对 象 或 对 象组的请求。

1.6.3指定对象接口

对象声明的每一个操作指定操作名、作为参数的对象和返回值,这就是所谓的操作的型构( s i g n a t u r e )。 对 象 操 作 所 定 义 的 所 有 操 作 型 构 的 集 合 被 称 为 该 对 象 的接口( i n t e r f a c e )。对象接口描述了该对象所能接受的全部请求的集合,任何匹配对象接口中型构的请求都可以发送给该对象。

类型( t y p e )是 用 来 标 识 特 定 接 口 的 一 个 名 字 。 如 果 一 个 对 象 接 受 “W i n d o w” 接 口 所 定 义的所有操作请求,那么我们就说该对象具有“W i n d o w” 类 型 。 一 个 对 象 可 以 有 许 多 类 型 , 并且不同的对象可以共享同一个类型。对象接口的某部分可以用某个类型来刻画,而其他部分则可用其他类型刻画。两个类型相同的对象只需要共享它们的部分接口。接口可以包含其他接口作为子集。当一个类型的接口包含另一个类型的接口时,我们就说它是另一个类型的子类型( s u b t y p e ), 另 一 个 类 型 称 之 为 它 的超类型( s u p e r t y p e )。 我 们 常 说 子 类 型继承了 它 的 超 类 型的接口。


在面向对象系统中,接口是基本的组成部分。对象只有通过它们的接口才能与外部交流,如果不通过对象的接口就无法知道对象的任何事情,也无法请求对象做任何事情。对象接口与其功能实现是分离的,不同对象可以对请求做不同的实现,也就是说,两个有相同接口的对象可以有完全不同的实现。

当给对象发送请求时,所引起的具体操作既与请求本身有关又与接受对象有关。支持相同请求的不同对象可能对请求激发的操作有不同的实现。发送给对象的请求和它的相应操作在运行时刻的连接就称之为动态绑定(dynamic binding)

动态绑定是指发送的请求直到运行时刻才受你的具体的实现的约束。因而,在知道任何有正确接口的对象都将接受此请求时,你可以写一个一般的程序,它期待着那些具有该特定接口的对象。进一步讲,动态绑定允许你在运行时刻彼此替换有相同接口的对象。这种可替换 性 就 称 为多态( p o l y m o r p h i s m ), 它 是 面 向 对 象 系 统 中 的 核 心 概 念 之 一 。 多 态 允 许 客 户 对 象 仅要求其他对象支持特定接口,除此之外对其假设几近于无。多态简化了客户的定义,使得对象间彼此独立,并可以在运行时刻动态改变它们相互的关系。

设计模式通过确定接口的主要组成成分及经接口发送的数据类型,来帮助你定义接口。设计模式也许还会告诉你接口中不应包括哪些东西。M e m e n t o ( 5 . 6 )模 式 是 一 个 很 好 的 例 子 ,它描述了怎样封装和保存对象内部的状态,以便一段时间后对象能恢复到这一状态。它规定了M e m e n t o对象必须定义两个接口:一个允许客户保持和复制m e m e n t o的 限 制 接 口 , 和 一 个 只有原对象才能使用的用来储存和提取m e m e n t o中 状 态 的 特 权 接 口 。

设计模式也指定了接口之间的关系。特别地,它们经常要求一些类具有相似的接口;或它们对一些类的接口做了限制。例如,D e c o r a t o r ( 4 . 4 )P r o x y ( 4 . 7 )模式要求D e c o r a t o rP r o x y对象的接口与被修饰的对象和受委托的对象一致。而V i s i t o r ( 5 . 11 )模式中,V i s i t o r接口必须反映出v i s i t o r能访问的对象的所有类。

1.6.4描述对象的实现

至此,我们很少提及到实际上怎么去定义一个对象。对象的实现是由它的类决定的,类指定了对象的内部数据和表示,也定义了对象所能完成的操作,如右图所示。

我们基于O M T的 表 示 法 , 将 类 描 述 成 一 个 矩 形 , 其 中 的 类 名 以 黑体表示的。操作在类名下面,以常规字体表示。类所定义的任何数据都在操作的下面。类名与操作之间以及操作与数据之间用横线分割。

返回类型和实例变量类型是可选的,因为我们并未假设一定要用具有静态类型的实现语言。

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

下图中的虚箭头线表示一个类实例化另一个类的对象,箭头指向被实例化的对象的类。新 的 类 可 以 由 已 存 在 的 类 通 过类继承( c l a s s i n h e r i t a n c e )来定义。当子类( s u b c l a s s )继承父类(parent class)时,子类包含了父类定义的所有数据和操作。子类的实例对象包含所有子类和父类定义的数据,且它们能完成子类和父类定义的所有操作。我们以竖线和三角表示子类关系,如下图所示。

抽象类(abstract class)的主要目的是为它的子类定义公共接口。一个抽象类将把它的部分或全部操作的实现延迟到子类中,因此,一个抽象类不能被实例化。在抽象类中定义却没有实现的操作被称为抽象操作(abstract operation)。非抽象类称为具体类(concrete class)

子 类 能 够 改 进 和 重 新 定 义 它 们 父 类 的 操 作 。 更 具 体 地 说 , 类 能 够重定义( o v e r r i d e )父类定义的操作,重定义使得子类能接管父类对请求的处理操作。类继承允许你只需简单的扩展其他类就可以定义新类,从而可以很容易地定义具有相近功能的对象族。

抽象类的类名以斜体表示,以与具体类相区别。抽象操作也用斜体表示。图中可以包括实现操作的伪代码,如果这样,则代码将出现在带有摺角的框中,并用虚线将该摺角框与代码所实现的操作相连,图示如下。

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

1.类继承与接口继承的比较
理解对象的类( c l a与s s对)象的类型( t y p之e )间的差别非常重要。一个对象的类定义了对象是怎样实现的,同时也定义了对象的内部状态和操作的实现。

类型,不同类的对象可以有相同的类型。当然,对象的类和类型是有紧密关系的。因为类定义了对象所能执行的操作,也定义了

对象的类型。当我们说一个对象是一个类的实例时,即指该对象支持类所定义的接口。
C + +E i ff e l语言的类既指定对象的类型又指定对象的实现。S m a l l t a l k程序不声明变量的类型,所以编译器不检查赋给变量的对象类型是否是该变量的类型的子类型。发送消息时需

要检查消息接收者是否实现了该消息,但不检查接收者是否是某个特定类的实例。理解类继承和接口继承(或子类型化)之间的差别也十分重要。类继承根据一个对象的实现定义了另一个对象的实现。简而言之,它是代码和表示的共享机制。然而,接口继承(或子类

型化)描述了一个对象什么时候能被用来替代另一个对象。因为许多语言并不显式地区分这两个概念,所以容易被混淆。在C + +E i ff e l语言中,继

承 既 指 接 口 的 继 承 又 指 实 现 的 继 承 。C + +中 接 口 继 承 的 标 准 方 法 是 公 有 继 承 一 个 含()虚成员函数的类。C + +中 纯 接 口 继 承 接 近 于 公 有 继 承 纯 抽 象 类 , 纯 实 现 继 承 或 纯 类 继 承 接 近 于 私 有继承。S m a l l t a l k中 的 继 承 只 指 实 现 继 承 。 只 要 任 何 类 的 实 例 支 持 对 变 量 值 的 操 作 , 你 就 可 以将这些实例赋给变量。

尽管大部分程序设计语言并不区分接口继承和实现继承的差别,但使用中人们还是分别对待它们的。S m a l l t a l k程序员通常将子类当作子类型(尽管有一些熟知的例外情况[ C o o 9 2 ] ),C + +程序员通过抽象类所定义的类型来操纵对象。

很多设计模式依赖于这种差别。例如,Chain of Responsibility(5.1)模式中的对象必须有一个公共的类型,但一般情况下它们不具有公共的实现。在C o m p o s i t e ( 4 . 3 )模 式 中 , 构 件 定 义 了一个公共的接口,但C o m p o s i t e通常定义一个公共的实现。C o m m a n d ( 5 . 2 )O b s e r v e r ( 5 . 7 )S t a t e ( 5 . 8 )S t r a t e g y ( 5 . 9 )通常纯粹作为接口的抽象类来实现。

2.对接口编程,而不是对实现编程

类继承是一个通过复用父类功能而扩展应用功能的基本机制。它允许你根据旧对象快速定义新对象。它允许你从已存在的类中继承所需要的绝大部分功能,从而几乎无需任何代价就可以获得新的实现。

然而,实现的复用只是成功的一半,继承所拥有的定义具有相同接口的对象族的能力也是很重要的(通常可以从抽象类来继承)。为什么?因为多态依赖于这种能力。

当继承被恰当使用时,所有从抽象类导出的类将共享该抽象类的接口。这意味着子类仅仅添加或重定义操作,而没有隐藏父类的操作。这时,所有的子类都能响应抽象类接口中的请求,从而子类的类型都是抽象类的子类型。

只根据抽象类中定义的接口来操纵对象有以下两个好处:
1)客户无须知道他们使用对象的特定类型,只须对象有客户所期望的接口。
2)客户无须知道他们使用的对象是用什么类来实现的,他们只须知道定义接口的抽象类。这将极大地减少子系统实现之间的相互依赖关系,也产生了可复用的面向对象设计的如

下原则:

针对接口编程,而不是针对实现编程。

不将变量声明为某个特定的具体类的实例对象,而是让它遵从抽象类所定义的接口。这是本书设计模式的一个常见主题。

当你不得不在系统的某个地方实例化具体的类(即指定一个特定的实现)时,创建型模式

( A b s t r a c t F a c t o r y ( 3 . 1 ),B u i l d e r ( 3 . 2 ),F a c t o r y M e t h o d ( 3 . 3 ),P r o t o t y p e ( 3 . 4 )S i n g l e t o n ( 3 . 5 ) )可以帮你。通过抽象对象的创建过程,这些模式提供不同方式以在实例化时建立接口和实现的透明连接。创建型模式确保你的系统是采用针对接口的方式书写的,而不是针对实现而书写的。

1.6.5运用复用机制

理解对象、接口、类和继承之类的概念对大多数人来说并不难,问题的关键在于如何运用它们写出灵活的、可复用的软件。设计模式将告诉你怎样去做。

1.继承和组合的比较

面向对象系统中功能复用的两种最常用技术是类继承和对象组合(object composition)。正如我们已解释过的,类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见。

对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。

继承和组合各有优缺点。类继承是在编译时刻静态定义的,且可直接使用,因为程序设计语言直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部操作时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。

但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。更糟的是,父类通常至少定义了部分子类的具体表示。因为继承对子类揭示了其父类的实现细节,所以继承常被认为“破坏了封装性”[ S n y 8 6 ]。子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。

当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。

对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。

对象组合对系统设计还有另一个作用,即优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另一方面,基于对象组合的设计会有更多的对象(而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。

这导出了我们的面向对象设计的第二个原则:

优先使用对象组合,而不是类继承。

理想情况下,你不应为获得复用而去创建新的构件。你应该能够只使用对象组合技术,

通过组装已有的构件就能获得你需要的功能。但是事实很少如此,因为可用构件的集合实际上并不足够丰富。使用继承的复用使得创建新的构件要比组装旧的构件来得容易。这样,继承和对象组合常一起使用。

然而,我们的经验表明:设计者往往过度使用了继承这种复用技术。但依赖于对象组合技术的设计却有更好的复用性(或更简单)。你将会看到设计模式中一再使用对象组合技术。

2.委托

委托(d e l e g a t i o n)是一种组合方法,它使组合具有与继承同样的复用能力[ L i e 8 6 , J Z 9 1 ]。在委托方式下,有两个对象参与处理一个请求,接受请求的对象将操作委托给它的代理者
(d e l e g a t e)。这类似于子类将请求交给它的父类处理。使用继承时,被继承的操作总能引用接受请求的对象,C + +中通过t h i s成员变量,S m a l l t a l k中则通过s e l f。 委 托 方 式 为 了 得 到 同 样 的效果,接受请求的对象将自己传给被委托者(代理人),使被委托的操作可以引用接受请求的对象。

举例来说,我们可以在窗口类中保存一个矩形类的实例变量来代理矩形类的特定操作,这样窗口类可以复用矩形类的操作,而不必像继承时那样定义成矩形类的子类。也就是说,一个窗口拥有一个矩形,而不是一个窗口就是一个矩形。窗口现在必须显式的将请求转发给它的矩形实例,而不是像以前它必须继承矩形的操作。

下面的图显示了窗口类将它的A r e a操 作 委 托 给 一 个 矩 形 实 例 。

箭 头 线 表 示 一 个 类 对 另 一 个 类 实 例 的 引 用 关 系 。 引 用 名 是 可 选 的 , 本 例 为 “r e c t a n g l e”。

委托的主要优点在于它便于运行时刻组合对象操作以及改变这些操作的组合方式。假定矩形对象和圆对象有相同的类型,我们只需简单的用圆对象替换矩形对象,则得到的窗口就是圆形的。

委托与那些通过对象组合以取得软件灵活性的技术一样,具有如下不足之处:动态的、高度参数化的软件比静态软件更难于理解。还有运行低效问题,不过从长远来看人的低效才是更主要的。只有当委托使设计比较简单而不是更复杂时,它才是好的选择。要给出一个能确切告诉你什么时候可以使用委托的规则是很困难的。因为委托可以得到的效率是与上下文有关的,并且还依赖于你的经验。委托最适用于符合特定程式的情形,即标准模式的情形。

有一些模式使用了委托,如S t a t e ( 5 . 8 )S t r a t e g y ( 5 . 9 )Vi s i t o r ( 5 . 11 )。在S t a t e模式中,一个对象将请求委托给一个描述当前状态的S t a t e对象来处理。在S t r a t e g y模 式 中 , 一 个 对 象 将 一个特定的请求委托给一个描述请求执行策略的对象,一个对象只会有一个状态,但它对不同的请求可以有许多策略。这两个模式的目的都是通过改变受托对象来改变委托对象的行为。在V i s i t o r中 , 对 象 结 构 的 每 个 元 素 上 的 操 作 总 是 被 委 托 到Vi s i t o r对象。


其 他 模 式 则 没 有 这 么 多 地用到委托。M e d i a t o r ( 5 . 5 )引 进 了 一 个 中 介 其 他 对 象 间 通 信 的 对


象。有时,M e d i a t o r对 象 只 是 简 单 地 将 请 求 转 发 给 其 他 对 象 ; 有 时 , 它 沿 着 指 向 自 己 的 引 用来传递请求,使用真正意义的委托。Chain of Responsibility(5.1)通过将请求沿着对象链传递来处理请求,有时,这个请求本身带有一个接受请求对象的引用,这时该模式就使用了委托。B r i d g e ( 4 . 2 )将实现和抽象分离开,如果抽象和一个特定实现非常匹配,那么这个实现可以代理抽象的操作。

委托是对象组合的特例。它告诉你对象组合作为一个代码复用机制可以替代继承。

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

另一种功能复用技术(并非严格的面向对象技术)是参数化类型(parameterized type),也就是类属( g e n e r i c ) ( A d aE i ff e l )或模板( t e m p l a t e s ) ( C + + )。 它 允 许 你 在 定 义 一 个 类 型 时 并 不 指 定 该类型所用到的其他所有类型。未经指定的类型在使用时以参数形式提供。例如,一个列表类能够以它所包含元素的类型来进行参数化。如果你想声明一个I n t e g e r列表,只需将I n t e g e r类型作为列表参数化类型的参数值;声明一个S t r i n g列表,只需提供S t r i n g类 型 作 为 参 数 值 。 语言的实现将会为各种元素类型创建相应的列表类模板的定制版本。

参数化类型给我们提供除了类继承和对象组合外的第三种方法来组合面向对象系统中的行为。许多设计可以使用这三种技术中的任何一种来实现。实现一个以元素比较操作为可变元的排序例程,可有如下方法:

1)通过子类实现该操作( Te m p l a t e M e t h o d ( 5 . 1 0 )的一个应用)
2 )实现为传给排序例程的对象的职责( S t r a t e g y ( 5 . 9 ) )
3)作为C + +模板或A d a类 属 的 参 数 , 以 指 定 元 素 比 较 操 作 的 名 称 。这些技术存在着极大的不同之处。对象组合技术允许你在运行时刻改变被组合的行为,

但是它存在间接性,比较低效。继承允许你提供操作的缺省实现,并通过子类重定义这些操作。参数化类型允许你改变类所用到的类型。但是继承和参数化类型都不能在运行时刻改变。哪一种方法最佳,取决于你设计和实现的约束条件。

本 书 没 有 一 种 模 式 是 与 参 数 化 类 型 有 关 的 , 尽 管 我 们 在 定 制 一 个 模 式 的C + +实 现 时 用 到 了参数化类型。参数化类型在像S m a l l t a l k那 样 的 编 译 时 刻 不 进 行 类 型 检 查 的 语 言 中 是 完 全 不 必要的。

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

一个面向对象程序运行时刻的结构通常与它的代码结构相差较大。代码结构在编译时刻就被确定下来了,它由继承关系固定的类组成。而程序的运行时刻结构是由快速变化的通信对象网络组成。事实上两个结构是彼此独立的,试图由一个去理解另一个就好像试图从静态的动、植物分类去理解活生生的生态系统的动态性。反之亦然。

考虑对象聚合( a g g r e g a t i o n )和相识( a c q u a i n t a n c e )的 差 别 以 及 它 们 在 编 译 和 运 行 时 刻 的 表 示是多么的不同。聚合意味着一个对象拥有另一个对象或对另一个对象负责。一般我们称一个对象包含另一个对象或者是另一个对象的一部分。聚合意味着聚合对象和其所有者具有相同的生命周期。

相识意味着一个对象仅仅知道另一个对象。有时相识也被称为“关联”或“引用”关系。相识的对象可能请求彼此的操作,但是它们不为对方负责。相识是一种比聚合要弱的关系,它只标识了对象间较松散的耦合关系。

聚合和相识很容易混淆,因为它们通常以相同的方法实现。S m a l l t a l k中 , 所 有 变 量 都 是其他对象的引用,程序设计语言中两者并无区别。C + +中 , 聚 合 可 以 通 过 定 义 表 示 真 正 实 例的成员变量来实现,但更通常的是将这些成员变量定义为实例指针或引用;相识也是以指针或引用来实现。

从根本上讲,是聚合还是相识是由你的意图而不是由显式的语言机制决定的。尽管它们之间的区别在编译时刻的结构中很难看出来,但这些区别还是很大的。聚合关系使用较少且比相识关系更持久;而相识关系则出现频率较高,但有时只存在于一个操作期间,相识也更具动态性,使得它在源代码中更难被辨别出来。

程序的运行时刻结构和编译时刻结构存在这么大的差别,很明显代码不可能揭示关于系统如何工作的全部。系统的运行时刻结构更多地受到设计者的影响,而不是编程语言。对象和它们的类型之间的关系必须更加仔细地设计,因为它们决定了运行时刻程序结构的好坏。

许多设计模式(特别是那些属于对象范围的)显式地记述了编译时刻和运行时刻结构的差别。C o m p o s i t e ( 4 . 3 )D e c o r a t o r ( 4 . 4 )对 于 构 造 复 杂 的 运 行 时 刻 结 构 特 别 有 用 。O b s e r v e r ( 5 . 7 )也与运行时刻结构有关,但这些结构对于不了解该模式的人来说是很难理解的。Chain ofR e s p o n s i b i l i t y ( 5 . 1 )也产生了继承所无法展现的通信模式。总之,只有理解了模式,你才能清楚代码中的运行时刻结构。

1.6.7设计应支持变化

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

为了设计适应这种变化、且具有健壮性的系统,你必须考虑系统在它的生命周期内会发生怎样的变化。一个不考虑系统变化的设计在将来就有可能需要重新设计。这些变化可能是类的重新定义和实现,修改客户和重新测试。重新设计会影响软件系统的许多方面,并且未曾料到的变化总是代价巨大的。

设计模式可以确保系统能以特定方式变化,从而帮助你避免重新设计系统。每一个设计模式允许系统结构的某个方面的变化独立于其他方面,这样产生的系统对于某一种特殊变化将更健壮。

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

1)通过显式地指定一个类来创建对象在创建对象时指定类名将使你受特定实现的约束而不是特定接口的约束。这会使未来的变化更复杂。要避免这种情况,应该间接地创建对象。

设计模式:A b s t r a c t F a c t o r y ( 3 . 1 ),F a c t o r y M e t h o d ( 3 . 3 ),P r o t o t y p e ( 3 . 4 )

2)对特殊操作的依赖当你为请求指定一个特殊的操作时,完成该请求的方式就固定下来了。为避免把请求代码写死,你将可以在编译时刻或运行时刻很方便地改变响应请求的方法。

设计模式:C h a i n o f R e s p o s i b i l i t y ( 5 . 1 ),C o m m a n d ( 5 . 2 )
3)对 硬 件 和 软 件 平 台 的 依 赖外 部 的 操 作 系 统 接 口 和 应 用 编 程 接 口( A P I )在 不 同 的 软 硬 件


平台上是不同的。依赖于特定平台的软件将很难移植到其他平台上,甚至都很难跟上本地平台的更新。所以设计系统时限制其平台相关性就很重要了。

设计模式:A b s t r a c t F a c t o r y ( 3 . 1 ),B r i d g e ( 4 . 2 )

4)对对象表示或实现的依赖知道对象怎样表示、保存、定位或实现的客户在对象发生变化时可能也需要变化。对客户隐藏这些信息能阻止连锁变化。

设计模式:A b s t r a c t F a c t o r y ( 3 . 1 ),B r i d g e ( 4 . 2 ),M e m e n t o ( 5 . 6 ),P r o x y ( 4 . 7 )

5)算法依赖算法在开发和复用时常常被扩展、优化和替代。依赖于某个特定算法的对象在算法发生变化时不得不变化。因此有可能发生变化的算法应该被孤立起来。

设计模式:B u i l d e r ( 3 . 2 ),I t e r a t o r ( 5 . 4 ),S t r a t e g y ( 5 . 9 ),T e m p l a t e M e t h o d ( 5 . 1 0 ),V i s i t o r ( 5 . 11 )

6)紧耦合紧耦合的类很难独立地被复用,因为它们是互相依赖的。紧耦合产生单块的系统,要改变或删掉一个类,你必须理解和改变其他许多类。这样的系统是一个很难学习、移植和维护的密集体。

松散耦合提高了一个类本身被复用的可能性,并且系统更易于学习、移植、修改和扩展。设计模式使用抽象耦合和分层技术来提高系统的松散耦合性。

设计模式:A b s t r a c t F a c t o r y ( 3 . 1 ),C o m m a n d ( 5 . 2 ),F a c a d e ( 4 . 5 ),M e d i a t o r ( 5 . 5 ),Observer(5.7),Chain of Responsibility(5.1)

7)通过生成子类来扩充功能通常很难通过定义子类来定制对象。每一个新类都有固定的实现开销(初始化、终止处理等)。定义子类还需要对父类有深入的了解。如,重定义一个操作可能需要重定义其他操作。一个被重定义的操作可能需要调用继承下来的操作。并且子类方法会导致类爆炸,因为即使对于一个简单的扩充,你也不得不引入许多新的子类。

一般的对象组合技术和具体的委托技术,是继承之外组合对象行为的另一种灵活方法。新的功能可以通过以新的方式组合已有对象,而不是通过定义已存在类的子类的方式加到应用中去。另一方面,过多使用对象组合会使设计难于理解。许多设计模式产生的设计中,你可以定义一个子类,且将它的实例和已存在实例进行组合来引入定制的功能。

设计模式:B r i d g e ( 4 . 2 ),C h a i n o f R e s p o n s i b i l i t y ( 5 . 1 ),C o m p o s i t e ( 4 . 3 ),D e c o r a t o r ( 4 . 4 ),O b s e r v e r ( 5 . 7 ),S t r a t e g y ( 5 . 9 )

8)不能方便地对类进行修改有时你不得不改变一个难以修改的类。也许你需要源代码而又没有(对于商业类库就有这种情况),或者可能对类的任何改变会要求修改许多已存在的其他子类。设计模式提供在这些情况下对类进行修改的方法。

设计模式:A d a p t e r ( 4 . 1 ),D e c o r a t o r ( 4 . 4 ),Vi s i t o r ( 5 . 11 )

这些例子反映了使用设计模式有助于增强软件的灵活性。这种灵活性所具有的重要程度取决于你将要建造的软件系统。让我们看一看设计模式在开发如下三类主要软件中所起的作用:应用程序、工具箱和框架。

1.应用程序

如果你将要建造像文档编辑器或电子制表软件这样的应用程序(Application Program),那么它的内部复用性、可维护性和可扩充性是要优先考虑的。内部复用性确保你不会做多余的设计和实现。设计模式通过减少依赖性来提高内部复用性。松散耦合也增强了一类对象与其他多个对象协作的可能性。例如,通过孤立和封装每一个操作,以消除对特定操作的依赖,


可使在不同上下文中复用一个操作变得更简单。消除对算法和表示的依赖可达到同样的效果。当设计模式被用来对系统分层和限制对平台的依赖性时,它们还会使一个应用更具可维护性。通过显示怎样扩展类层次结构和怎样使用对象复用,它们可增强系统的易扩充性。同时,耦合程度的降低也会增强可扩充性。如果一个类不过多地依赖其他类,扩充这个孤立的

类还是很容易的。

2.工具箱

一个应用经常会使用来自一个或多个被称为工具箱(Toolkit)的预定义类库中的类。工具箱是一组相关的、可复用的类的集合,这些类提供了通用的功能。工具箱的一个典型例子就是列表、关联表单、堆栈等类的集合,C + +I / O流 库 是 另 一 个 例 子 。 工 具 箱 并 不 强 制 应 用 采 用某个特定的设计,它们只是为你的应用提供功能上的帮助。工具箱强调的是代码复用,它们是面向对象环境下的“子程序库”。

工具箱的设计比应用设计要难得多,因为它要求对许多应用是可用的和有效的。再者,工具箱的设计者并不知道什么应用使用该工具箱及它们有什么特殊需求。这样,避免假设和依赖就变得很重要,否则会限制工具箱的灵活性,进而影响它的适用性和效率。

3.框架

框架( F r a m e w o r k )是 构 成 一 类 特 定 软 件 可 复 用 设 计 的 一 组 相 互 协 作 的 类[ D e u 8 9 , J F 8 8 ]。例如,一个框架能帮助建立适合不同领域的图形编辑器,像艺术绘画、音乐作曲和机械
C A D [ V L 9 0 , J o h 9 2 ]。另一个框架也许能帮助你建立针对不同程序设计语言和目标机器的编译器[ J M L 9 2 ]。而再一个也许能帮助你建立财务建模应用[ B E 9 3 ]。 你 可 以 定 义 框 架 抽 象 类 的 应 用相关的子类,从而将一个框架定制为特定应用。

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

这个层次的复用导致了应用和它所基于的软件之间的反向控制(inversion of control)。当你使用工具箱(或传统的子程序库)时,你需要写应用软件的主体并且调用你想复用的代码。而当你使用框架时,你应该复用应用的主体,写主体调用的代码。你不得不以特定的名字和调用约定来写操作地实现,但这会减少你需要做出的设计决策。

你不仅可以更快地建立应用,而且应用还具有相似的结构。它们很容易维护,且用户看来也更一致。另一方面,你也失去了一些表现创造性的自由,因为许多设计决策无须你来作出。

如果说应用程序难以设计,那么工具箱就更难了,而框架则是最难的。框架设计者必须冒险决定一个要适应于该领域的所有应用的体系结构。任何对框架设计的实质性修改都会大大降低框架所带来的好处,因为框架对应用的最主要贡献在于它所定义的体系结构。因此设计的框架必须尽可能地灵活、可扩充。

更进一步讲,因为应用的设计如此依赖于框架,所以应用对框架接口的变化是极其敏感的。当框架演化时,应用不得不随之演化。这使得松散耦合更加重要,否则框架的一个细微变化都将引起强烈反应。

刚才讨论的主要设计问题对框架设计而言最具重要性。一个使用设计模式的框架比不用


设计模式的框架更可能获得高层次的设计复用和代码复用。成熟的框架通常使用了多种设计模式。设计模式有助于获得无须重新设计就可适用于多种应用的框架体系结构。

当框架和它所使用的设计模式一起写入文档时,我们可以得到另外一个好处[ B J 9 4 ]。了解设计模式的人能较快地洞悉框架。甚至不了解设计模式的人也可以从产生框架文档的结构中受益。加强文档工作对于所有软件而言都是重要的,但对于框架其重要性显得尤为突出。学会使用框架常常是一个必须克服很多困难的过程。设计模式虽然无法彻底克服这些困难,但它通过对框架设计的主要元素做更显式的说明可以降低框架学习的难度。

因为模式和框架有些类似,人们常常对它们有怎样的区别和它们是否有区别感到疑惑。它们最主要的不同在于如下三个方面:

1)设计模式比框架更抽象框架能够用代码表示,而设计模式只有其实例才能表示为代码。框架的威力在于它们能够使用程序设计语言写出来,它们不仅能被学习,也能被直接执行和复用。而本书中的设计模式在每一次被复用时,都需要被实现。设计模式还解释了它的意图、权衡和设计效果。

2)设计模式是比框架更小的体系结构元素一个典型的框架包括了多个设计模式,而反之决非如此。

3)框架比设计模式更加特例化框架总是针对一个特定的应用领域。一个图形编辑器框架可能被用于一个工厂模拟,但它不会被错认为是一个模拟框架。而本书收录的设计模式几乎能被用于任何应用。当然比我们的模式更特殊的设计模式也是可能的(如,分布式系统和并发程序的设计模式),尽管这些模式不会像框架那样描述应用的体系结构。

框架变得越来越普遍和重要。它们是面向对象系统获得最大复用的方式。较大的面向对象应用将会由多层彼此合作的框架组成。应用的大部分设计和代码将来自于它所使用的框架或受其影响。

1.7怎样选择设计模式

本书中有2 0多 个 设 计 模 式 供 你 选 择 , 要 从 中 找 出 一 个 针 对 特 定 设 计 问 题 的 模 式 可 能 还 是很困难的,尤其是当面对一组新模式,你还不怎么熟悉它的时候。这里给出几个不同的方法,帮助你发现适合你手头问题的设计模式:

  • 考虑设计模式是怎样解决设计问题的1 . 6节 讨 论 了 设 计 模 式 怎 样 帮 助 你 找 到 合 适 的 对象、决定对象的粒度、指定对象接口以及设计模式解决设计问题的几个其他方法。参考这些讨论会有助于你找到合适的模式。

  • 浏览模式的意图部分1 . 4节列出了目录中所有模式的意图( i n t e n t )部 分 。 通 读 每 个 模 式 的意图,找出和你的问题相关的一个或多个模式。你可以使用表1 - 1所 显 示 的 分 类 方 法 缩小你的搜查范围。

  • 研究模式怎样互相关联图1-1以图形方式显示了设计模式之间的关系。研究这些关系能指导你获得合适的模式或模式组。

  • 研究目的相似的模式模式分类描述部分共有三章,一章介绍创建型模式,一章介绍结构型模式,一章介绍行为型模式。每一章都以对模式介绍性的评价开始,以一个小节的比较和对照结束。这些小节使你得以洞察具有相似目的的模式之间的共同点和不同点。

  • 检查重新设计的原因看一看从“设计应支持变化”小节开始讨论的引起重新设计的各


种原因,再看看你的问题是否与它们有关,然后再找出哪些模式可以帮助你避免这些会

导致重新设计的因素。
考虑你的设计中哪些是可变的这个方法与关注引起重新设计的原因刚好相反。它不是

考虑什么会迫使你的设计改变,而是考虑你想要什么变化却又不会引起重新设计。最主要 的 一 点 是封 装 变 化 的 概 念,这是许多设计模式的主题。表1 - 2列 出 了 设 计 模 式 允 许 你独立变化的方面,你可以改变它们而又不会导致重新设计。


目 的创建

结构

行为

1-2设计模式所支持的设计的可变方面可变的方面

对产品对象家族对如何创建一个组合对象对被实例化的子类对被实例化的类对一个类的唯一实例

对对象的接口
对对象的实现对一个对象的结构和组成对对象的职责,不生成子类对一个子系统的接口对对象的存储开销对如何访问一个对象;该对象的位置

对满足一个请求的对象
对何时、怎样满足一个请求
对一个语言的文法及解释对如何遍历、访问一个聚合的各元素对对象间怎样交互、和谁交互对一个对象中哪些私有信息存放在该对象之外,以及在对什么时候进行存储对多个对象依赖于另外一个对象,而这些对象又如何保持一致

对对象的状态
对算法
对算法中的某些步骤对某些可作用于一个(组)对象上的操作,但不修改这些对象的类

1.8怎样使用设计模式

一旦你选择了一个设计模式,你怎么使用它呢?这里给出一个有效应用设计模式的循序渐进的方法。

1)大致浏览一遍模式特别注意其适用性部分和效果部分,确定它适合你的问题。

2 )回 头 研 究 结 构 部 分 、 参 与 者 部 分 和 协 作 部 分确保你理解这个模式的类和对象以及它们是怎样关联的。

3 )看 代 码 示 例 部 分 , 看 看 这 个 模 式 代 码 形 式 的 具 体 例 子研究代码将有助于你实现模式。4 )选 择 模 式 参 与 者 的 名 字 , 使 它 们 在 应 用 上 下 文 中 有 意 义设计模式参与者的名字通常

过于抽象而不会直接出现在应用中。然而,将参与者的名字和应用中出现的名字合并起来是很有用的。这会帮助你在实现中更显式的体现出模式来。例如,如果你在文本组合算法中使用了S t r a t e g y模式,那么你可能有名为S i m p l e L a y o u t S t r a t e g yT e X L a y o u t S t r a t e g y这样的类。

5)定义类声明它们的接口,建立它们的继承关系,定义代表数据和对象引用的实例变量。识别模式会影响到的你的应用中存在的类,做出相应的修改。

6)定义模式中专用于应用的操作名称这里再一次体现出,名字一般依赖于应用。使用与每一个操作相关联的责任和协作作为指导。还有,你的名字约定要一致。例如,可以使用

C r e a t e-”前缀统一标记F a c t o r y方法。
7)实现执行模式中责任和协作的操作实现部分提供线索指导你进行实现。代码示例部

分的例子也能提供帮助。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值