Ruby Study 13 : Modules and Mixins

简介:
在Ruby中class的superclass只能有一个, childclass可以有多个. 所以一个class如果要继承多个class的特性是不可能的.
如果有这种需求, 可以考虑把这些特性封装在Module中.  Module可以很好的解决这类问题, 通过require或者load或者include可以把Module的instance method等带入到当前环境, 也就是可以使用Module中的方法等. 一个class可包含多个Module, 接下来详细的讲解.
1. A Module Is like a Class
为什么说Module和Class很相像呢? 
首先是Module是Class的superclass. 如下 :

p Class.superclass
p Module.superclass 
结果
Module
Object


其次, 定义Module时, Module也可以包含常量, Module singleton method, instance method等.

module MyMod1
  NAME = "digoal.zhou"

  class Myclass1

  end
  def method1
    puts("this is MyMod1's instance method.")
  end
end

2. Module methods (Like Class singleton method)
Module 里面可以定义instance method也可以像Class那样定义singleton method. 例如 :
module MyMod1
  def method1  # 定义instance method
    puts("this is MyMod1's instance method.")
  end

  def self.method2  # 定义singleton method
    puts("this is MyMod1's singleton method.")
  end

  def MyMod1.method3  # 定义singleton method
    puts("this is MyMod1's another singleton method.")
  end
end

其中singleton method可以通过如下方法调用 : 
MyMod1.method2
MyMod1.method3
输出
this is MyMod1's singleton method.
this is MyMod1's another singleton method.

但是, 需要注意Module与Class的不同之处, Module不能创建它的object, 但是Class可以创建它的object. 因此Module的instance method不能通过创建对象来调用. 自建的Class有superclass和subclass. 而自建的Module没有superclass和subclass(Module本身是Object的subclass). 如下 :
p MyMod1.class
p Module.superclass
p MyMod1.superclass
输出
Object
Module
C:/Users/digoal/Desktop/new.rb:16:in `<main>': undefined method `superclass' for MyMod1:Module (NoMethodError)


3. Modules as Namespaces
Module 是一系列methods, classes, constants的封装, 在Module内部它们在一个namespace内, 相互可以访问, 但是对Module外部是隔离的.
Ruby的类库里定义了一些Module如, Math, Kernel.
The  Math  module contains module functions for basic trigonometric and transcendental functions. (例如sqrt方法, PI常量)
Kernel则包含了一些我们已经常用的chomp,chop,lambda,puts,print等方法.
详见API.
例如 :
module MyModule
    GOODMOOD = "happy"
    BADMOOD = "grumpy"
   
    def greet
        return "I'm #{GOODMOOD}. How are you?"
    end
    def MyModule.greet
        return "I'm #{BADMOOD}. How are you?"
    end
end

p MyModule.greet
输出
"I'm grumpy. How are you?"

变量的访问范围则和以前讲解的一样.

4. Included Modules, or Mixins
那到底要怎么调用Module 的instance method呢?
mixin, 例如使用include.
例如 :
module MyModule
    x = 1
    GOODMOOD = "happy"
    BADMOOD = "grumpy"
   
    def greet
        return "I'm #{GOODMOOD}. How are you?"
    end
    def MyModule.greet
        return "I'm #{BADMOOD}. How are you?"
    end
end

include MyModule

p greet
p GOODMOOD
p MyModule.greet
p x  # 报错, 因为x 是MyModule这个object的local variable. 不能在外部直接访问, 后面还有个例子可以说明Module的local variable带入到了当前环境, 但是不可被访问. 但是我个人认为Module的local variable不应该带入到当前环境, 即不可见为啥还要带进来.
结果
"I'm happy. How are you?"
"happy"
"I'm grumpy. How are you?"
C:/Users/digoal/Desktop/new.rb:19:in `<main>': undefined local variable or method `x' for main:Object (NameError)

如果在class的定义中include Module, 则可以把它的constants, instance method, class, instance variable, 带入到class中, 就好像这些是写在你定义的class里面一样.
例如
class Myclass1
include MyModule
def m1
  return BADMOOD
end
end
ob1 = Myclass1.new

p ob1.greet
p ob1.m1
结果
"I'm happy. How are you?"
"grumpy"

下面是一个include多个模块的例子 :
module MagicThing
    attr_accessor :power
end
module Treasure
    attr_accessor :value
    attr_accessor :owner 
end
class Weapon
    attr_accessor :deadliness
end
class Sword < Weapon        # descend from Weapon
    include Treasure        # mix in Treasure
    include MagicThing      # mix in MagicThing
    attr_accessor :name
end
s = Sword.new
s.name = "Excalibur"
s.deadliness = "fatal"
s.value = 1000
s.owner = "Gribbit The Dragon"
s.power = "Glows when Orcs appear"
puts(s.name)            #=> Excalibur
puts(s.deadliness)      #=> fatal
puts(s.value)           #=> 1000
puts(s.owner)           #=> Gribbit The Dragon
puts(s.power)           #=> Glows when Orcs appear

下面的例子说明Module的local variable在include是被带入到当前环境, 因为如果没有带进来, no_bar应该返回的是1.
x = 1             # local to this program
module Foo
    x = 50 # local to module Foo
           # this can be mixed in but the variable x won't be visible 
    def no_bar
        return x 
    end
    def bar
         @x = 1000      
         return  @x
    end
    puts( "In Foo: x = #{x}" )   # this can access the module-local x
end
include Foo                     # mix in the Foo module
puts(x)           #=> 1
puts( no_bar )    # Error: undefined local variable or method 'x'
puts(bar)         #=> 1000

下面的例子说明Module object的instane variables不会被带入到当前环境, 而class variables可以带入, 并修改, 如下 :
module X
    @instvar = "X's @instvar"
    @@modvar = "X's @@modvar"
   
    def self.aaa
        puts(@instvar) 
    end

    def self.bbb
        puts(@@modvar)
    end
end
X.aaa   #=> X's @instvar
X.bbb   #=> X's @@modvar
include X
p @instvar   #>= nil
p @@modvar  #=> X's @@modvar
class Myclass1
  include X
  def m1
    p @instvar
  end
  def m2
    p @@modvar
  end
  def m3(newvar)
    @@modvar = newvar
  end
end

ob1 = Myclass1.new
ob1.m1  #=> nil
ob1.m2  #=> "X's @@modvar"
ob1.m3("ob1 modify @@modvar")  # 因为Module object的class可以被include到当前环境, 是可以被修改的.
X.bbb #=> ob1 modify @@modvar

下面的例子可以看出, Module中定义的方法在include到当前环境后, 它就像在当前环境定义的一样, 所以我们看到amethod里面的@insvar实际上是给当前环境的instance variable赋值, 而和Module object的instance variable没有一点关系.
module X
    @instvar = "X's @instvar" 
    @anotherinstvar = "X's 2nd @instvar"
     
        def amethod
             @instvar = 10       # creates @instvar in current scope
             puts(@instvar)
        end      
end
include X
p( @instvar )                    #=> nil
amethod                          #=> 10
puts( @instvar )                 #=> 10
@instvar = "hello world"
puts( @instvar )                 #=> "hello world"
p( X.instance_variables )      #=> [:@instvar, @anotherinstvar]
p( self.instance_variables )   #=> [:@instvar]  # 这也是在执行 amethod 后给self环境创建的一个instance variable.


5. Name Conflicts
如果两个module同时定义了一个同名的方法, 那么如果这两个module都被include了, 则后面的覆盖前面的同名方法, 例如 :
module M1
  def method1
    return "xxx"
  end
end

module M2
  def method1
    return "yyy"
  end
end

class Myclass1
  include M1
  include M2
end

ob1 = Myclass1.new
p ob1.method1
输出
yyy

如果把include的顺序调换一下结果则是xxx
class Myclass1
  include M2
  include M1
end

ob1 = Myclass1.new
p ob1.method1
输出
xxx

换成class variable,  constants 与上面的测试结果一样 .
module M1
  A = "xxx"
  def method1
    return A
  end
end

module M2
  A = "yyy"
  def method1
    return A
  end
end

class Myclass1
  include M2
  include M1
  def m1
    p A
  end
end

ob1 = Myclass1.new
ob1.m1  #=>  "xxx"

include M2
p A   #=>  "yyy"


6. Alias Methods
怎么处理重名的instance method? 可以在module的定义中使用alias. 例如
module Happy
    def Happy.mood
        return "happy"
    end
   
    def expression
        return "smiling"
    end
    alias happyexpression expression
end
module Sad
    def Sad.mood
        return "sad"
    end
   
    def expression
        return "frowning"
    end
    alias sadexpression expression
end
class Person
    include Happy
    include Sad
    attr_accessor :mood
    def initialize
        @mood = Happy.mood
    end
end
p2 = Person.new
puts(p2.mood)                 #=> happy
puts(p2.expression)           #=> frowning
puts(p2.happyexpression)      #=> smiling
puts(p2.sadexpression)        #=> frowning


7. Mix in with Care!
Module中可以include  其他Module, class中也可以include Module, class又可以继承其他superclass, 所以Module的出现使程序的继承关系变得很复杂, 不能滥用, 否则就连DEBUG都会成个头痛的问题. 来看一个例子, 你可能会觉得有点晕.
# This is an example of how NOT to use modules!
module MagicThing                           # module 
    class MagicClass                        # class inside module
    end
end
module Treasure                             # module 
end
module MetalThing
    include MagicThing                      # mixin
    class Attributes < MagicClass           # subclasses class from mixin
    end
end
include MetalThing                          # mixin
class Weapon < MagicClass                   # subclass class from mixin
    class WeaponAttributes < Attributes     # subclass
    end                           
end
class Sword < Weapon                        # subclass       
    include Treasure                        # mixin
    include MagicThing                      # mixin
end


8. Including Modules from Files
前面我们都是用include来加载一个Module的, 这种用法需要定义Module在同一个源码文件里面. 
因为Module一般都是重复利用的, 所以放在其他文件里面会复用性更强, 放在文件中的话我们可以使用require , require_relative , load等来加载.
使用文件来加载就涉及到文件放在什么地方, 如何找到我们要加载的文件的问题了.
例如 : 
require( "./testmod.rb" )
或省略.rb后缀
require( "./testmod" )  # this works too
如果不写绝对路径的话, 那么被加载的文件需要在搜索路径或者$:定义的路径中. $:是一个Array对象, 可以追加
p $:.class
p $:
$: << "."  # 追加当前目录
p $:
输出
Array
["C:/Ruby193/lib/ruby/site_ruby/1.9.1", "C:/Ruby193/lib/ruby/site_ruby/1.9.1/i386-msvcrt", "C:/Ruby193/lib/ruby/site_ruby", "C:/Ruby193/lib/ruby/vendor_ruby/1.9.1", "C:/Ruby193/lib/ruby/vendor_ruby/1.9.1/i386-msvcrt", "C:/Ruby193/lib/ruby/vendor_ruby", "C:/Ruby193/lib/ruby/1.9.1", "C:/Ruby193/lib/ruby/1.9.1/i386-mingw32"]
["C:/Ruby193/lib/ruby/site_ruby/1.9.1", "C:/Ruby193/lib/ruby/site_ruby/1.9.1/i386-msvcrt", "C:/Ruby193/lib/ruby/site_ruby", "C:/Ruby193/lib/ruby/vendor_ruby/1.9.1", "C:/Ruby193/lib/ruby/vendor_ruby/1.9.1/i386-msvcrt", "C:/Ruby193/lib/ruby/vendor_ruby", "C:/Ruby193/lib/ruby/1.9.1", "C:/Ruby193/lib/ruby/1.9.1/i386-mingw32", "."]

1.8和1.9使用require加载文件的区别, 1.8不转换相对路径为绝对路径因此
require "a"
require "./a"
将加载两次.

而1.9 会把相对路径转换成绝对路径, 因此
require "a"
require "./a"
只会加载1次.

require_relative是1.9新增的方法, 用来加载相对路径的文件.
require_relative( "testmod.rb" )    # Ruby 1.9 only
相当于
require("./testmod.rb")

require详解 : 
require(name) → true or false click to toggle source 
Loads the given name, returning true if successful and false if the feature is already loaded. 

If the filename does not resolve to an absolute path, it will be searched for in the directories listed in $LOAD_PATH ($:). 

If the filename has the extension “.rb”, it is loaded as a source file; if the extension is “.so”, “.o”, or “.dll”, or the default shared library extension on the current platform, Ruby loads the shared library as a Ruby extension. Otherwise, Ruby tries adding “.rb”, “.so”, and so on to the name until found. If the file named cannot be found, a LoadError will be raised. 

For Ruby extensions the filename given may use any shared library extension. For example, on Linux the socket extension is “socket.so“ and require 'socket.dll' will load the socket extension. 

The absolute path of the loaded file is added to $LOADED_FEATURES ($"). A file will not be loaded again if its path already appears in $". For example, require 'a'; require './a' will not load a.rb again. 

  require "my-library.rb"
  require "db-driver"

值得注意的是如果文件中定义了Module, 则这些module在使用前需要先执行include. 例如 : 
我有一个./rb.rb文件如下 :
module M1
  def method1
    puts("this is a M1's instance method")
  end
end

def method2
  puts("this is a file's method")
end

另外有一个程序如下 : 
$: << "."
require("./rb.rb")
include M1
method1  # 使用method1前必须先include M1
method2  # 使用method2则在require这个文件后就可以直接使用了. 原因很容易理解.



另外一个要注意的是, 使用require加载文件时, 文件中的代码被逐一执行, 如果有输出的则会输出. 例如前面的rb.rb文件后面添加一行 puts("rb.rb file loaded now.") ,
module M1
  def method1
    puts("this is a M1's instance method")
  end
end

def method2
  puts("this is a file's method")
end
puts("rb.rb file loaded now.")

那么在加载这个文件的时候会有输出
rb.rb file loaded now.

接下来介绍另一种加载文件的方法, load.
load是Kernel模块的方法, 它与require的不同之处, 使用load如果多次加载同一个文件, 它会多次执行. 而使用require多次加载同一个文件它只会加载一次.
例如rb.rb如下
C2 = 100
module M1
  C1 = 10
  def method1
    puts("this is a M1's instance method")
  end
end

def method2
  puts("this is a file's method")
end
puts("C2 is #{C2}.")

使用load加载2次看看发生了什么 : 
$: << "."
load("rb.rb")
load("rb.rb")
输出
C:/Users/digoal/Desktop/rb.rb:1: warning: already initialized constant C2
C:/Users/digoal/Desktop/rb.rb:3: warning: already initialized constant C1
C2 is 100.
C2 is 100.

使用require则只执行了一次 : 
$: << "."
require("rb.rb")
require("rb.rb")
输出
C2 is 100.

另外load 还有一个wrap参数, true或false, 默认是false.
true的情况下, load的这个文件被当成一个匿名module执行, 执行完后不会把文件中的代码带入当前环境. 
false则和require类似, 会把方法, 常量, 类等带入当前环境.
load(filename, wrap=false) → true click to toggle source 
Loads and executes the Ruby program in the file filename. If the filename does not resolve to an absolute path, the file is searched for in the library directories listed in $:. If the optional wrap parameter is true, the loaded script will be executed under an anonymous module, protecting the calling program’s global namespace. In no circumstance will any local variables in the loaded file be propagated to the loading environment. 

例如 : 
rb.rb还是如下,
C2 = 100
module M1
  C1 = 10
  def method1
    puts("this is a M1's instance method")
  end
end

def method2
  puts("this is a file's method")
end
puts("C2 is #{C2}.")

使用load加载这个文件, 分别选用wrap=true和false.
$: << "."
load("rb.rb",true)
# p C2  # 报错
# method2  # 报错
# p M1::C1  # 报错
# include M1  # 报错
输出
C2 is 100.

如果把三个注释去掉报错如下 : 
C:/Users/digoal/Desktop/new.rb:3:in `<main>': uninitialized constant C2 (NameError)

使用false load
$: << "."
load("rb.rb",false)
p C2
method2
p M1::C1
输出
C2 is 100.
100
this is a file's method
10

最后一点load和require的区别, load必须使用文件名全称, 包括rb后缀, 否则无法使用.
$: << "."
load("rb")
报错
C:/Users/digoal/Desktop/new.rb:2:in `load': cannot load such file -- rb (LoadError)
 from C:/Users/digoal/Desktop/new.rb:2:in `<main>'


$: << "."
require("rb")
输出
C2 is 100.


【参考】
The Book Of Ruby
Ruby 1.9.3 API
相关文章
|
移动开发 关系型数据库 Ruby
|
1月前
|
数据采集 Web App开发 数据处理
Ruby网络爬虫教程:从入门到精通下载图片
Ruby网络爬虫教程:从入门到精通下载图片