Ruby 如何知道一个类定义完了?

xjz19901211 · 2013年02月17日 · 最后由 xjz19901211 回复于 2013年02月24日 · 4990 次阅读

先看代码:

module M
  def a
    puts 'm-a'
  end
end

class A
  def self. inherited(cls)
     # cls.send(:include, M)
     # 这里直接inculde 好像是不行的
     # 等A initialize 时再self.send(:extend, M)倒是可以。。
  end
end

class B < A
   def a
      def 'b-a'
   end
end

我想为每一个继承自 A 的类做一些修改,使用 inherited 会被 B 的方法覆盖的,我想要的是这些修改覆盖掉 B 类的方法,以后再打开 B 类的修改我就不管了

其实就是 Rails 里,我有了现有的代码,然后有另一个项目,有一点点区别,以后也许会有很多不同的,我不想写 if 什么的,也不想在 before_filter 里面每次都 include 一个模块(以前有个项目就是,布一个项目,客户端请求哪个项目我就 extend 哪个项目的模块进来。。),只想等每个 Controller 类关闭之后,也就是 end 后根据我的 config 来用对应的模块中的方法覆盖此类中原来的方法。。不知道怎么做了

============

@wppurking@kenshin54 的帮助下目前的一个实现 见 22 楼

求优美的实现方式。。

另附一个有些烂的实现

require 'pry'
require 'rails'

NameSpace = "M"

module M
  module B
    def a
      puts 'ma'
    end

    def  b
      puts 'mb'
    end
  end
end

def get_module(cls)
  (NameSpace + "::" + cls.to_s).safe_constantize
end

class A
  def self.inherited(cls)
    super

    _module = get_module(cls)

    return unless _module

    cls.class_eval do
      def initialize
        super

        print self.class, ' init', "\n"

        _module = get_module(self.class)

        extend _module if _module
      end
    end

    nil
  end
end



class B < A
  def a
    puts 'ba'
  end
end

class C < B

end


B.new.a
B.new.b

C.new.a

# Result
# B init
# ma
# B init
# mb
# C init
# ba

实现的就是如上效果,如果我 M 模块中有 B 模块的话,等同于 B include 了这个模块,而且模块中的方法会替换掉 B 类中定义的相同的方法,上面的弄影响了正常的 initialize 方法,虽然在 Rails 的 Controller 中一般都没有使用这个东东,相比使用 method_added 然后 remove_method,感觉应该好一点吧。。


通过 method_added 也可以使得 initialize 可以正常使用

或者教我用其他方法解决呃。。

前提是现在的代码不用做过多改变。。

好高端啊~

为啥不用闭包?

话说也不需要啊,你之前的 include 的方式也可以啊,通过 config 配置来判断你要 include 什么。 载入什么不是重点吧。

有点不懂你的需求,呵呵

@badboy 关键是我在什么时候 include。。。 上那我代码里那个,a 方法被 B 的覆盖了,我想要的是模块 M 覆盖类 B 的 a 方法

你要在父类里,修改子类的方法? 子类调用 a 方法,a 会覆盖父类的 a,这是正常的继承。

你要让再父类去修改子类的 a。。。没想过!!! 为什么你不再 B 里去判断你的 include 呢?

有很多个 B,B1,B2 的,不同的还得 include 对应的模块,前提是有那个模块在,我总不能一个个去写吧。。

我现在正在给 A 写个 initialize,然后里面 self.send(:extend, M)

试验试验。。。

直接用个实例变量表示 A,B 不行吗

@jjym 怎么做?我没看懂,真不好意思

你这种情况要么在相关类都加载完后去执行 include,可以试试在 config.after_initialize 中执行,但是这对于一些 rails 启动时未加载,后续加载的类或者动态加载的类就无效了。

禁止子类或者 module 覆盖方法好像没这功能,但是有绕个圈来实现的,通过 method_added 来做,这个文章里有写,gem 比较老了,不知道能不能用,实现方式很简单,不能用可以自己再写个。

http://scie.nti.st/2008/9/17/making-methods-immutable-in-ruby/ https://github.com/up_the_irons/immutable

这需求类似 java 的 final method

@kenshin54 恩多谢,我先弄弄看

@xjz19901211 不知道我的理解是否正确。

require 'active_support/core_ext'

module M
 # 可以不需要 Concern 转而使用 self.included(mod) 加 mod.send(..) 处理
  extend ActiveSupport::Concern

  included do
    alias_method :ori_a, :a
    remove_method :a
  end

  def a
    puts 'm-a'
  end
end

class A
end

class B < A

  def a
    puts 'b-a'
  end

  # 因为需要 B 先有 a 方法, 才能够在 Module 中处理 a 方法.
  # 如果是动态 include  那应该没有问题.
  include M
end

b_klass = B.new
b_klass.a # m-a
b_klass.ori_a # b-a

@wppurking 这样是可以实现,不过每个模块要查找自己有定义的方法,然后 included 的时候再替换,最后就是那个 include M 要每个继承自 A 的类都要加上

我想要每个继承自 A 的类自动找到对应的模块然后加上。比如:B => M, B1 => M1....

就是只修改 A 这个类来实现

感觉是不是我想法有问题。。。。。。

以前我做过的事,rails 中的 before_filter 来弄

def load_project_module(project)
    project_module_name = project.classify
    return unless project_module_name.safe_constantize

    controller_name = (params[:controller] + "_controller").classify
    project_controller_name = project_module_name + "::" + controller_name
    project_controller = project_controller_name.safe_constantize

    return unless project_controller && project_controller_name == project_controller.to_s

    self.send(:extend, project_controller)
  end

这个在 Controller 里面确实去调用 extend 里的方法了,这个我真不知道是为什么。。 因为我刚刚试了下一个对象 extend 一个模块是不能覆盖掉已经有的方法。。 不知道什么情况,看来回公司后又要把 Ruby 元编程拿来看看了。。

难道是做了特殊的处理? 有点怀疑。。

@xjz19901211 换个方式,直接从 A 类处理了子类 include 的 module 的方法。感觉有点暴力啊,如果真要这么做一定要让所有单元测试跑通才行啊。

class A

  class << self
    # class macro 用于可动态调整, 例如
    # B.class_eval do
    #   include M2
    #   check_super :m2_b
    # end 
    # B.new.m2_b # m2-b
    #
    def check_super(method_name)
      # 根据 method_name 查询所有的 module 的第一个 instance_method, 如果存在, 并且 self 不是 A 这个父类的话, 就使用 module 中的 method_name
      first_module_method = self.included_modules.map { |_module| _module.instance_method(method_name) if _module.instance_methods(false).include?(method_name) }.first
      self.send(:remove_method, method_name) if self != A and first_module_method
    end

    def method_added(method_name)
      check_super(method_name)
    end
  end
end

module M
  def a
    puts 'm-a'
  end
end

class B < A
  include M

  def a
    puts 'b-a'
  end
end

B.new.a # m-a

@wppurking 这个。。。确实的暴力,不过我一直都是用暴力解决的,我现在用 A 的 initialize 来实现,这样每次 new 都会 extend,至于 extend 为什么可以用,这个我真不知道,我在 Rails 里可以,直接开 irb 试不可以。。。

现在遇到的问题是:


class A
  def initialize
    # self.send(:extend, module)
  end
end

class B < A

end

class C < B

end

# =========
module AM
  module B

  end

  module C

  end
end

然后我 C.new 时,只能管不到 B 了

看了你的实现我又学到新方法了,B 里的 include M 应该可以直接在 A 里做了,通过 inherited 回调来为 A 的所有继承者来 include 对应的模块,如果模块中有这个方法就移除类里的这个方法

接着试验去。。

@wppurking 这样移除方法,我还得写个判断,只有名字是 Xx::Yyy 下的模块有这个方法我才弄掉这个方法,这样就不怕别的模块来了我也把方法删了。。

绕了一圈,感觉应该差不多了,不过最初的那个如果知道一个类定义完了,这个问题还不知道,不知道有没有回调。。。

或是扩展 ruby 的核心什么的来实现??这个我真不知道了。。

@xjz19901211 :) 不要忘记测试 . 测试很重要。

@wppurking 恩,还好这项目我接手后都写了测试,改完可以运行下测试就知道有问题没,不然改后 BUG 出来又不知道什么情况了,哈哈

@wppurking 这样弄起来发现问题确实会很后,怕以后出了问题,不知道哪里找去。。 现在我的实现


module ProjectExt

  def inherited(cls)
    project_name = get_project_name

    ext_project_module(project_name, cls)

    cls.class_eval do
      def self.method_added(method_name)
        remove_method(method_name) if _included_modules_has_method?(method_name)
      end

      define_singleton_method "_included_modules_has_method?" do |method_name|
        self.included_modules.each do |m|
          next unless m.to_s =~ /^#{project_name}\:\:/

          return true if m.instance_methods(false).include?(method_name)
        end

        return false
      end

    end

    super
  end


  def ext_project_module(project_name, cls)
    return unless project_name && project_name.safe_constantize

    project_controller_name = project_name + "::" + cls.to_s
    project_controller = project_controller_name.safe_constantize

    return unless project_controller && project_controller_name == project_controller.to_s

    cls.send(:include, project_controller)
  end


  def set_project_name(name)
    @project_name = name.classify
  end


  def get_project_name
    @project_name ||= (APP_CONFIG["project"] || "").classify
  end

end

然后,测试

# encoding: utf-8
require 'spec_helper'

module M
  module B

    def a; 'ext-a'; end

  end
end

class A
  extend ProjectExt

  set_project_name("m")
end


class B < A
  def a; 'a'; end

  def b; 'b'; end
end

class C
  def a; 'a'; end
end

module M2
  def a; 'other-a'; end
  def b; 'other-b'; end
  def c; 'other-c'; end 
end

describe ProjectExt do
  it '自动加载扩展模块,使用扩展模块的方法代替掉原有方法' do
    B.new.a.should == "ext-a"
  end

  it '其它模块引入情况' do
    class ::B
      # 因为有M::B,所以这里定义不了
      def a; 'a'; end

      include M2
    end

    # 因为M2在M::B后面,所以这里会先找到M2中的函数
    B.new.a.should == 'other-a'
    B.new.b.should == 'b'
    B.new.c.should == 'other-c'
  end

  it '引入非目标扩展模块不受影响' do
    C.send :include, M2

    C.new.a.should == 'a'
    C.new.b.should == 'other-b'
    C.new.c.should == 'other-c'
  end
end


虽然 问题多,不过暂时在我项目上还不会有影响,先用着

主要就是后面再也不能定义已经被移除的方法了,然后,再来一个 include 相同的方法会覆盖了,不过这样我项目里暂时没什么问题。。 弄好其它的测试还是得多写写了

求更优美的解决方案。。 哈哈

你看看这样写满足你要求吗?

#定义config
CONFIG={"A"=>["M1"],"B"=>["M2"],"C"=>["M3"]}
#module块
module M1
def a
    puts "this is M1 on A"
end
end
module M2
def a
    puts "this is M2 on B"
end
end
module M3
def a
    puts "this is M3 on C"
end
end

#include负责向类(的实例)追加功能,而extend则只向某特定的对象追加模块的功能.
#这里修改父类
class Object
 def self.load_config_module
    CONFIG[self.to_s].each do |module_name|
        eval("extend #{module_name}")
    end if CONFIG && CONFIG[self.to_s]
 end
end
#类
class A
    load_config_module
end

class B < A
    load_config_module
end

class C < A
    load_config_module
end

puts "======A========"
A.a
puts "======B========"
B.a
puts "======C========"
C.a

结果
$ ruby testrb.rb
======A========
this is M1 on A
======B========
this is M2 on B
======C========
this is M3 on C

就是每一个类都需要加一句 load_config_module 如果不加,就不会加载,加载什么就根据你的 config 的配置

@badboy 谢谢你的解答,不过这样还是不能覆盖掉类下面原的方法,如:

class Object
 def self.load_config_module
    CONFIG[self.to_s].each do |module_name|
        eval("include #{module_name}")
    end if CONFIG && CONFIG[self.to_s]
 end
end
#类
class A
    def a
      puts 'a-a'
    end
    load_config_module
end

class B < A
    load_config_module

    def a
      puts 'b-a'
    end
end

A.new.a
B.new.a

A 还是输出 a-a,B 输出了 b-b

感觉是我表述的不清楚。。

我提出的处理方式,不是为了让你破坏子类

而是让你可以根据 config 配置的方式把你的 module 融入你不同的类

我不太赞成那种破坏的方式,嘿嘿

你刚才的代码里

class B < A
    load_config_module

    def a
      puts 'b-a'
    end
end

改成

class B < A
    def a
      puts 'b-a'
    end
    load_config_module
end

那么 module 的会覆盖类里的 a,这样应该也是可以满足你的,不过感觉挺不舒服

@badboy 这样每个类里在结束都要加 load_config_module 这一句,如我标题所示,我想知道怎么知道这个类结束了,然后自动做这个操作。。。。

#定义config
CONFIG={"A"=>["M1"],"B"=>["M2"]}
#module块
module M1
def a
    puts "this is M1 on A"
end
end
module M2
def a
    puts "this is M2 on B"
end
end
module M3
def a
    puts "this is M3 on C"
end
end

#include负责向类(的实例)追加功能,而extend则只向某特定的对象追加模块的功能.
#这里修改父类
class Object
 def self.load_config_module
    CONFIG[self.to_s].each do |module_name|
        eval("extend #{module_name}")
    end if CONFIG && CONFIG[self.to_s]
 end
end
#类
class A
    def self.inherited
       load_config_module
    end
end

class B < A
end

class C < A
end

puts "======A========"
A.a
puts "======B========"
B.a
puts "======C========"
C.a

#结果
$ ruby testrb.rb
======A========
this is M1 on A
======B========
this is M2 on B
======C========
this is M3 on C

@badboy 你真的是转圈圈了。。 请把 extend 改成 include,虽然这个关系不大,最重要的是在 B 和 C 中定义一个方法,然后让模块中的方法来覆盖他

我给你的例子就是啊!

@badboy 你给的是

class B < A
end

class C < A
end

两个类都是空的

好吧。。。我错了。

动态加进去呢?嘿嘿,淫荡就淫荡得彻底一点

#定义config
CONFIG={"A"=>["M1"],"B"=>["M2"],"C"=>["M3"]}
#module块
module M1
def a
    puts "this is M1 on A"
end
end
module M2
def a
    puts "this is M2 on B"
end
end
module M3
def a
    puts "this is M3 on C"
end
end

#include负责向类(的实例)追加功能,而extend则只向某特定的对象追加模块的功能.
#这里修改父类
class Object
 def load_config_module
    CONFIG[self.class.to_s].each do |module_name|
        eval("extend #{module_name}")
    end if CONFIG && CONFIG[self.class.to_s]
 end
end
#类
class A
  def initialize
   load_config_module
  end
end

class B < A
    def a
        puts "fuck b"
    end
end

class C < A
    def a
        puts "fuck c"
    end
end

puts "======A========"
A.new.a
puts "======B========"
B.new.a
puts "======C========"
C.new.a

#结果
$ ruby testrb.rb
======A========
this is M1 on A
======B========
this is M2 on B
======C========
this is M3 on C

不过如果你的业务很复杂,建议你做成 gem 包 或者参考@kenshin54 说的,在 rails 层面上在 config.after_initialize,加载完后再载入你的混入。。。

@badboy 然后这样就和我问题里写的一个实现差不了多少了(请看本贴最后的更新内容),initialize 挂了 而且你这里如果是 C < B 的话,B 的模块不会自动加上来的,嘿嘿

看你这需求蛋疼得狠,呵呵

@badboy 我也怀疑我的需求有问题。。。

不过暂时想不到别的方法来做,只好乱搞了。。

把你那些诡异的东西封装到 gem 里呗,要哪个 gem,装一个

@badboy 我直接写个文件里了,这里东西懒的写 Gem 了,这个不折腾了,我感觉痛苦了。。

应该是我这种做法有问题,就这样算了,以后想到更好的方法再说。。

不太明白楼主的需求,ruby OO 语法也不太熟悉。简单表达下我的理解:

class A
    def a
        p 'called from A'
        self._a
    end
end

class B < A
    def _a
        p 'called from B'
    end
end

B.new.a # called from A, called from B

# change some codes in parent class A
class A
    def a
        p 'called from A'
        p 'add some new modules'
        self._a
    end
end

B.new.a # called from A, add some new modules, called from B

@itsvoid 谢谢你的解答,不过你的解答不符合我的需求

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