Ruby中的设计模式

        继续 节讲述过的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 这么好用

 


节选自《松本行弘的程序世界》

 

你可能感兴趣的:(Ruby中的设计模式)