Ruby Ruby 元编程(第二版)--- 方法

qinsicheng · 2023年01月27日 · 650 次阅读

方法

​ 在 Ruby 这种动态语言中,方法的调用是极为灵活的,并不会在编译器就爆出各种错误,比方说我定义一个 User 类,我想调用 hi 方法,我并没有定义 hi 方法,但这并不妨碍我编写代码,运行期我去找 hi 方法,如果发现没有这个方法,最终我可以向这个类中添加我想要的方法在运行期间。这给了我们更多的操作空间,这将是我们要学习的。

现在我们有一个老的系统需要我们重构,老板要求系统自动为超过 99 美金的开销天添加标记

class DS
    def initialize ; end# 连接数据源

  # 原先设计的访问方法
    def get_cpu_info(id) ; end
    def get_cpu_price(id) ; end

    def get_mouse_info(id) ; end
    def get_mouse_price(id) ; end

    def get_keyBoard_info(id) ; end
    def get_keyBoard_price(id) ; end
end
ds = DS.new
# 获取信息
ds.get_cpu_info(1)
# 获取价格
ds.get_cpu_price(1)

我们现在需要将数据源封装起来,每个 computer 为一个对象,并为每个组件定义通用的方法

class Computer
  # data_source 就是上面的DS对象
    def initialize(computer_id,data_source)
        @id = computer_id
        @data_source = data_source
    end

    def mouse
        info = @data_source.get_mouse_info(@id)
        price = @data_source.get_mouse_price(@id)
        result = "Mouse: #{info} : (#{price})"
        result = "*" + result if price > 99
        return result
    end

    def cpu
        info = @data_source.get_cpu_info(@id)
        price = @data_source.get_cpu_price(@id)
        result = "Cpu: #{info} : (#{price})"
        result = "*" + result if price > 99
        return result
    end

    # ... 类似操作
end

​ 我们可以看到 mouse 和 cpu 就有大量的代码重复,如果后面还需要加其他的,则会让代码臃肿且冗余。

​ 我们有两种办法进行重构优化:动态方法 和 method_missing

动态方法

动态调用方法

class MyClass
    def method(content)
    end
end
obj = MyClass.new
obj.method "Hello World"
# 动态派发和上面普通调用的结果是一样的, 将 obj.method 替换为 obj.send(:method)
obj.send(:method,"Hello World")

为什么使用动态派发?

​ 因为可以在运行最后才决定具体调用哪个方法。而不是硬编码决定

这里使用:method,而不是"method",实际上是一样的

obj.send("method","Hello World")

​ :method 表示的是一个 Symbol 符号

​ "method"则是一个 String 字符串,一般在元编程我们常常使用 Symbol,因为 Symbol 是不可变的。字符串是可变的。

符号与字符串是很容易相互转化的

"abc".to_sym # => :abc
:abc.to_s    # => "abc"

动态定义方法

class MyClass
    # 这里就是定义了一个实例方法 将 def my_method 替换为 define_method :my_method,参数部分,通过Block传递
    define_method :my_method do |my_arg|
        my_arg * 3
    end
end
obj = MyClass.new 
obj.my_method 2

​ 在运行时定义方法的技术称为动态方法Module#define_method

为什么使用动态方法,而不是直接定义:def

​ 因为这样可以在运行最后决定方法名叫什么,和上面动态调用方法类似,我们想在运行期间再决定一些事情

重构 Computer 类

class Computer
    def initialize(computer_id,data_source)
        @id = computer_id
        @data_source = data_source
    end

    def mouse
        companent :mouse
    end

    def cpu
        companent :cpu
    end

    def companent(name)
    # 这里使用了动态调用方法
        info = @data_source.send("get_#{name}_info",@id)
        price = @data_source.send("get_#{name}_price",@id)
        result = "#{name}: #{info} : (#{price})"
        result = "*" + result if price > 99
        return result
    end
    # ... 类似操作
end

​ 我们使用动态派发的方式,抽离出一个公共组件,其他配件可以直接使用,代码量减少的多

​ 我们再用动态定义方法去试着重构一下代码

class Computer
    def initialize(computer_id,data_source)
        @id = computer_id
        @data_source = data_source
    end

    # 这里定义一个类方法,这里的self指向的是Computer类常量
    def self.define_companent(name) 
        # 根据传入的Symbol,创建相应的方法
        define_method(name) do
            info = @data_source.send("get_#{name}_info",@id)
            price = @data_source.send("get_#{name}_price",@id)
            result = "#{name}: #{info} : (#{price})"
            result = "*" + result if price > 99
            return result
        end
    end
    ## 这里主动调用 并动态创建对应的方法
    define_companent :mouse
    define_companent :cpu
    define_companent :keyboard
end

现在 Computer 已经剩不了多少代码了,我们使用内省方式缩减代码

class Computer
    def initialize(computer_id,data_source)
        @id = computer_id
        @data_source = data_source
    # 主动根据DS中给定的访问方法,创建访问方法,而不需要我们再去手动控制
        data_source.methods.grep(/^get_(.*)_info$/ |) {
            # 被正则表达式匹配到的方法,会依次调用这里传递的块儿,并将内容封装到 $1 全局变量中
            Computer.define_companent $1
        }
    end

    def self.define_companent(name)
        define_method(name) do
            info = @data_source.send("get_#{name}_info",@id)
            price = @data_source.send("get_#{name}_price",@id)
            result = "#{name}: #{info} : (#{price})"
            result = "*" + result if price > 99
            return result
        end
    end
end

​ 我们在初始化方法中加入几场代码就可以让代码更加简洁。

method_missing 方法

你将学习幽灵方法和动态代理

class User
    def method_missing(method,*args)
        puts "You called: #{method} (#{args.join(',')})"
    puts "(You alse passed it a block)" if block_given?

    end
end

obj = User.new 
obj.hi

​ 在 Ruby 中我们可以随意调用一个方法,而这个方法可能根本不存在,当运行时在当前对象的继承链上都没有找到这个方法时,会去找当前对象的 method_missing 方法,它就好像每个无家可归的人最终的点,method_missing 是BasicObject中定义的私有实例方法,所以每个子类都可以使用这个方法,而 BasicObject 中是直接抛出这个异常,所以需要我们自己去重写。method_missing也叫做幽灵方法。

现在我们通过 method_missing 来重构我们的 Computer 类

class Computer
    def initialize(computer_id,data_source)
        @id = computer_id
        @data_source = data_source
    end
    # name 为 调用的方法名,args 表示参数 ,*表示接受所有的参数,封装为一个数组
    def method_miss(name,*args)
        # 判断@data_source是否有这个方法?如果没有则调用super.method_miss,也就是未找到该方法
        super if !@data_source.respond_to?("get_#{name}_info")
        # 如果有这个方法
        info = @data_source.send("get_#{name}_info",@id)
        price = @data_source.send("get_#{name}_price",@id)
        result = "#{name}: #{info} : (#{price})"
        result = "*" + result if price > 99
        return result
    end
end

​ 现在发现我们不需要再定义额外的方法了,直接通过幽灵方法来做判断与返回,这里方法:respond_to?表示该实例是否有目标方法,

​ 如果方法返回值为 Boolean,一般会在方法名定义时使用?,如:def is_black?()表示。

respond_to_missing 方法

如何问 Computer 对象是否响应幽灵方法?

cmp = Computer.new(0,DS.new)
cmp.respond_to?(:mouse)   # => false 

​ 无法响应到,因为 :mouse 是一个幽灵方法,我们需要重新定义respond_to_missing()

class Computer
  def respond_to_missing?(method,include_private=false)
    @data_source.respond_to?("get_#{method}_info") || super
  end
end

cmp = Computer.new(0,DS.new)
cmp.respond_to?(:mouse)   # => true

​ 所以正确的做法是每次覆写method_missing时,同时也覆写respond_to_missing?方法

动态代理

​ 通常,幽灵方法都是锦上添花的左右,不过有些对象的功能,几乎完全依赖于他,这些对象通常是一些封装对象,他们封装的可以是另一个对象,web 服务或者其他语言写成的代码,这些对象通过 method_missing 方法收集方法调用,并把这些调用转发到被封装的对象上

const_missing 方法

​ 如果对于一个常量的引用发现找不到,则会默认调用const_missing方法,将常量名作为一个符号进行传递。

​ 在 Rake 中就有使用,为了兼容老版本的 Task 和新版本的 Rake::Task。

module Rake
  class Task; end
  class FileTask; end
end

class Module
  def const_missing(const_name)
    case const_name
    when :Task
      p "提示:原有的Task,已经移入,Rake名称空间,请使用 Rake::Task"
      Rake::Task
    when :FileTask
      p "提示:原有的Task,已经移入,Rake名称空间,请使用 Rake::FileTask"
      Rake::FileTask
    end
  end
end
# main是Object的实例,Object是Class的实例,Class的父类是Module,所以当我们使用猴子补丁修改
# Module#const_missing时,main对象是继承到这个方法的。
# 我们想要访问Task常量,发现并不存在,则触发了const_missing(const_name)
p Task.new
p FileTask.new

method_missing 隐藏 Bug

我们设计一个按照人名,抽号码的小程序

class Roulette
    def method_missing(name,*args)
        person = name.to_s.capitalize
        3.times do
      # 这里在块儿内定义了number
            number = rand(10)+1
            puts "#{number}..."
        end
    # 这里又使用了number,因为作用域的不同,运行时找不到这个变量,所以默认会找 number这个方法,因为也没有这个方法,所以调用了method_missing方法,导致不断的重入
        "#{name} got #{number}"
    end
end
number_of = Roulette.new 
p number_of.bob 
p number_of.Jack    

不知道你是否可以看出来?当程序运行时会不断方法重入,直到栈溢出。所以我们需要进行改良。

class Roulette
    def method_missing(name,*args)
        person = name.to_s.capitalize
        # 判断是否名字是否存在,如果不存在,直接报错
        super unless %w[Bob Frank Bill].include? person
    # 将局部变量作用域移出
        number = 0
        3.times do
            number = rand(10)+1
            puts "#{number}..."
        end
        "#{name} got #{number}"
    end
end
number_of = Roulette.new 
p number_of.Bob 
p number_of.Frank   

白板类

​ 比如说上面的 number_of.display 我们希望实际调用 method_missing 方法,但实际上可能调用了 Object.display 方法,这是因为我们从 Object 类中继承了大量的方法,所以时常导致幽灵方法与继承方法的重复

​ 如果实例存在继承方法,则幽灵方法是失效的。我们有两个办法:

  1. 删除继承来的方法
  2. 写一个白板类,也就是很干净了的类,没有继承的方法

白班类:BasicObject

​ BasicObject 是 Object 的父类,其中定义的实例方法很少,所以我们可以让现有的类继承 BasicObject,从而可以避免继承 Object 类的方法,这是最简单的白板类实现方法

# [:__send__, :!, :instance_eval, :==, :instance_exec, :!=, :equal?, :__id__]
p BasicObject.instance_methods

删除方法

  1. Module#undef_method 删除所有的方法,包括继承的
  2. Module#remove_method 只删除接受者自己的方法

所以我们最终可以选择让Roulette 继承 BasicObject,或者删除指定方法

对比动态方法与幽灵方法

​ 幽灵方法更容易出现隐性 Bug,所以能使用动态方法,尽量使用动态方法,除非不得不使用时,才去使用,记住如果重写response_to? ,也要重写response_to_missing?

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号