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_worldend

上面是一个rpc server,编写一个函数,调用rpc命令进行注册。

采用define_method、eval、alias方法,可以实现一个判断rpc/目录下的*.rb文件,进行加载和rpc接口注册的功能,实现代码如下:

module RPC    require 'rack/rpc'    #require rpc/*.rb文件    Dir.glob(File.join(File.dirname(__FILE__), 'rpc', "*.rb")) do |file|      require file    end    class Runner < Rack::RPC::Server      #include rpc/*.rb and regsiter rpc call      #eg. rpc/god.rb   god.hello      @@rpc_list = []      Dir.glob(File.join(File.dirname(__FILE__), 'rpc', "*.rb")) do |file|        rpc_class = File.basename(file).split('.rb')[0].capitalize        rpc_list = []        #加载module下的方法到Runner这个类下面        eval "include Frigga::RPC::#{rpc_class}"        #获取声明的RPC接口        eval "rpc_list = Frigga::RPC::#{rpc_class}::RPC_LIST"        rpc_list.each do |rpc_name|          #alias一个新的rpc方法,叫old_xxxx_xxxx          eval "alias :old_#{rpc_class.downcase}_#{rpc_name} :#{rpc_name}"          #重新定义rpc方法,添加一行日志打印功能,然后再调用old_xxxx_xxxx rpc方法          define_method "#{rpc_class.downcase}_#{rpc_name}".to_sym do |*arg|            Logger.info "[#{request.ip}] called #{rpc_class.downcase}.#{rpc_name} #{arg.join(', ')}"            eval "old_#{rpc_class.downcase}_#{rpc_name} *arg"          end            #注册RPC调用          rpc "#{rpc_class.downcase}.#{rpc_name}" => "#{rpc_class.downcase}_#{rpc_name}".to_sym          #添加到全局变量,汇总所有的rpc方法          @@rpc_list << "#{rpc_class.downcase}.#{rpc_name}"        end      end      def help        rpc_methods = (['help'] + @@rpc_list.sort).join("\n")      end      rpc "help" => :help    end  end #RPC

完成上述功能后,可以非常方便的开发rpc接口,例如下面这个IP地址增、删、查的代码,注册ip.list, ip.add和ip.del方法:

module RPC    module Ip      #RPC_LIST used for regsiter rpc_call      RPC_LIST = %w(list add del)      def list        $white_lists      end            def add(ip)         if ip =~ /^((25[0-5]|2[0-4]\d|[0-1]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[0-1]?\d\d?)$/          $white_lists << ip          write_to_file          return "succ"        else          return "fail"        end      end      def del(ip)        if $white_lists.include?(ip)          $white_lists.delete ip          write_to_file          return "succ"        else          return "fail"        end              end      def write_to_file          File.open(IP_yml, "w") do |f|            $white_lists.uniq.each {|i| f << "- #{i}\n"}          end      end    end   end

DSL——使用instance_eval

instance_eval是ruby语言中的瑞士军刀,特别是支持DSL方面。我们来看一下chef(一个开源的自动化部署工具)中设置文件模板的API:

template "/path/to/file.conf" do      source "file.conf.erb"      owner  "wilbur"      mode   "0744"    end

上述代码中,source、owner、mode需要从外部block,传递到template内部的block中,为了实现该目的,采用了instance_eval代码如下:

class ChefDSL      def template(path, &block)        TemplateDSL.new(path, &block)      end    end    class TemplateDSL      def initialize(path, &block)        @path = path        instance_eval &block      end      def source(source); @source = source; end      def owner(owner);   @owner  = owner;  end      def mode(mode);     @mode   = mode;   end    end

上面这个小技巧使得TemplateDSL对象可以应用block,和在自己的scope一样。block可以访问和调用TemplateDSL中的变量和方法。

如果没有使用instance_eval,如下面的代码,ruby就会抛出一个NoMethodError,因为source、owner、mode无法在block中被访问到。

class TemplateDSL      def initialize(path, &block)        @path = path        block.call      end    end

当然也可以使用yeild传递变量的方式实现,但没有instance_eval简洁和灵活。

命令行交互——使用instance_eval

命令行交互,可以采用highline这个gem.但highline在有些方面不能满足我的需求,比如类似上面介绍的chef template功能,达到的效果如下,大大简化了重复代码:

#检查frigga fail,询问是否继续        Tip.ask frigga_fail? do          banner "Check some frigga failed, skip failed host and continue deploy?"          on :yes          on :quit do            raise Odin::TipQuitExcption          end        end        ...        #运行时显示结果如下:        Check some frigga failed, skip failed host and continue deploy? [yes/quit]        #输入yes继续,输入quit退出

实现代码如下:

require 'colorize'  class Tip    def self.ask(stat = true, &block)      new(&block).ret if stat == true    end    attr_reader :ret    def initialize(&block)      @opt = []      @caller = {}      @banner = ""      @ret = false      self.instance_eval(&block)      print "#{@banner} [#{@opt.join('/')}]: ".light_yellow      loop do        x = gets.chomp.strip.to_sym        if @opt.include?(x)          @ret = ( @caller[x].call if @caller.key?(x) )          if @ret == :retry            print "\n#{@banner} [#{@opt.join('/')}]: ".light_yellow            next          else            return @ret          end        else          print "input error, please enter [#{@opt.join('/')}]: ".light_yellow        end      end    end    def on(opt, &block)      @opt << opt      @caller[opt] = block  if block_given?    end    def banner(str)      @banner = str    end  end
ruby元编程在实际项目中的使用

相关文章:

你感兴趣的文章:

标签云: