Modules and Mixins

文章出處: http://blog.163.com/digoal@126/blog/static/163877040201222712214285/

在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 programs 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.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章