Day14. Module & #extend #prepend #include - Ruby 继承 part1

Day14-15 一共会介绍 Ruby的2类、4种继承方式。

Day2 我们提到 Ruby 为单一继承的语言,若想要实现多重继承,可以使用mixin的方式达到相似的效果。

class Warrior
  include Sword
  include Shield
end

如上程序码所示,上方的Warrior使用了 include 继承了Sword, Shield里面所有的方法。除了include以外,还有prepend, extend两种方式可以继承。使用module继承的方式我们称作 mixin

以下为今天会介绍的内容

  • module
  • 实体方法继承
  • 类别方法继承
  • 继承类别/实体方法
  • 总结

module & namespace

Module 的其中一个功能可以做为路径NameSpace,我们用2个例子说明

#=========== 例1 ===========#
class Admin::Order
end

# 可以写成以下形式
module Admin
  class Order
  end
end

#=========== 例2 ===========#
class Admin::Order < Admin::Base
end

# 可以写成以下形式
module Admin
  class Order < Base
  end
end

Admin::Order  #=> Admin::Order

当我们搜寻Admin::Order,能够找到刚刚宣告的类别,而:: 为路径的概念。为了说明::的使用方法,我们以更复杂的例子来说明

module Taipei
  class Car
    TITLE = '北部车款介绍'
    
    # 厂牌: 在这里定义常数
    module Brand
      FORD = 0
      TESLA = 1
    end
    
    def self.drive
      'boo boo'
    end
  end
end

Taipei::Car::TITLE         #=> "北部车款介绍"
Taipei::Car::Brand::TESLA  #=> 1
Taipei::Car.drive          #=> "boo boo"

接着,我们开始讲module层级的继承

include ➡️ 继承实体方法

include是最被广泛使用,也是最简单的继承方式,以下我们先说明include的用法。

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_name
    [material.capitalize, 'Sword'].join(' ')
  end
end

class Warrior
  include Sword
end

warrior = Warrior.new
warrior.sword_name        #=> "Silver Sword"

warrior.material = 'Metal'
warrior.sword_name         #=> "Metal Sword"

战士的剑预设为银剑,透过setter将材质改为铁,变为铁剑。

关於 mattr_accessor 的用法可以参考Ruby文件,在这边我们就把它当作 Day12 提到的attr_accessor ,我们在Day13提到,实体方法不一定要在initialize里面宣告,这边的实体变数定义在module里面,预设为silver

在我其中一个Rails专案中,有很多个controllerindex的行为都是呈现 DataTable 的组成表单。因此我把共同的行为都写在module DataTable,待需要时就include 进来,就不用每使用一个Datatable就写那麽多程序码。

接着我们来探讨,include的覆写与继承问题。下方的例子为在class定义了sword_name覆写了原本的sword_name,这种覆写的行为不管在使用继承还是mixin都称为monkey patch

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_name
    [material.capitalize, 'Sword'].join(' ')
  end
end

class Warrior
  include Sword
  
  def sword_name
    '倚天剑'
  end 
end

warrior = Warrior.new
warrior.sword_name       #=> "倚天剑" 

除了覆写以外,我们能不能保留原本的行为?使用include的答案是不行,但我们可以透过其他方式调用!

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_meterial
    meterial
  end
end

class Warrior
  include Sword
  
  def sword_name
    [sword_meterial.capitalize, '剑'].join(' ')
  end 
end

warrior = Warrior.new
warrior.material = '长'
warrior.sword_name       #=> "长剑"

使用include我们无法保持原本的行为,但透过接下来我们要介绍的prepend,可以使用#super 来达到我们要的目的。

prepend ➡️ 继承实体方法

prependinclude一样,我们可以透过prepend继承实体方法,不过prepend却有一些用法使得可以和include区隔。

prepend的词意为前面,在继承链中代表着会覆盖原本class的方法。首先,我们来看prepend是如何覆盖原本的类别方法

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_name
    [material.capitalize, 'Sword'].join(' ')
  end
end

class Warrior
  prepend Sword
  
  def sword_name
     '倚天剑'
  end 
end

warrior = Warrior.new
warrior.sword_name       #=> "Silver Sword"

除覆盖以外,我们提到可以用关键字#super来保留原本的用法。下列为使用 #super的执行结果

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_name
    puts %Q(#========= #{[material.capitalize, 'Sword'].join(' ')} =========#)
    super
  end
end

class Warrior
  prepend Sword
  
  def sword_name
     '倚天剑'
  end 
end

warrior = Warrior.new
warrior.sword_name       

#== 先印:   #========= Silver Sword =========#
#== 再回传: => "Silver Sword"

这种方式很像是controller 常使用的 before_action 行为,反之如果我们改成以下写法,就类似after_action

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_name
    super
    puts %Q(#========= #{[material.capitalize, 'Sword'].join(' ')} =========#)
  end
end

extend ➡️ 继承类别方法

当我们使用extend,就可以继承类别方法。

module Sword
  mattr_accessor :material, default: :silver
  
  def sword_name
    [material.capitalize, 'Sword'].join(' ')
  end
end

class Warrior
  extend Sword
end

Warrior.sword_name  #=> "Silver Sword"

include & extend 的组合技 ➡️ 继承类别/实体方法

我们可以透过 #included的方法,一并使用类别方法和实体方法。

module Notification::Helper
  # 实体方法写在这里
  def self.included(base)
    puts "MyModule is included to #{base}"
    
    base.extend(ClassMethods)
    
    # 等同於 def foo; 'foo' end
    define_method :foo, -> {'foo'}
    
    #=> add instance method
    subclass.class_eval do
    end
    
    #=> add class method
    subclass.instance_eval do
    end
  end

  # 类别方法写在这里
  module ClassMethods
    def call(params = {})
      new(params).call
    end
    
     # 等同於 def bar; 'bar' end
    define_method :bar, -> {'bar'}
  end
  
  def call
    {
      title: @title,
      content: @content
    }
  end
end

class Noti
  include Notification::Helper
  
  def initialize(**argv)
    @title, @content = argv[:title], argv[:content]
  end
end

#==            # MyModule is included to Noti
Noti.bar       #=> bar
Noti.new.foo   #=> foo
Noti.call(title: '我是标题', content: '我是内文')  #=> {:title=>"我是标题", :content=>"我是内文"}

包括#included,一共有以下这些hook可以使用。我们今天讲了前三个,明天会讲最後一个

  • Module#included
  • Module#extended
  • Module#prepended
  • Class#inherited

ancestors

若想要看截至为止的继承链,我们可以使用ancestors

Noti.ancestors
#=> [Noti, Notification::Helper, Object, Kernel, BasicObject]

ancestors 意思为祖先,如果有看幽游白书的读者们,可以知道雷禅是幽助的16代前的祖先。长相如下 ⬇️

幽助.ancestors
#=> [幽助, ..., ..., ..., ..., ..., ..., ..., ..., ..., ..., ..., 雷禅]

在继承链里面的方法撞名时,ancestors 可以帮助我们判断,最後是哪个方法覆写了其他同名的方法。假设Noti, Notification::Helper 都有方法foo,则Noti会覆盖Notification::Helperfoo

module A
end

module B
end

class Wanc
  include A
  include B
end

class Gang
  prepend A
  prepend B
end

Wanc.ancestors #=> [Wanc, B, A, Object, Kernel, BasicObject]
Gang.ancestors #=> [B, A, Gang, Object, Kernel, BasicObject]

搭配include, prepend的用法搭配ancestors查看,可以看到先後顺序不一样。

总结

以下总结class, module之间用法的差别。

Class Module
可初始化
用法 创建物件 namespace & mixin 继承
继承 可以 不行
可用的hook #inherited #included, #extended, #prepended

明天开始讲 Ruby 的继承!

参考资料


<<:  Day 01-AWS Solution Architect Associate的铁人之旅行前会

>>:  Day 14 Azure cognitive service: Text-to-Speech- Azure 念给你听

Day5. 及早失败,从中学习- 低精度原型

试想当你有了一个绝佳的 idea,如何验证这个概念可不可行、够不够好呢?设计师常用的方法,是制做一个...

卡夫卡的藏书阁【Book4】- Kafka 主题、偏移量、分区

“There is an infinite amount of hope in the unive...

Day 22 - Rancher Fleet 架构介绍

本文将於赛後同步刊登於笔者部落格 有兴趣学习更多 Kubernetes/DevOps/Linux 相...

Re-architect - Domain Layer (二)

上一次介绍完了介面,今天就要来说说实作的部分了,从这里开始我要采取一种“小步快跑”的方式,原本 Ed...

Day 26 Ruby Symbol

在 Ruby 内有符号(Symbol)这个物件,他跟字串的用法蛮像的,但本质上则不一样。 究竟 Sy...