Ruby里有一种叫module的东西,他和我们比较熟知的Class有很多类似之处,同样是组合一堆常量、变量和方法的单元,但是module不能像类一样被实例化。
Class被定义为Module的子类,但Class不只是module的一个子集,Class的某些独有的特性是module不具备的,比如继承或派生的概念。
扩展阅读:
那Class怎么成为Module子类的?
我们可以使用module去构建某一模组的命名空间,这一模组可能包括一些方法或属性,甚至包括一些类,class其实也构建了命名空间,但是module比class显得更简洁、可控,我们可以打开module去为其添加新的属性、方法或类,但是这种更改是静态或是全局的。
比如,如果我们要写一套用于BASE64编解码的功能模块,因为BASE64是一套既定的规则(算法),我们没必要去定义一个类,然后每次要做BASE64编解码的时候,在内存空间中去一个新建一个对象来使用类中的方法;或者到这里我们想到了在类中定义静态方法来避免新建动态对象,的确,在诸如Java这样的语言中,我们习惯这样做,但是Ruby能为我们提供更纯粹的方式,那就是定义一个module:
module Base64
def self.encode(plain_text)
# do_encoding
end
def self.decode(base64_str)
# do_decoding
end
end
编解码是一个很泛的概念,比如还有URL编码、图片编码、视频编码等等,现在我们定义这个模块,就我们实现的编解码方法划定了一个特定命名空间;有了这个模块,我们就可以从以下方面来灵活应用:
一方面,在想做编解码的地方,可以用模块加半角圆点前缀的方式(如Base64.encode(pain_text))直接调用这个模块所提供的方法;
请注意到,我们在模块方法的定义中都加了self.前缀,如果我们想单独使用这个模块而不是作为其它模块或类定义的一部分,这是必要的,因为module的使用实际上和class相差无几,而module又不能实例化,我们需要将其定义为这个module对象(module也是对象哦!那self就是当前正在定义的这个module)的单例方法(静态方法);当然,如果这个模块里的所有方法都想要有被单独使用的能力,那我们可以通过extend的方式来简化代码的编写,就像下面这样:
module Base64
extend self
def encode(plain_text)
# do_encoding
end
def decode(base64_str)
# do_decoding
end
end
另一方面,如果我们想为一个正在定义中的模块或类增加BASE64的处理能力,那我们就可以通过module的第二个强大的功能来完成这个需求,这个功能就是下面我们提到的混入。
前文提到module没有继承和派生的概念,我们不能用继承关键字去继承一个module,如果要在一个特定Class (ConcreteClass)中去使用另一个已定义的module (ConcreteModule),我们需要使用include的方式将ConcreteModule里定义的东西组装到我们当前所要设计的单元中来,然后ConcreteModule里定义的东西就好像我们直接在ConcreteClass里定义的一样。
# demo_include_module.rb
module ConcreteModule
ConstA = 'constant A defined in ConcreteModule'
def method_a
puts 'method_a defined in module ConcreteModule'
end
end
class ConcreteClass
include ConcreteModule
def method_b
puts 'method_b defined in class ConcreteClass'
end
end
puts ConcreteClass::ConstA
ConcreteClass.new.method_a
ConcreteClass.new.method_b
# demo_include_module.rb
module ConcreteModule
ConstA = 'constant A defined in ConcreteModule'
def method_a
puts 'method_a defined in module ConcreteModule'
end
end
class ConcreteClass
include ConcreteModule
def method_b
puts 'method_b defined in class ConcreteClass'
end
end
puts ConcreteClass::ConstA
ConcreteClass.method_a
ConcreteClass.new.method_a
执行这段Ruby代码:
> ruby demo_include_module.rb
将会得到如下输出:
constant A defined in ConcreteModule
method_a defined in module ConcreteModule
method_b defined in class ConcreteClass
如果我们想让ConcreteModule中的方法变成ConcreteClass的类方法,只需将类名后面的include换为extend来引入ConcreteModule即可。
# demo_extend_module.rb
module ConcreteModule
ConstA = 'constant A defined in ConcreteModule'
def method_a
puts 'method_a defined in module ConcreteModule'
end
end
class ConcreteClass
extend ConcreteModule
def method_b
puts 'method_b defined in class ConcreteClass'
end
end
puts ConcreteModule::ConstA
ConcreteClass.method_a
ConcreteClass.new.method_a
然后执行代码:
> ruby demo_extend_module.rb
constant A defined in ConcreteModule
/xx/yy/demo_extend_module.rb:20:in `' : undefined method `method_a' for # (NoMethodError)
method_a defined in module ConcreteModule
from -e:1:in `load'
from -e:1:in `'
Process finished with exit code 1
可以看到,extend引入的模块里的方法变成了目标类(对象)的单例方法,而且Ruby不允许我们像调用实例方法那样去在一个new出来的对象上调用单例方法。
扩展问题:
- 如果ConcreteModule本来就extend了self,那我们再通过include的方式引入ConcreteModule会有什么效果?
- ConcreteModule中定义的常量会以怎样的方式进入ConcreteClass?
上文提及的include和extend就是module的混入,通过混入,我们可以引入预先定义的各种模块,组成更强大的模块或类,并且能够方便地从模块中获得静态方法和实例方法。
我们在定义一个module时,还可以为其定义钩子方法,钩子方法名以动词的过去分词形式出现,当模块被以不同方式(include, extend)加入其它模块或类时,对应的*ed(过去分词)回调方法将得到调用,有了钩子方法,我们可以去监控定义了钩子方法的模块被其它定义所引用的情况:
# demo_module_included_hook.rb
module ConcreteModuleBase
def self.included(other)
puts "#{other} included #{self}"
end
end
class ConcreteClass
include ConcreteModuleBase
end
# cc = ConcreteClass.new
执行包含以上代码的.rb文件:
>ruby demo_module_included_hook.rb
ConcreteClass included ConcreteModuleBase
通过输出结果我们可以看到,程序侦测到了ConcreteModuleBase被引用的情况,这里已经体现了Ruby动态编程的些许魅力(关于Ruby动态编程,这里暂不讨论)!
除了included, extended, 我们还可以定义inherited, method_added, singleton_method_added等钩子方法来侦测不同行为的发生,真是很nice的一个特性。
以上讨论了module的基本概念,两大用途,以及Ruby的钩子机制怎样去监控一个特定module是怎样被引用或是被扩展的,这些远不是module的全部内涵,比如模块函数(module_function)、作用域(scope),还有module的加载机制,均未涉及。
module的更多机制、特性或是用法技巧,有待更多的讨论和发掘。本次讨论,我们以最后一个扩展问题来结束:
扩展问题:
Ruby中没有专门定义接口(interface),为什么?