写点什么

使用单实例类来处理对象元信息

  • 2008-01-21
  • 本文字数:3114 字

    阅读完需:约 10 分钟

假设你有大量的对象 —— 一个对象图 ——它们是一些操作或者 API 调用的结果。任务:分析数据并将分析结果作为对象图的元数据。

这么说过于抽象?请考虑一个编辑器是如何工作的:解析器产生一个代表代码的树状结构的解析树(或者抽象语法树,AST)。然后:许多算法遍历解析树,处理数据:收集在符号表中的符号,做类型推论,使用类型做数据类型检查,等等。

但是请稍等:最后两步处理有问题:类型推论代码在哪里存贮它产生的数据为类型检查者使用?最方便的方式是将数据存储到需要数据的地方 —— 例如,如果推论出(AST 中的)一个表达式节点返回类型 Foo,那么最好就在这个节点中存储该信息。

为了说明我们的解决方案,我们会看一些 AST 方面的工具 —— 不是一个编译器,而是与编译器有类似需求的工具。在 Ruby 中, ParseTree 类库以 AST 的形式返回 Ruby 源代码。例子:

[:vcall, :obj, :say_hello, [:array, [:lit, 42], [:lvar, :foo]]]这是一些以 ParseTree AST 方式展现的 Ruby 代码。由于此处是一篇文章而不是一个虚拟机,以对象和组织为一个图的对象引用的方式来说明问题有点困难 —— 所以我们使用 ParseTree 的 s-expr 形式来表示树。S-exprs 是嵌套的列表,每个列表代表树中的一个节点,列表的第一个元素表明了节点的类型。在上例中,该节点代表对一个虚拟方法(vcall)的调用。参数包括方法接收者(即被调用的方法中的“self”对象),方法名称和方法的参数。

我们讨论的工具可以是一个类型推论器,静态分析工具或者自动重构工具。对于这类工具的工作需求之一是类型推论,也就是,决定变量或者表达式的类型。例如,这个 AST 子句代表了一个对方法 to_s 的调用:

[:vcall, :obj, :to_s]从上面的句子中可以收集到什么信息,可以用这些信息做什么?例如,返回值的类型是难以决定的 —— 但是既然它是 to_s 方法,它会返回代表一个对象的字符串 —— 那就让我们认为它返回”String”类型。这不是必然为真的 —— 这是一个猜测。对于诸如代码补全之类的工具,这个推测已经足够好了 —— 100%的精确在此情况下是不可能的。在某些情况下,可以收集到更多信息,分析器可以确定更精确的信息。

分析器处理解析树,用分析产生的元数据来注解 AST。为了保持代码的模块化,将分析器与元数据的消费者分离是个好主意。元数据的消费者可能是遍历 AST 并使用元数据做某些事的代码。例如,Ruby 编辑器可以高亮一个覆盖了其超类中方法的方法。

解决方案

让我们长话短说:这里是一个 ParseTree 节点的注解方案:

node = [:vcall, :obj, :to_s] <br></br>def node.set_metadata(key, value)<br></br>  @_metadata ||= Hash.new<br></br>  @_metadata[key] = value<br></br>end <br></br>def node.metadata<br></br> @_metadata ||= {}<br></br>end<br></br>node.set_metadata(:type, :String)这段代码是做什么的?

秘密武器:单实例类

在 Ruby 中每个对象都是一个类的实例。不象许多其他 OOP 语言,Ruby 允许你改变一个对象的类。不要把改变对象的类与打开类(open class)相混淆。在 Ruby 中,修改一个类是可能的 —— 甚至在运行时。单实例类同样是可以修改的 —— 只不过它们的改变仅仅影响一个对象的类。看起来差别似乎很小 —— 但是它对于限制修改类对该类对象带来的影响有很大的好处。另一方面,打开类对类定义的修改影响了所有的代码和所有相应对象。打开类是有用的,但是也可能会和其他人的代码搅和在一起,而且如果有太多的代码在公共类上作打开动作,可能会与已有方法发生名称冲突。在 ParseTree 节点这个案例中 —— 这些节点是普通的 Ruby 数组 —— 意味着仅仅真正被使用的数组对象才受到影响 —— 而不是在堆中的所有的数组都受到了影响。

再换个角度来看:使用单实例类,对于类的改变是局部化的,仅对产生和使用单实例类的代码有影响 —— 这些变化对于外界永远是不可见的。而另一方面,打开类是全局性的改变:一个类名称是一个全局的变量,例如“String”指向代表字符串的类对象。就像作用域较小的变量(本地变量,成员变量)应该比作用域较大的全局变量更优先使用,在单实例类中作出的变化也应该优先于打开类。

当然,选择哪个方案(单实例或者打开类)取决于具体的情况。对于打开类,类的改变发生一次 —— 在改变之后,该类的所有对象都拥有了增加的方法。对于单实例类,类的改变(即创建单实例类,改变对象的类指针指向单实例类)在每次改变的时候都发生。

如下所示,语法是简单的:

def object_variable.method_name()<br></br> # code<br></br>end指向对象的变量前缀于方法名。一种更灵活的更模块化的方式是使用混入(Mixin)来做。混入允许将处理某些方面的方法集合在一个模块中然后将这些方法一次性的混入到类中。例如:

复制代码
module Metadata
def set_metadata(key, val)
@_metadata[key] = val
end
def metadata
@_metadata ||= {}
end
end
x = [:vcall, :obj, :to_s]
x.extend Metadata
x.set_metadata(:type, :String)

混入允许将定义在一个模块中的方法混入一个类中 —— 在这里,混入的是节点对象的单实例类。再次重申:现在只有这个对象具有这些混入的方法。

另一个例子 —— 除去死代码

如果静态分析器代码的例子过于抽象,那么让我们尝试另外一个工具:一个死代码移除器。“死代码”是实际上没有做任何事情的代码,它们被除去后不会影响程序的行为。例如这样的代码:

for x in [1,2,3] do<br></br>end根据我们的注解概念,我们可以找出这类代码并象这样来注解:

node.metadata(:dead_code, :true)但是将代码标记为死的仅仅是第一步 —— 现在我们需要除去它们。在这里我们会容易看到将代码分析和具体动作分离的意义。一个 IDE 也许仅仅希望高亮某些代码为死代码,但是 IDE 也可能提供了一种简单的方式(一种快速修复)来除去代码。

如何除去这段死代码?当然 ,可以将节点从 AST 中取出然后使用 Ruby2Ruby 来生成 Ruby 源代码。但是,这不是一个非常优美的解决方案:Ruby2Ruby 将 ParseTree AST 转换为 Ruby 源代码,但是它会丢失很多有用的信息,例如格式(空格)和注释。在 IDE 或者重构 / 清理工具中丢失这些信息是不可接受的。

元数据来救场了:节点可以用注解来记录来源的位置 —— 例如:在起初的源代码中的什么地方发现了这个特定的节点。有了这个信息就容易了:清理代码只需要扫描代码,发现所有标记为 dead_code 的节点并根据元数据中的源代码位置的字符偏移量将这些节点在源文件中删除。(当然 一旦这些节点被删除了,剩下的节点的偏移量会变化。这个问题可以通过简单地跟踪被删除的字符数量,并将此数量从实际的偏移量中减去来解决。这样,就不必重新解析和分析代码了)。

结论

这篇文章中的例子着重于语言方面的工具 —— 但是文章的思想是适用于所有的对象集合的。在任何对象图需要被注解的地方,也许是在独立开发的解析器的多个处理步骤中,将注解与节点一起保存是很方便的。如果对象图的类不是在开发者的控制之下而且在其他地方被广泛使用(例如,在 ParseTree 中的数组类),单实例类是个好的选择。对于其他情况,打开类也许是一个更好的方案。

本文所提到的特殊工具的实现:ParseTree 在 Ruby 1.8. x、Rubinius 和 JRuby ( jparsetree ) 中都有实现。JRuby 中的 ParseTree 版本增加了每个节点的来源位置信息,所以更容易修改源文件。愿你也能想出一些工具的创意并实现它们 —— 用 Ruby 是很容易的。

查看英文原文 Using singleton classes for object metadata


译者简介: 曹云飞,西安交通大学计算机软件硕士。现就职于 Ethos ,热衷于计算机理论与应用技术的钻研,软件架构与敏捷开发,目前从事 consumer product 方面的工作。参与 InfoQ 中文站内容建设,请邮件至 china-editorial@infoq.com

2008-01-21 02:561660
用户头像

发布了 47 篇内容, 共 12.9 次阅读, 收获喜欢 3 次。

关注

评论

发布
暂无评论
发现更多内容

Webpack中的高级特性

Geek_02d948

webpack

华为云ECS,弹性伸缩按需选择,让企业以更低成本享受云服务

爱尚科技

华为云ECS,如何助力数字化企业创新发展

爱尚科技

太厉害了!阿里年薪120W架构师整理的学习笔记,看完收获良多

小二,上酒上酒

Java 架构 微服务 高并发

一站式全覆盖数据 I/O 平台 - Alluxio 与 Aunalytics 的完美结合

Alluxio

分布式 presto Alluxio 大数据 开源 #开源

Nodejs相关ORM框架分析

coder2028

node.js

华为云ECS,去除现代化企业服务器的数据安全忧虑

爱尚科技

京东T8连夜肝出的《JVM性能优化知识点》吊打所有提问的面试官

小二,上酒上酒

性能优化 JVM Java虚拟机

重塑感知,荣耀金洋,银行APP用户体验外滩峰会相聚上海

易观分析

银行 峰会 上海

特征平台在数禾的建设与应用

阿里云大数据AI技术

sql 大数据 flink 企业号十月 PK 榜

华为云CDN,海量资源智能路由,让内容传输更快一步

IT科技苏辞

华为云CDN,用技术实力助力企业创新,促进产业化转型

IT科技苏辞

Webpack完整打包流程分析

Geek_02d948

webpack

融云一站式「云市场」上线,携手生态伙伴,共建价值平台

融云 RongCloud

通讯协议 市场 CND

阿里技术专家压箱底好货:Redis深度历险笔记

小小怪下士

Java redis 程序员

来了!Spring Boot从入门到入土的私藏教程,不收藏你就亏了

小二,上酒上酒

spring springboot

【文本检测与识别白皮书-3.2】第一节: 基于分割的场景文本识别方法

合合技术团队

人工智能 文字识别 文字检测 智能识别

太厉害了!GitHub上标星80K的微服务实战笔记,看完跪了

小二,上酒上酒

Java 微服务

阿里架构师耗时三个月整理的 Spring实战笔记:入门到实战

小二,上酒上酒

spring

京东T8架构师墙裂推荐:史上最全高性能MySQL实战(赶紧收藏)

小二,上酒上酒

Nodejs:ESModule和commonjs,傻傻分不清

coder2028

node.js

华为云ECS,弹性云服务器标杆,众多企业的共同选择

爱尚科技

javascript尾递归优化

hellocoder2029

JavaScript

企业为什么需要UI快速开发框架

力软低代码开发平台

腾讯T4耗时36天整理出了:多线程+JVM+设计模式+Redis+MySQL

小二,上酒上酒

MySQL redis JVM 多线程

员工离职率高如何解决?

优秀

企业管理 员工离职

javascript 高级编程 之 Array 用法总结

hellocoder2029

JavaScript

js通过经纬度来计算两地之间的距离

源字节1号

微信小程序 软件开发 前端开发 后端开发

将 NGINX 部署为 API 网关,第 3 部分:发布 gRPC 服务

NGINX开源社区

nginx api 网关 gprc

Go语言入门07—指针

良猿

Go golang 后端 10月月更

清华年薪百万大佬,带你深入JVM实战调优,看完还敢说你懂JVM

小二,上酒上酒

Java 马士兵

使用单实例类来处理对象元信息_Ruby_Werner Schuster_InfoQ精选文章