继续 上 节讲述过的Singleton 、 Proxy 及 Iterator各模式,本节再来考察几个别的设计模式。下面按顺序来考察 Prototype 、 Template Method 和 Observer这三个设计模式。
4.2.2 重复使用既存对象的Prototype (原型)模式
引用 《 设计模式 》一书中 的 解释 ,Prototype 模式 “ 明确一个实例作为要生成对象的种类原型,通过 复制 该实例来生成新的对象 ” 。
《 设计模式 》 中这一模式的例题使用的是迷宫生成MazeFactory 类。这一例子通过拥有生成墙、房间、门等对象的原型,不需要派生就可以生成各种各样的迷宫。但是,实话说来,这个例题并没有让人真正感觉到原型的宝贵之处。
实际上 , Prototype模式本来并不太适用于 C++ 这样的静态语言。在动态语言中, Prototype 模式才能够真正发挥它的巨大威力。
通常在面向对象的语言中,首先准备好所谓的类,也就是对象的雏形,然后从类来生成实际的对象,这些做法都是理所当然的。在面向对象编程中,类是最为基本的存在。
但是,类真的是必需的吗?Smalltalk 的 “ 亚种 ” Self语言的设计人员就认为类并不是必 需 的。Self 中不存在所谓的类,基本操作并不是从类来生成对象,而是 复制 对 象 。
基本思想就是如此。
在需要新种类对象的时候,首先 复制 一个既存的对象,给 复制 的对象直接增加方法或实例变量等功能,生成最初的第一个新种类对象。如果该对象需要不止一个的话,那就从第一个原型来 复制 ,需要几个就复制几个。最初一个虽然是雏形,但它并不是所谓类这种人为生成的特别对象,它也只是一个普通的对象, 只 不过偶尔被用为复制的原型而已。
雏形这个词的英语是Prototype 。像这样的不用类,而是 用 原型模式和复制的编程语言称为原型模式的编程语言。实际上,Prototype 模式不单是一种设计模式,也许称之为一种编程范例才更为合适。
相对于类模式的编程,原型模式的编程的构成元素比较少,具有简单实现面向对象功能设计的倾向。因此,最近有越来越多的规格较小的编程语言采用这种模式。比如,大多数Web 浏览器中嵌入的 JavaScript 的面向对象功能就是原型模式的 。 最近,受到一部分 人 关注的Io 语言 也是原型模式的。
4.2.3 亲身体验Io 语言
只讲理论还是难 以 得到具体的印象,那就让我们来边看实际程序,边来亲身体验原型模式的编程吧。虽然用Ruby 也可以进行原型模式的编程, 但 这次我们使用更为彻底的Io 语言( 参见 图 4-17 )。
图 4-17 使用Io 语言的原型模式的编程 示 例
让我们来仔细看看图 4-17 吧。这是描述狗的简单例子。虽然没有实用性,但从中应该能体会到原型模式的气氛。
(1)的部分生成新的对象,赋值给名为 Dog 的变量。请注意,这里出现的 Object 和 Dog 都不是类。在Io 语言中, Object 只是 有 代表性的对象,除最基本的以外 其他 什么都不知道。以它为基础可以生成各种雏形。这里调用 clone 方法来复制一个基础对象,并给它起个名字,叫做 Dog 。刚刚复制出来的 Dog 对象跟 Object 是一样的,没有狗的任何功能。
只是 Object 的话,什么功能都没有,是没有任何使用价值的,让我们来教给它狗的功能吧。(2) 部分中 先是给它定一个 “ 坐下 ” 的 sit 方法。把一个 method 对象赋值给 Dog 对象的 sit 属性,就给 Dog 对象追加了一个方法。
调用 sit 方法就等于调用 " I ' m sitting/n " print ,显示 I ' m sitting. 。这一部分相当于调用字符串对象的 print 方法,从中可以体会到彻底的面向对象编程。这个例子中仅仅增加了一个 sit 方法,实际上可以根据需要想追加几个方法就可以追加几个。
你看, Dog 对象就是这样 实现 的。再强调一遍,其中作为雏形的是狗对象,而不是类。因此, ( 3 )部分中 对 Dog 对象调用 sit 方法的时候,结果与其他狗一样显示出 I’m sitting. 。
在像Ruby 这样类模式的语言中,作为对象雏形的类拥有与对象完全不同的方法(类的方法),而与之相对的是,原型模式的语言中根本就没有类的存在,雏形与基于它生成的对象是完全一样的。
(4) 部分 使用 clone 方法基于雏形生成新的狗对象。这里请注意,不管是生成新的雏形,还是从雏形生成新的对象,都是仅仅使用 clone 方法实现的。在类模式的语言中,用子类化和实例化这两个不同的概念实现的程序,这里仅仅用 clone 这样一个简单的程序就实现了, 真 让人感动。
当然,简单并不都是一切,原型模式有原型模式的优点,类模式也有类模式的优点,因为原型模式的语言还不太广为人知,这里特意选它作为例子, 就是为了 让大家体会一下它的单纯。
4.2.4 Ruby 中的原型
基本上讲Ruby 是类模式的语言,但也拥有支持原型模式编程的功能 , 具体来说有以下3 种功能。
(1 )复制对象的 clone 方法 。
(2 )给个别对象增加方法的特异方法功能 。
(3 )给个别对象增加一组功能的 extend 方法 。
图 4-18 用Ruby重写图4-17的程序
因为Ruby 中没有像 Io 语言中 Object 这样一个对象原型,所以作为准备,程序一开始( object = Object. new )先是生成一个 Object 类的实例。在这以后,除了语法上细微的区别之外,差不多是把Io 程序照搬过来的。
从中我们可以看到动态语言中Prototype 模式这种令人吃惊的力量。这在必须明确对象类型的静态语言中是实现不了的。因为在静态语言中没有原型编程,是不可能给复制的对象增加新方法的。
4.2.5 编写抽象算法的Template Method (模板方法)模式
接着来看Template Method 吧。还是引用 《 设计模式 》 中的 解释 ,Template Method 模式是 “ 在父类的一个方法中定义算法的框架,其中几个步骤的具体内容则留给子类来实现。使用Template Method 模式,可以在不改变算法构造的前提下,在子类中定义算法的一些步骤 ” 。
这实际上是面向对象编程中使用继承的一般技巧。
作为例子,让我们来看一下Ruby 的 p 方法吧。 p 方法是程序调试中用来显示对象内容的方法。显示调试信息算法主要是:
(1 )把对象的调试用输出信息转换成字符串 。
(2 )调用 puts 方法输出 。
这就是调试输出的算法,因为实在是太简单了,称之为算法 简直 有些过分。
但实际上令人意外的是,为了输出调试信息,分别为各种对象定义调试用输出字符串是非常困难的。针对各种不同的类都要分别进行不同处理,这就很困难了,每次增加新类的时候,为支持新类也都需要做大量的工作。
这时候使用Template Method 模式,问题就变得简单了。使用 Template Method 模式,输出调试信息的 p 方法会变成如下的代码。简单得令人意外。
def p(obj)
puts obj.inspect
end
这个简单的方法只是把算法的 ( 1 ) 和(2 )原封不动地换成了程序语言。在这个定义中,各种对象中准备调试信息的具体处理是由该对象的 inspect 方法来实现的。在定义新类的时候,只要给它定义了合适的 inspect 方法,就可以在任何时候使用 p 方法来输出适当的调试信息。
在父类中定义抽象化的算法,调用隐 藏 了实现细节的方法,然后在子类中实现具体的细节,这就是Template Method 模式。
4.2.6 用Ruby 来尝试 Template Method
Ruby的类库中最大限度灵活运用 Template Method 模式的部分,应该说是 Enumerable 模块和 Comparable 模块了。
Enumerable模块中实现循环的 each 方法采用了Template Method 模式。表 4-2 是Enumerable 模块的方法一览。
表 4-2 Enumerable提供的方法
(续)
这些方法的定义都是仅仅依赖于 each 方法。因此,在用户定义的类中,只要定义了 each 方法,一旦把Enumerable 模块 include 进来,就都可以使用表中的32 个方法了。
Enumerable模块实际上是用 C 编写的,用 Rudy 也可以简单地定义同样的模块,那就让我们边看 Ruby 的定义,边来考虑一下如何更好地使用 Template Method 模式 。
其中一个实现起来最为简单的方法,应该是收集所有元素的 entries 方法了。 entries 方法的实现代码如图 4-19 所示。看一下图 4-19 就能明白,处理内容是很简单的。
图4-19 用 Ruby 实现的 entries 方法
(1 )生成数组。
(2 )用 each 取出每个元素。
(3 )把元素追加到数组里。
(4 )最后返回数组。
从中可以看出,这个方法只是定义了自己处理的 框架 ,对应于每个对象的处理则由 each 方法来提供。
像Template Method 模式的这种使用方法, Comparable 模块中也是一样。
Comparable模块利用基本的比较大小方法 <=> ,提供各种比较演算。 <=> 方法把自身与参数相比较,如果自身较大,则返回正整数 ; 若二者相等,则返回0 ; 若自身较小,则返回负整数。以这个方法为基础,Comparable 模块提供了 == 、 > 、 >= 、 < 、 <= 以及 between?共 6 种比较运算。
作为Comparable 提供的比较运算的代表,我们来看一看 > 方法的实现吧( 参见 图4 -20 )。实际上> 方法还要加上错误处理,但基本处理如图 4 -20 所示。
图4-20 用 Ruby 实现的 > 方法
像这样,使用Template 模式,不涉及各种数据结构细节,而只在抽象的水平上编写算法的程序。也就是说,算法是在抽象水平很高的状态下表述的,同样的代码能够适用于各种各样的情况。
像这样避免了代码的重复,从DRY 原则的观点来看 ,也是很优秀的。
4.2.7 动态语言与 Template Method( 模板方法 ) 模式
一般Template Method 模式与继承往往是成对讨论的,但像 Enumberable 那样,只需要 include 进来,不管继承关系如何,即可以向任何类里追加功能,这一点很有魅力。本来,Ruby 的 include 就是一种受限的多重继承, 这本 没有什么不可思议的。
Template Method模式的这种优秀性质与语言是不是静态没有关系。像 Java 那种含有静态类型,而且不允许多重继承的语言,必须强制性地拥有继承关系。所以,像 Enumerable 这样在各种各样的类中都能利用的算法集,使用 Template Method 模式很难实现( interface 与委托的组合也不是不可能),但这不是静态语言的问题。
但是,像Io 与 Ruby 这种也善于原型模式编程的语言,更加进化一步,可以往特定对象里追加算法集。图 4-21 表示往特定对象里追加Enumerable 功能,虽然这个例子有点 牵强 。
图4-21 往特定对象里追加 Enumerable 功能的示例
尽管哪儿也没定义类,但使用 extend ,骰子对象中就能够利用Enumerable 模块的功能了。用 extend 及特异方法往特定对象里追加功能的做法,也能够用来实现 Singleton 模式。
4.2.8 避免 高度 依赖 性的观察者(Observer ) 模式
观察者模式是,当某个对象的状态发生变化时,依存于该状态的全部对象都自动得到通知,而且为了让它们都得到更新,定义了对象间一对多的依存关系。
这是控制类与类之间依存关系的一种模式。举一个例子,想想微软的Excel 软件吧。以表中的数据为基础表示图形的时候,编辑了表中的数据之后,自然希望图形的内容也跟着变化。或者,从同一组数据,想同时看到直方图和扇形图等多种图形的情况也是有的。
能够实现这一要求的最简单的方法,应该是在表编辑功能里附加更新图形 显示 的处理。但是这样做的话,附加的是与表编辑在本质上不同的处理,使事情复杂化,更重要的是,当想要再利用表编辑功能时,还要牵连到不一定必要的图形 显示 功能。表编辑功能与图形 显示 功能之间的这种关系称为高度 依赖 性。
一般,高度依 赖 性不好。 从 本质上 讲 ,软件是个复杂的东西,为了控制复杂性,有效 的 方法是将整体分割成几个相互独立的部分进行开发。但是,有了高度依 赖 性,就不能将组成程序的 “ 零件 ” (类以及子程序)进行分解,一个一个的 “ 零件 ” 会很大,结果就很难控制复杂性。
观察者模式是一种 避免 这种高度依 赖 性的手段。构成观察者模式的有两个对象,一个称为Observer (观察者),接受变更通知;另一个称为 Subject (对象)或 Observable (被观察者),发出变更通知。
说说刚才的表编辑的例子,表数据就是Subject ,图形就是 Observer 。从观察者与被观察者这两个名字上,让人得到被动的印象, 在 实际处理中,被观察者会发出通知 “ 我已经变化了哦 ” 。
4.2.9 Observable 模块
Ruby中为实现 Observer 模式提供了名为 observer 的库。 observer 库提供 Observer 模块。 Observer 模块的 API 请参见 表 4-3 。实际的库使用例 如 图 4-22所示 。
表 4-3 Observable模块的 API (应用编程接口)
解释 一下图 4-22中 的程序。首先是 require observer 库。利用库的时候,这是必须写的。
然后是定义被观察者类 Tick 。注释中也写到,该类每1 秒发 送1 次更新通知。它是相当于时钟的类。 Tick 的发音,好像是时钟的滴答滴答声。Tick 是被观察者,所以要将 Observable 模块 包含 进来。仅一句话就能让任意类成为被观察者,这正是Ruby 的威力。
tick 方法是主循环。有了这个处理,每隔 1 秒,就循环发出更新通知。虽然仅仅sleep 1 秒,但为了 保证 能在整秒发出更新通知, 便 以微秒为单位进行了补正( sleep1.0 - Time... 的部分) 。
实际的更新通知只是调用 changed 方法设置更新标志,然后用 notify_observers 方法通知观察者。他们都写在 loop (循环)内。
虽然像这样每次肯定都要定期发出更新通知的情况,把 changed 与 notify_observers 分离开来没有意义,但是考虑到频繁变化、每次更新处理的花费 都 比较大的情况, 还是 将二者分离开了。比如刚才的表编辑的例子中,与每次细微的变化都要更新图形相比,键盘输入告一段落时集中更新图形,应该更有效率。
图 4-22 使用observer 库的 Ruby 程序 , 观察者与被观察者不相互依存
后半部分的TextClock 类是观察者类。依照 Tick 发送 的通知,在控制画面上显示现在时刻。TextClock 类不是特定类的子类或者别的什么,只是拥有被更新通知调用的 update 方法。 update 方法接受Tick 类 notify_observers 方法传过来的时、分、秒三个整数参数。
实际显示用了ANSI 的转义字符( printf 以下的部分)。用 ESC[8D 将光标移到行首,后面显示时刻。为避免缓冲问题,每次调用 STDOUT.flush 。
定义了Tick (被观察者)与 TextClock (观察者)两个类之后就简单了。 先 生成Tick 类的对象(图 4-22 倒数第3 行的部分),然后使之与 TextClock 类相关联 ,最 后启动Tick 类的主循环(图 4-22 的末尾部分) , 就这么多。
执行图 4-22 的程序,控制画面上就会显示出一个数字时钟。因为是无限循环,想要停止时,请按下Ctrl+C 。
这次只做了一个观察Tick 类的对象 TextClock ,如果愿意,可以添加任意多个观察者。比如,对同一个 Tick ,不光能添加文字时钟,还可以添加图形时钟。
这个程序最应该注意的一点是,Tick 类与 TextClock 之间的关联,只用一行(图 4-22 倒数第2 行)就完成了。 Tick 类与 TextClock 类之间,只有 “ 更新以后,调用update 方法 ”以及“在 update方法中,传递时、分、秒 ” 这种简单的约定,不存在别的关系。只要是遵守相同约定的类,都可以简单地进行交换。
可以看出,使用Observer 模式,显然能够降低相互依 赖性 。既可以将观察者类做成零部件,又可以根据需要更换被观察者(比如测试用的假程序)。这个性质对于提高软件的开发效率和测试效率,都是很有用的。
4.2.10 Observer 模式与动态语言
动态语言的性质在Observer 模式中也很有用。由于 Ruby 的动态性质, observer 库具有以下的 灵活 性。
(1 )观察者类不必是特定类的子类 。
(2 )观察者类不必实现特定的接口(本来在 Ruby 中也没有接口) 。
(3 )观察者类的更新方法名可以自由决定( Ruby 1.9的功能) 。
(4 )观察者类更新方法的参数可以自由决定 。
(5 )被观察者类不必是特定类的子类 。
(6 )对被观察者类的要求,只是将 Observerable 模块 包含 进来 。
我想Java 那种静态语言也 具有 与Ruby 的 observer 库 相 同功能的库。事实上,有几种DI 容器( Dependency Injection Container )框架,也 具 有与observer 库相类似的处理。
但是,如果编码太繁杂了,或者需要用XML 文件代替 Java 来描述类之间关联的话,我认为 就 没有Ruby 这么好用 了 。
节选自《松本行弘的程序世界》