Module引入

类引入

ruby 中module用于替代多继承的功能,是一个可以包含常量、方法、类定义以及其他模块的集合。

module除了不可以new实例,以及没有Class类中一些特殊的方法和class没什么区别。

其定义语法如下:

module ModuleA
  constA

  def method1
    ...do something
  end
end

Mix-in 可以将多个模块混合到类中,即通过include 在类中引入模块,以获得模块的方法、常量等。

class ClassA
  include ModuleA
  include ModuleB
  ...
end

class ClassB
  include ModuleA
  include ModuleB
  ...
end

每个模块具有单独的命名空间,以保证模块下的常量和方法互不影响。

Const

在类中引入模块后,其中的常量都被引入到了所在类中,当然也可以直接通过模型的命名空间直接访问模型的常量。

module ModuleA
  constA = 'a'
end

class ClassA
  p ModuleA::constA # => "a"
  
  include ModuleA # 引入模块
  p constA # => "a" # 直接在类中使用模块中的常量
end

Method

在类中引入模块后,其中的方法可以被该类的实例调用。

同时也可以在模块中将方法定义为模块方法。

module ModuleA
  def method1
		p "do method1.."
  end
  
  def method2
    p "do method2.."
  end
  
  module_function :method2 # 将方法2设置为模型方法
end 

class ClassA
  include ModuleA
  # 可以直接通过模型的命名空间调用模型方法
  ModuleA.method2 # => "do method2.." 
  a = ClassA.new
  a.method1 # => "do method1.."
end

NameSpace

在引入多个模块时如果常量出现同名问题可以指定具体的命名空间以使用正确的常量。

否则 ruby 将使用后引入的常量,且多次引入不会重新引入。

且在模块中的非模块方法(可以直接调用的,模块的自方法)同名时,引入模块的类实例没法精准指定具体方法,所以一定要注意。

module C
    Name = 'lisi'

    def hello(name)
        p "#{Name} : hello," << name
    end

    # module_function :hello
end 

module B
    Name = 'zhangsan'

    def self.hello(name)
        p "#{Name} : hello," << name
    end

    # module_function :hello
end 


class A
    include C
    include B
    
    p Name # => "zhangsan"
    B.hello "wangwu" # => "zhangsan : hello,wangwu"
    # C.hello "wangwu" ## => 报NoMethodError (undefined method `hello' for C:Module)
    a = A.new
    a.hello "wangwu" # => "lisi : hello,wangwu" 因为在moduleB 中hello为自方法
end

include & extend

  • include添加一个模块的方法到实例中

  • extend添加一个模块的方法到类中

module Log 
  def class_type
    "This class is of type: #{self.class}"
  end
end
class TestClass 
  include Log 
end

tc = TestClass.new.class_type
puts tc #This class is of type: TestClass
class TestClass
  extend Log
  # ...
end

tc = TestClass.class_type
puts tc  # This class is of type: TestClass

ruby中使用include也可以增加实例方法,因为include有一个self.included的钩子函数,可以用它来修改类中对于模块的引入。

module Foo
  def self.included(base)
    base.extend(ClassMethods)
  end
  
  def foo
    puts 'instance method'
  end

  module ClassMethods
    def bar
      puts 'class method'
    end
  end
end

class Baz
  include Foo
end

Baz.bar # class method
Baz.new.foo # instance method
Baz.foo # NoMethodError: undefined method ‘foo’ for Baz:Class
Baz.new.bar # NoMethodError: undefined method ‘bar’ for #<Baz:0x1e3d4>

extend也有一个叫self.extended的方法,作用和include中的self.included类似。

同时included方法可以用作文件夹在时的一些初始化操作:

module A 
    def A.included(mod)       
      puts "#{self} included in #{mod}"     
    end   
end   
module Enumerable     
  include A   
end
# => prints "A included in Enumerable"

尤其值得注意的是,self其实就是调用的该类的一个单例类,可以从下面的例子看到:

car = "car"

class << car
  def f1; puts "f1"; end
  def self.f2; puts "f2"; end
  
  class << self 
    def f3; puts "f3"; end
    def self.f4; puts "f4"; end
  end
end

car.f1									# => f1
car.singleton_class.f2	# => f2
car.singleton_class.f3	# => f3
car.singleton_class.singleton_class.f4	# => f4

参考资料:《Ruby中include和extend的比较》

module引入

不同于类,module本身不能实例对象,所以module通过extend引入别的module时,其中打方法会被添加到自身的metaclass中,并且可以通过module名直接调用:

module ExM
  def hello_exm
    puts 'hello exm'
  end
end

module InM
  def hello_inm
    puts 'hello inm'
  end
end
module Base
  include InM;
  extend ExM;
end

Base.hello_exm
# => hello exm
Base.hello_inm
# => NoMethodError (undefined method `hello_inm' for Base:Module)

Base通过include引入到方法则会在Base被其他class通过include引入时添加到class到实例方法中:

class KLASS_IN
  include Base;
end

KLASS_IN.hello_inm
# => NoMethodError (undefined method `hello_inm' for KLASS_IN:Class)
KLASS_IN.new.hello_inm
# => hello inm

此时在KLASS_INself方法和实例的方法中都不能找到hello_exm方法:

KLASS_IN.hello_exm
# => NoMethodError (undefined method `hello_exm' for KLASS_IN:Class)
KLASS_IN.new.hello_exm
# => NoMethodError (undefined method `hello_exm' for #<KLASS_IN:0x00007fbebd098610>)

可以先看一个例子:

module SelfM
  def self.hello_self
    puts "hello myself"
  end
end

module Base
  include SelfM;
  extend SelfM;
end

Base.hello_self
# => NoMethodError (undefined method `hello_self' for Base:Module)

可以看到在Base中没有找到SelfM中的方法。self的方法其实存放在metaclass中。而ruby中的引入不会将metaclass中的方法引入。

所以在上面的例子中ExM中的方法已经被添加到了Basemetaclass中,所以ExM中的方法在KLASS_INmetaclass和实例中都找不到对应方法。


如果Baseextend到其他到类,同样ExM中的方法不会被引入到该类中,而InM中到方法会被添加到该类的metaclass中:

class KLASS_EX
  extend Base;
end

KLASS_EX.hello_inm
# => hello inm
KLASS_EX.hello_exm
# => NoMethodError (undefined method `hello_exm' for KLASS_EX:Class)

还有一点值得注意的是,Module类中提供了一个名为module_function(*args)的方法。其可以将指定的方法添加到当前的module中,并且同时在该module被其他类引入时,将方法添加到类的实例中。

 module_function :mehtod_name

在缺省情况下,module_function调用之后会作用于其后添加的所有方法,类似private

Rails引入

concern

Rails中的ActiveSupport::Concern通过其中的append_features(base)方法:

def append_features(base)
  if base.instance_variable_defined?(:@_dependencies)
    base.instance_variable_get(:@_dependencies) << self
    return false
  else
    return false if base < self
    @_dependencies.each { |dep| base.include(dep) }
    super
    # look here!
    base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
    base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
  end
end

使得引入了ActiveSupport::Concernmodule可以在其被其他类include引入时,将其中名为ClassMethodsmodule中的方法添加到对应类的metaclass中。

module InM
  def hello_inm
    puts 'hello inm'
  end
end

module BaseModel
  extend ActiveSupport::Concern
  include InM
  
  def hello_base
    puts 'hello base module'
  end
  
  module ClassMethods
  	def hello_record
      puts 'hello record'
    end
  end
end

class Re < ApplicationRecord
    include BaseModel
end

Re.hello_record
# => hello record

而其中的其他方法,或者在该module中通过include引入的其他module中的方法可以被引入到对应类的实例中。

Re.new.hello_base
# => hello base module
Re.new.hello_inm
# => hello inm

autoload_path

通过命令行调用rails runner 'puts ActiveSupport::Dependencies.autoload_paths'可以看到Rails配置的autoload_paths

$ bin/rails runner 'puts ActiveSupport::Dependencies.autoload_paths'
...
/Users/boohee/works/ruby/polestar/app/controllers
/Users/boohee/works/ruby/polestar/app/controllers/concerns
/Users/boohee/works/ruby/polestar/app/jobs
/Users/boohee/works/ruby/polestar/app/mailers
/Users/boohee/works/ruby/polestar/app/models
/Users/boohee/works/ruby/polestar/app/models/concerns
/Users/boohee/works/ruby/polestar/app/services
...

这些路径下的module将会被Rails自动加载,这使得我们在使用对应的module时不用添加表示文件结构的命名空间

例如不使用autoload_path的情况下,在app/modules下添加共用的module:

.
└── commons
    └── soft_delete.rb

soft_delete.rb文件中module,必须添加命名空间Commons

module Commons::SoftDelete
  extend ActiveSupport::Concern

  def delete
    update(deleted_at: Time.now)
  end

  def delete!
    update!(deleted_at: Time.now)
  end
end

在其他Record引入时也需要添加命名空间Commons

class User < ApplicationRecord
  include Common::SoftDelete
end

我们可以在项目根目录的application.rb中指定Rails自动将app/modules/commons加载,这使得我们可以省去在引入时使用命名空间:

config.autoload_paths += Dir["#{config.root}/app/models/[a-z]*s/"] +
  Dir[Rails.root.join("app/workers")]

app/modules/commons/soft_delete.rb文件的module修改为module SoftDeleteRecord的引入则可以修改为:

class User < ApplicationRecord
  include SoftDelete
end

注:autoload_paths 在初始化过程中计算并缓存。目录结构发生变化时,要重启服务器。spring可能会缓存autoload_paths,即使是重启了服务,修改目录后需要暂时关闭spring

最后更新于