《Effective Ruby:改善Ruby程序的48条建议》一第6条:了解Ruby如何构建继承体系

简介:

本节书摘来自华章出版社《Effective Ruby:改善Ruby程序的48条建议》一书中的第2章,第2.1节,作者 [美]彼得 J.琼斯(Peter J. Jones),更多章节内容可以访问云栖社区“华章计算机”公众号查看

第6条:了解Ruby如何构建继承体系

问题:当你向一个对象发送消息时,你知道Ruby是怎么定位到那个正确的方法的吗?答案看上去很简单:使用继承体系。之所以说答案是看上去简单,因为Ruby构建继承体系是在幕后进行的。这是Ruby用自己的方式来隐藏真正发生的事情的罕见情形之一,它常常带来不必要的困惑。Ruby提供给我们的用来发现继承关系中的类的方法并能完全解释其本质。还好,Ruby解释器在内部构建的继承体系是一致而简单的,只要你明白一些它的技巧。
探索Ruby查询方法的机制将揭开语言实现方式的面纱,并为我们提供解开类的真正继承体系的完美环境。这也是你应该知道的事情。好消息是读完这一条,你不会再对Ruby的对象模型感到惊讶。坏消息是我们需要从复习经典的OOP术语及一些常见的术语定义开始。下面这几个段落,你得忍忍。
对象是变量的容器。这些变量被称为实例变量并代表了一个对象的状态。每个对象都有一个特殊的内部变量来连接唯一的一个类。由于这个连接的存在,我们称该对象是该类的一个实例。
类是方法和常量的容器。这些方法被称为实例方法并代表了类的所有实例对象的行为。
事情变得有点困惑和迂回,因为类本身也是对象。因此,每个类也是一个被称为类变量的容器。这些类对象,像所有其他对象一样,也是有方法的。从技术上讲,这些方法与实例方法没有区别,但为了避免过度混淆,因此被称为类方法。换句话说,类是一种其变量被称为类变量、其方法被称为类方法的对象。(类对象也可以有实例变量,但我们会在第15条再讨论它)。
超类(superclass)是在类层次结构中表示一个类的父类的花哨的名字。如果类B继承自类A,那么类A是类B的超类。类有其特殊的内部变量来跟踪其超类。
模块和类除了一点以外在各方面都是相同的。所以,像类一样,模块也是对象,并且是一个特定类的实例。类是类Class的实例,而模块是类Module的实例。Ruby在内部实现模块和类使用了相同的数据结构,但限制了通过它们的类方法能做的事情(模块是没有new方法的)以及更严格的语法。
模块在Ruby中有很多用法,但现在我们只关心它在继承体系中的作用。尽管Ruby并不直接支持多继承,但可以通过include方法将模块引入类,这实现了和多继承类似的效果。
单例类(singleton class)是个令人困惑的名词,因为它是继承体系中一个匿名且不可见的类。
这个类从目的上带来的困惑并不比从名字上带来得多。有时候它们被称为本征类(eigenclasses)或是元类(metaclasses)。甚至Ruby解释器的源码中也是交换地使用着这些术语。本书中,我将始终称其为单例类,因为在Ruby中当你使用内省方法(introspection methods)如singleton_class和singleton_methods时它的名字就叫作单例。
单例类在Ruby中承担了重要角色,比如提供了存储类方法和从modules中引入的方法的空间。不像其他的类,它们是Ruby自身根据需要动态创建的。它们的创建也是有限制的。比如,你不能创建一个单例类的实例。你唯一需要记住的是,单例类仅仅是没有名字的、被加以限制的常规类。
接收者是调用方法的那个对象。比如,在“customer.name”中,被调用的方法是name而接收者是customer。当name方法执行时,self变量被设置在customer上,任何实例变量所访问的都将是customer对象。有时候接收者在方法调用时被省略,这时会隐式地将self设置为当前上下文。
哇!现在我们终于完成了Ruby对象模型中的词汇课程以及它们构建继承体系的方式。让我们开始装配一个小的类体系并在IRB中运行它。


89fc77aa95c7b03e57b806eda0b5db436d36fa4a

这段代码没有什么让人惊讶的。如果你调用customer对象的name方法,方法的查找会和你预期的完全一样。首先,Customer类会检查自身是否有这个实例方法。显然没有,于是继续搜索,顺着继承体系向上找到了Person类,在该类中找到了该方法并将其执行。如果在Person类中没有找到的话,Ruby会继续向上查找直到找到该方法或者到达根类BasicObject。你可以从!


92044b50f1d0e631aec00b376a21b19facc0b3c0

如你所知,如果方法在查找过程中直到类树的根节点仍然没有找到匹配的方法,那么它将重新从起点开始查找,不过这一次会查找method_missing方法。第30条会请你自行定义method_missing方法,因此这里我们不再涉及。也就是说,除了Kernel模块中method_missing的默认实现会引发异常告诉你说你调用了一个未定义的方法以外,不会发生别的什么了。
这提醒了我:是时候举个简单的例子来制造点困难了:


c105ba62ebc2a8f861c26b0141c219d2fae6e03a

我已经将name方法从Person类中取出并移到一个模块中,并将模块引入了Person类。Customer类的实例仍然可以如你所料响应name方法,但是为什么呢?显然,模块ThingsWithNames并不在继承体系中,因为Person类的超类仍然是Object类,那会是什么呢?其实,Ruby在这里对你撒谎了,我们需要多讲一点单例类。
当你使用include方法来将模块引入类时,Ruby在幕后悄悄地做了些事情。它创建了一个单例类并将它插入类体系中。这个匿名的不可见类被链向这个模块,因此它们共享了实例方法和常量。本例中Person类在包含模块ThingsWithNames时,Ruby创建了一个单例类并悄悄地作为Person类的超类插入了继承体系之中。你会说,“但是调用Person的superclass方法返回的是Object而不是ThingsWithNames啊。”是的,这是因为:单例类是匿名而不可见的,因此super-class和class方法都会跳过它。所以,一个更精确的类体系也需要包含模块,如图2-2所示。


cf9c9691180174046fe32ff2d16609f08040b6a1

当每个模块被类包含时,它会立即被插入继承体系中包含它的类的上方,以后进先出(LIFO)的方式。每个对象都通过变量superclass连接,像单向链表一样。这唯一的结果就是,当Ruby寻找一个方法时,它将以逆序访问每个模块,最后包含的模块最先访问到。很重要的一点是,模块永远不会重载类中的方法。因为模块插入的位置是包含它的类的上方,而Ruby总是会在向上检查之前先检查类本身。(好吧……这不是全部的事实。确保你阅读了第35条,来看看Ruby 2.0中的prepend方法是如何使其复杂化的。)
既然你已经适应了模块和单例类间的交互,那么我的责任是改变点什么从而让困难来得更猛烈(但可爱)一点:


cb5707773627f3410d10d3608ce377c700673054

如果你以前没见过这种语法,你可能会有点困惑,但这其实很明确。上面的代码定义了仅用于customer对象的一个方法。这个特定的方法在其他任何对象上都不能调用。你觉得Ruby是怎样实现它的呢?如果你指出单例类,那么你说对了。事实上,这个方法被称为单例方法。当Ruby执行这段代码时,它将创建一个单例类,将name方法作为其实例方法,随后将这个匿名类作为customer对象的类进行插入。不过即使此时customer对象的类是单例类,Ruby的内省方法class方法仍将跳过它并返回Customer。这让我们觉得难以理解,但让Ruby的实现变得简单。当它查找name方法时,仅需要遍历类的结构就可以了。不需要特殊的逻辑。
这也透露了一些为什么这种类被称为单例类的线索。多数类服务很多对象,比如Array类包含你的程序中用到的每个数组对象使用的方法。而单例类仅仅服务一个对象。
除了类方法以外,我没有更多的花样可以描述了。你将很高兴地发现你可能已经理解它们了。它们是我们已经探索过的单例类的另一种表现形式。
注意,当你定义单例方法时,你指定了一个对象(可以认为是接收者),它将是这个对象的所有者。当你定义一个类方法时,你通过定义类对象做了完全相同的事情,不论是通过类名还是更常见的self变量。对象毕竟是对象,即使它是类。因此类方法也是作为单例类的实例方法存储的。自己来看:

3bf5a96ff6baa2ea44d3b2ee8b6a4a2d6e41e2fd

最后,当Ruby希望查找一个方法时,只需要从继承体系着手。不论是实例方法还是类方法都是这样。一旦你理解了Ruby是如何操作继承体系的,那么理解查找方法的过程就非常简单了:
1.?找到接收者的类,实际上它可能是一个隐藏的单例类。(记住,你在Ruby代码中不能这样做,因为class方法会跳过单例类。你得直接调用singleton_class方法。)
2.?查找该类中存储的实例方法列表。如果找到该方法就停止搜索并执行代码。否则,继续第3步。
3.?顺着继承体系向上找到其超类并重复第2步。(这个超类也可能是另一个单例类。在Ruby中,通过superclass方法,它可能是不可见的。)
4.?重复第2步和第3步直到Ruby找到这个方法或者抵达体系的顶点。
5.?当抵达顶点时,Ruby回到原来的接收者的第1步的地方继续查找,不过这回找的是method_missing方法。
如你所见,当方法存储于不可见的单例类时,在代码中很难明确地看到整个继承体系。但是你能使用Ruby为了内省而提供的几个方法来拼出完整的场景:
singleton_class方法返回接收者的单例类,如果不存在就创建它。
ancestors方法返回组成了继承体系的所有类和模块的数组。它只能在类和模块上被调用,并会跳过单例类。
included_modules方法返回和ancestors方法一样的数组,不过其中所有的类都被过滤掉了。
还好,你很少会使用这些方法。明白继承体系的构建和内部搜索原理已经足够解决大多数情况了。如果需要的话,第35条包含了一些例子。
要点回顾
要寻找一个方法,Ruby只需要向上搜索类体系。如果没有找到这个方法,就从起点开始搜索method_missing方法。
包含模块时Ruby会悄悄地创建单例类,并将其插入在继承体系中包含它的类的上方。
单例方法(类方法和针对对象的方法)存储于单例类中,它也会被插入继承体
系中。
相关文章
|
7月前
|
XML 前端开发 安全
构建自己的MVC框架(Ruby语言实现)-- 2. 创建ApplicationController
构建自己的MVC框架(Ruby语言实现)-- 2. 创建ApplicationController
|
7月前
|
前端开发 关系型数据库 开发工具
构建自己的MVC框架(Ruby语言实现)-- 开篇
构建自己的MVC框架(Ruby语言实现)-- 开篇
|
7月前
|
缓存 监控 数据库
使用Ruby构建可扩展的Web应用程序
在当今科技驱动的世界中,Web应用程序成为了企业和个人进行业务活动、提供服务和与用户互动的重要方式。而Ruby作为一种简洁、优雅且易于学习的编程语言,已经成为许多开发者的选择。本篇博客将介绍如何使用Ruby构建可扩展的Web应用程序。
59 0
|
7月前
|
前端开发 关系型数据库 开发工具
构建自己的MVC框架(Ruby语言实现)-- 第一章 从零到“它工作了!”
构建自己的MVC框架(Ruby语言实现)-- 第一章 从零到“它工作了!”
|
测试技术 C++ Ruby
C++程序中嵌入Ruby脚本系统
Ruby,一种为简单快捷面向对象编程(面向对象程序设计)而创的脚本语言,由日本人松本行弘(まつもとゆきひろ,英译:Yukihiro Matsumoto,外号matz)开发,遵守GPL协议和Ruby License。
1562 0
|
消息中间件 安全 Ruby
Nanite:Ruby程序的一个自我装配集群
本文讲的是Nanite:Ruby程序的一个自我装配集群,Nanite(由Ezra Zygmuntowicz开发)是Engine Yard云计算策略的一个新兵:它是“Ruby程序的一个自我装配集群”,用以构筑高度可伸缩的Web应用的后端。
1096 0
|
程序员 Ruby
《Effective Ruby:改善Ruby程序的48条建议》一导读
学习一门新的编程语言通常需要经过两个阶段。第一阶段是学习这门编程语言的语法和结构。如果我们具有其他编程语言的经验,这个阶段通常只需要很短的时间。以Ruby为例,接触过其他面向对象语言的程序员对Ruby的语法也会比较熟悉。有经验的程序员对于语言的结构(如何根据语法构建应用程序)是很熟悉的。
1207 0
|
NoSQL 数据库 Ruby
我的第一个Ruby On Rails + MongoDB程序
    最近想进一步学习一下MongoDB,而很久之前使用过ROR,正好也凑个机会重新拾起来。下面是建立第一个项目的过程。        主要参考文档:        1. Rails 3 - Getting started        2.
926 0