Ruby元编程技术详解(Ruby Metaprogramming techniques)

我最近考虑了很多元编程(Metaprogramming)的问题,并希望看到更多这方面技术的例子和讲解。无论好坏,元编程已经进入Ruby社区,并成为完成各种任务和简化代码的标准方式。既然找不到这类资源,我准备抛砖引玉写一些通用Ruby技术的文章。这些内容可能对从其它语言转向Ruby或者还没有体验到Ruby元编程乐趣的程序员非常有用。

1. 使用单例类 Use the singleton-class

  许多操作单个对象的方法是基于操作其单例类(singleton class),并且这样可以使元编程更简单。获得单例类的经典方法是执行如下代码:

代码如下:

sclass = (class << self; self; end)

  RCR231建议这样定义Kernel#singleton_class方法:

代码如下:

module Kernel  
  def singleton_class  
    class << self; self; end 
  end 
end

  我会在下文使用这个方法。

2. DSL的使用类方法来修改子类 Write DSL's using class-methods that rewrite subclasses

  当你想创建一个DSL来定义类信息时,最常见的问题是怎样表示信息来让框架的其它部分使用。以定义一个ActiveRecord模型对象为例:

代码如下:

class Product < ActiveRecord::Base  
  set_table_name 'produce'   
end

  在这个例子中,令人感兴趣的是set_table_name的使用。这是怎么起作用的呢?好吧,这里涉及到一个小魔法。这是一种实现方法:

代码如下:

module ActiveRecord  
  class Base  
    def self.set_table_name name  
      define_attr_method :table_name, name  
    end 
    def self.define_attr_method(name, value)  
      singleton_class.send :alias_method, "original_#{name}", name  
      singleton_class.class_eval do   
        define_method(name) do     
          value  
        end 
      end 
    end 
  end   
end

  这里令人感兴趣的是define_attr_method。在这个例子中我们需要获得Product类的单例类,但又不想修改ActiveRecord::Base。通过使用单例类我们达到了这个目的。我们为原来的方法取别名,再定义新的存取器(accessor)来返回值。如果ActiveRecord需要table name就可以直接调用存取器。这种动态创建方法和存取器的技术在单例类是很常见的,特别是Rails。

3. 动态创建class和module Create classes and modules dynamically

  Ruby允许你动态创建和修改class和module。你可以在没有冻结的class或module上做任何修改。特定情况下会很有用。Struct类可能是最好的例子:

代码如下:

PersonVO = Struct.new(:name, :phone, :email)  
p1 = PersonVO.new(:name => "Ola Bini")

  这会创建一个新类,并赋给PersonVO,然后创建一个类的实例。从草稿创建新类并定义新方法也很简单:

代码如下:

c = Class.new 
c.class_eval do 
  define_method :foo do 
    puts "Hello World" 
  end 
end 
c.new.foo    # => "Hello World"

  除了Struct,还能在SOAP4R和Camping找到轻松创建类的例子。Camping尤其令人感兴趣,因为它有专门的方法创建这些类,被你的controller和view继承。Camping的许多有趣的功能都是用这种方式实现的:

代码如下:

def R(*urls); Class.new(R) { meta_def(:urls) { urls } };   
end

  这使得可以这样创建controller:
class View < R '/view/(\d+)' 
  def get post_id  
  end 
end

  你也可以这样创建module,然后在类中包含module。

4. 使用method_missing来做有趣的事 Use method_missing to do interesting things

  除了闭包(block),method_missing可能是Ruby最强大的特性,也是最容易滥用的一个。用好method_missing的话有些代码会变得超级简单,甚至是不能缺少。一个好的例子(Camping)是扩展Hash:

代码如下:

class Hash 
  def method_missing(m,*a)  
    if m.to_s =~ /=$/  
      self[$`] = a[0]  
    elsif a.empty?    
      self[m]  
    else 
      raise NoMethodError, "#{m}" 
    end 
  end 
end

  就可以这样使用hash:

代码如下:

x = {'abc' => 123}  
x.abc # => 123  
x.foo = :baz 
x # => {'abc' => 123, 'foo' => :baz}

  如你所见,如果有人调用了一个hash不存在的方法,则会搜索内部集合。如果方法名以=结尾,则会赋给同名的key。

  Markaby中可以找到另一个很好的method_missing技巧。以下引用的代码可以生成任何包含CSS class的XHTML标签:

代码如下:

body do 
  h1.header 'Blog' 
  div.content do 
    'Hellu' 
  end 
end

会生成:

代码如下:

<body> 
  <h1 class="header">Blog</h1> 
  <div class="content"> 
    Hellu  
  </div> 
</body>

  绝大多数这种功能,特别是CSS class名是通过method_missing设置了self的属性然后返回self。

5. 方法模式的调度 Dispatch on method-patterns

  这对于无法预测的方法来说可以轻松的达到可扩展性。我最近创建了一个小型验证框架,核心的验证类会找出自身所有以check_开头的方法并调用,这样就可以轻松地增加新的验证:只要往类或实例中添加新方法。
methods.grep /^check_/ do |m|  
  self.send m  
end

  这非常简单,并且难以置信的强大。可以看一下Test::Unit到处使用这种方法。

6. 替换方法 Replacing methods

  有时候一个方法的实现不是你要的,或者只做了一半。标准的面向对象方法是继承并重载,再调用父类方法。仅当你有对象实例化的控制权时才有用,经常不是这种情况,继承也就没有价值。为得到同样的功能,可以重命名(alias)旧方法,并添加一个新的方法定义来调用旧方法,并确保旧方法的前后条件得到保留。

代码如下:

class String 
  alias_method :original_reverse, :reverse 
  def reverse   
    puts "reversing, please wait..." original_reverse  
  end 
end

  一个极端的用法是临时修改一个方法,然后再还原。例如:

代码如下:

def trace(*mths)  
  add_tracing(*mths) # aliases the methods named, adding tracing      
  yield 
  remove_tracing(*mths) # removes the tracing aliases  
end

  这个例子展示了编写add_tracing和remove_tracing的一种典型方法。它依赖于第1条的单例类:

代码如下:

class Object    
  def add_tracing(*mths)      
    mths.each do |m|   
      singleton_class.send :alias_method, "traced_#{m}", m   
      singleton_class.send :define_method, m do |*args|  
        $stderr.puts "before #{m}(#{args.inspect})" 
        ret = self.send("traced_#{m}", *args)  
        $stderr.puts "after #{m} - #{ret.inspect}" 
        ret  
      end 
    end    
  end 
  def remove_tracing(*mths)     
    mths.each do |m|  
      singleton_class.send :alias_method, m, "traced_#{m}" 
    end 
  end 
end 
"abc".add_tracing :reverse

  如果这些方法是添加到module(有一点点不同,看你能不能写出来!),你也可以在类而非实例上添加和删除tracing。

7. 使用nil类来引入空对象的重构 Use NilClass to implement the Introduce Null Object refactoring

  在Fowler的重构中,“引入空对象”的重构是一个对象要么存在,要么为空时有一个预定义值。典型例子如下:

代码如下:

name = x.nil? ? "default name" : x.name

  目前基于Java的重构会推荐创建一个类似于null的子类。例如NullPerson会继承Person,重载name方法总是返回"default name"。但是在Ruby中我们可以打开类,可以这样做:

代码如下:

def nil.name; "default name"; end 
x # => nil  
name = x.name # => "default name"

8. 学习eval的不同版本 Learn the different versions of eval

  Ruby有几种版本的执行方法(evaluation)。了解它们的区别和使用情景是很重要的。有eval、instance_eval、module_eval和class_eval几种。首先,class_eval是module_eval的别名。其次,eval和其他的有些不同。最重要的是eval只能够执行一个字符串,其它的可以执行block。这意味着eval是你做任何事的最后选择,它有它的用处,但绝大多数情况下应该用instance_eval和module_eval执行block。

  eval会在当前环境执行字符串,除非环境已经提供绑定(binding)。(见第11条)

  instance_eval会在接收者(reveiver)的上下文中执行字符串或block,没有指定的话self会作为接收者。

  module_eval会在调用的module的上下文中执行字符串或block。这个比较适合在module或单例类中定义新方法。instance_eval和module_eval的主要区别在于定义的方法会放在哪里。如果你用String.instance_eval定义foo方法会得到String.foo,如果是用module_eval会得到String.new.foo。

  module_eval几乎总是适用;要像对待瘟疫一样避免使用eval。遵守这些简单的规则会对你有好处。


9. 实例变量的内省 Introspect on instance variables

  Rails使用了一个技巧来使controller中的实例变量也能用在view中,就是内省一个对象的实例变量。这会严重破坏封装,然而有时候确实非常顺手。可以很容易的通过instance_variables、instance_variable_get和instance_variable_set实现。要把所有实例变量从一个复制到另一个,可以这样:

代码如下:

from.instance_variables.each do |v|  
  to.instance_variable_set v, from.instance_variable_get(v)  
end

10. 从block创建Proc并公开 Create Procs from blocks and send them around

  把一个Proc实例化保存在变量中并公开的做法使得很多API容易使用。这是Markaby用来管理CSS class定义的一种方法。很容易把block转换成Proc:
def create_proc(&p); p; end 
create_proc do 
  puts "hello" 
end       # => #<Proc ...>

  调用也很容易:
p.call(*args)

  如果要用proc来定义方法,应该用lambda来创建,就可以用return和break:
p = lambda { puts "hoho"; return 1 }  
define_method(:a, &p)

  如果有block的话method_missing会调用block:
def method_missing(name, *args, &block)  
  block.call(*args) if block_given?  
end 
thismethoddoesntexist("abc","cde") do |*args|  
  p args  
end  # => ["abc","cde"]


11. 用绑定(binding)来控制eval Use binding to control your evaluations

  如果你确实需要用eval,你可以控制哪些变量是有效的。这时候要用kernel方法binding来获得所绑定的对象。例如:

代码如下:

def get_b; binding; end 
foo = 13  
eval("puts foo",get_b) # => NameError: undefined local variable or method `foo' for main:Object

  ERb和Rails用这种技术来设置哪些实例变量是有效的。例如:

代码如下:

class Holder  
  def get_b; binding; end 
end 
h = Holder.new 
h.instance_variable_set "@foo", 25  
eval("@foo",h.get_b)

  希望这些技巧和技术已经为您阐明了元编程。我并不声称自己是Ruby或者元编程方面的专家,这只是我对这个问题的一些想法。

(0)

相关推荐

  • Ruby元编程基础学习笔记整理

    笔记一: 代码中包含变量,类和方法,统称为语言构建(language construct). # test.rb class Greeting def initialize(text) @text = text end def welcome @text end end my_obj = Greeting.new("hello") puts my_obj.class puts my_obj.class.instance_methods(false) #false means not i

  • ruby元编程之method_missing的一个使用细节

    我们知道顶级域,定义域的self是啥? 复制代码 代码如下: puts self    #main puts self.class #Object 我们知道当一个方法被调用的时候,如果没有对象接受,默认就是self,如: 复制代码 代码如下: def tell_me_who     puts self end tell_me_who  #main 方法调用是这样的步骤,先查找当前对象的所在类的实例方法存在方法与否,如果存在,调用方法,如果不存在则查看superclass,直到 BasicObje

  • ruby元编程实际使用实例

    很喜欢ruby元编程,puppet和chef用到了很多ruby的语言特性,来定义一个新的部署语言. 分享几个在实际项目中用到的场景,能力有限,如果有更优方案,请留言给我:) rpc接口模板化--使用eval.alias.defind_method require 'rack/rpc' class Server < Rack::RPC::Server def hello_world "Hello, world!" end rpc 'hello_world' => :hello

  • Ruby元编程的一些值得注意的地方

    避免无限循环的元编程. 写一个函数库时不要使核心类混乱(不要使用 monkey patch). 代码块形式最好用于字符串插值形式.         当你使用字符串插值形式,总是提供 __FILE__ 和 __LINE__,使得你的回溯有意义. class_eval 'def use_relative_model_naming?; true; end', __FILE__, __LINE__ define_method 最好用 class_eval{ def ... } 当使用 class_eva

  • Ruby元编程之梦中情人method_missing方法详解

    我最近读了些文章(比如这篇),宣传在 Ruby 里使用 method_missing 的. 很多人都与 method_missing 干柴烈火,但在并没有小心处理彼此之间的关系.所以,我想来探讨一下这个问题: ** 我该怎么用 method_missing ** 什么时候该抵挡 method_missing 的诱惑 首先,永远不要在还没花时间考虑你用得够不够好之前,就向 method_missing 的魅力屈服.你知道,在日常生活中,很少会让你以为的那样亟需 method_missing: 日常

  • ruby元编程之创建自己的动态方法

    method_missing是Ruby元编程(metaprogramming)常用的手法.基本思想是通过实现调用不存在的方法,以便进行回调.典型的例子是:ActiveRecord的动态查找(dynamic finder).例如:我们有email属性那么就可以调用User.find_by_email('joe@example.com'),虽然, ActiveRecord::Base并没有一个叫做find_by_email的方法. respond_to? 并不如method_missing出名,常用

  • Ruby和元编程之万物皆为对象

    开篇 空即是色,色即是空. 空空色色,色色空空,在Ruby语言中,万物皆为对象. Ruby是一个面向对象的语言(Object Oriented Language),面向对象的概念比其他语言要贯彻的坚定很多. Ruby中不存在Java中原始类型数据和对象类型数据之分.大部分Ruby中的的东东都是对象. 所以,想要掌握Ruby和Ruby的元编程,对象就是第一门必修功课.本回就着重研究一下Ruby中的对象. Ruby中的对象 如果你从其他面向对象的语言转来,一提到得到一个对象你可能会想到建立一个类,然

  • Ruby元编程小结

    今天被问到此类问题,以前总是觉得这个是比较宽泛的一个概念,自己即使是用过这些特性,但却一直不知道这叫"元编程" 直到今天被人问起的时候,方才顿悟一些,随后便在网上和自己的平实用的一些元编程做个小总结. 原来所谓的Ruby中的元编程,是可以在运行时动态的操作语言结构(如类.模块.实例变量等)的技术.你甚至于可以在不用重启的情况下,在运行时直接键入一段新的Ruby代码,并执行他. Ruby的元编程,也具有"利用代码来编写代码"的作用.例如,常见的attr_accesso

  • Ruby元编程技术详解(Ruby Metaprogramming techniques)

    我最近考虑了很多元编程(Metaprogramming)的问题,并希望看到更多这方面技术的例子和讲解.无论好坏,元编程已经进入Ruby社区,并成为完成各种任务和简化代码的标准方式.既然找不到这类资源,我准备抛砖引玉写一些通用Ruby技术的文章.这些内容可能对从其它语言转向Ruby或者还没有体验到Ruby元编程乐趣的程序员非常有用. 1. 使用单例类 Use the singleton-class 许多操作单个对象的方法是基于操作其单例类(singleton class),并且这样可以使元编程更简

  • java多线程编程技术详解和实例代码

     java多线程编程技术详解和实例代码 1.   Java和他的API都可以使用并发. 可以指定程序包含不同的执行线程,每个线程都具有自己的方法调用堆栈和程序计数器,使得线程在与其他线程并发地执行能够共享程序范围内的资源,比如共享内存,这种能力被称为多线程编程(multithreading),在核心的C和C++语言中并不具备这种能力,尽管他们影响了JAVA的设计. 2.   线程的生命周期 新线程的生命周期从"新生"状态开始.程序启动线程前,线程一直是"新生"状态:

  • ruby ftp封装实例详解

     ruby ftp封装实例详解 最近自己用ruby 封装了一个Net::FTP的工具类. class FtpTool def initialize() @current_ftp = create_ftp end # 获取指定格式的文件名称列表 # 例如: source = "test/*.txt" # 返回: [source/file_name.txt] def fetch_remote_filenames(source) return [] if source.blank? log_

  • VSCode + WSL 2 + Ruby环境搭建图文详解

    vscode配置ruby开发环境 vscode近年来发展迅速,几乎在3年之间就抢占了原来vim.sublime text的很多份额,犹记得在2015-2016年的时候,ruby推荐的开发环境基本上都是vim和sublime text,然而,随着vscode的发展,vscode下ruby的开发体验已经非常不错.现在基本上使用win 10 wsl2 + vscode + windows terminal的体验已经不逊于mac + vim (sublime) + item 2的体验了 总体步骤 使用w

  • JavaWeb会话技术详解与案例

    1.什么是会话: 2.会话技术有哪些: 什么是Cookie? Cookie,有时也用其复数形式 Cookies.类型为"小型文本文件",是某些网站为了辨别用户身份,进行Session跟踪而储存在用户本地终端上的数据(通常经过加密),由用户客户端计算机暂时或永久保存的信息. 3.cookie学习案例: cookie:是数组,中每个元素有:名称和值, 可以通过名称找到名称对应的元素. 重要: Cookie[] cookies = req.getCookies(); //创建cookie对象

  • Java WebService技术详解

    目录 WebService WebService简介 WebService原理 JAVA WebService规范 (1)JAX-WS: (2)JAXM&SAAJ: (3)JAX-RS: WebService入门案例 服务端的实现 客户端的实现 WSDL 文档结构 阅读方式 SOAP SOAP结构 UDDI Webservice的客户端调用方式 一:生成客户端调用方式 二:service编程调用方式 三:HttpURLConnection调用方式 使用注解修改WSDL内容 WebService

  • Java设计模式之享元模式示例详解

    目录 定义 原理类图 案例 需求 方案:享元模式 分析 总结 定义 享元模式(FlyWeight Pattern),也叫蝇量模式,运用共享技术,有效的支持大量细粒度的对象,享元模式就是池技术的重要实现方式. 原理类图 Flyweight :抽象的享元角色,他是抽象的产品类,同时他会定义出对象的内部状态和外部状态 ConcreteFlyweight :是具体的享元角色,具体的产品类,实现抽象角色,实现具体的业务逻辑 UnsharedConcreteFlyweight :不可共享的角色,这个角色也可

  • Java结构型设计模式之享元模式示例详解

    目录 享元模式 概述 目的 应用场景 优缺点 主要角色 享元模式结构 内部状态和外部状态 享元模式的基本使用 创建抽象享元角色 创建具体享元角色 创建享元工厂 客户端调用 总结 享元模式实现数据库连接池 创建数据库连接池 使用数据库连接池 享元模式 概述 享元模式(Flyweight Pattern)又称为轻量级模式,是对象池的一种实现.属于结构型模式. 类似于线程池,线程池可以避免不停的创建和销毁多个对象,消耗性能.享元模式提供了减少对象数量从而改善应用所需的对象结构的方式. 享元模式尝试重用

随机推荐