>> 連載の目次は こちら!
クラスの話の続き。
ここからは、Rubyにおけるクラスの機能拡張の方法を整理してみたいと思う。
前々回に継承の話をしたが、Rubyの特徴として、継承以外にも様々なクラスの拡張方法が用意されている。
それらの話のひとつめとして、まずはモジュールについて整理してみる。
■ モジュールの概要
- ある種の機能を実現するための一連のメソッドを提供する、まさにモジュール
- クラスに似ているが、インスタンス化できない、変数は持たない(定数は持てる)、などの特徴がある
- クラスに include/extend(後述)することで、モジュールの持つ機能(メソッド群)を、クラスに取り込むことができる
- 複数のモジュールを include/extend できるので、多重継承的な仕組みを実現できる
- クラスに取り込まずに、モジュール.メソッドで実行したり、関数のように実行したりする方法もある
- そのほか、クラスをカテゴライズするためにも利用できる(名前空間の提供)
■ 基本的な定義方法と、クラスへの取り込み方法
module Drummer
def take_rhythm
puts "DoDoDan!!"
end
end
module Bassist
def slap_on_bass
puts "ButchBatch!!"
end
end
module Guitarist
def play_emotional
puts "GaGaGaa!!"
end
end
module Vocalist
def sing
puts "LaLaLa♬"
end
end
class Band
# include でインスタンスメソッドとして取り込む(Mix-in)
include Guitarist
include Vocalist
# extend でクラスメソッドとして取り込む
extend Drummer
extend Bassist
def play
# インスタンスメソッドとして取り込んだメソッドをクラスの中で実行
play_emotional
sing
end
def groove
# クラスメソッドとして取り込んだメソッドをクラスの中で実行
Band.take_rhythm
Band.slap_on_bass
end
end
# モジュールを include/extend したクラスの中で、モジュールのメソッドを実行してみる
band = Band.new
band.groove
# DoDoDan!!
# ButchBatch!!
band.play
# GaGaGaa!!
# LaLaLa♬
# モジュールを include/extend したクラスや、そのインスタンスから、モジュールのメソッドを実行してみる
Band.take_rhythm # entend で取り込んだメソッド(クラスメソッド)なので、BandのClassオブジェクトから実行
# DoDoDan!!
band.sing # include で取り込んだメソッド(インスタンスメソッド)なので、Bandのインスタンスから実行
# LaLaLa♬
■ 同名のメソッドがあった場合の優先順位
- モジュールのメソッドよりクラスで定義されているメソッドが優先
- モジュール同士で同名のメソッドがあったら、あとから取り込んだメソッドが優先
- インスタンスメソッドとクラスメソッドなら、所属先が違うので重複しても大丈夫
module M1
def same1; puts "M1" end
def same2; puts "M1" end
def same3; puts "M1" end
def same4; puts "M1" end
end
module M2
def same2; puts "M2" end
def same3; puts "M2" end
def same4; puts "M2" end
end
module M3
def same3; puts "M3" end
def same4; puts "M3" end
end
class Cls
include M1
include M2
extend M3
def same4; puts "Cls" end
end
Cls.new.same1 # M1 M1にしか定義されていない
Cls.new.same2 # M2 M2があとからincludeされている
Cls.new.same3 # M2 インスタンスメソッドとしてはM2が最後にincludeされている
Cls.same3 # M3 クラスメソッドとしてはM3が最後にextendされている
Cls.new.same4 # Cls インスタンスメソッドとしてはClsで定義されている
Cls.same4 # M3 クラスメソッドとしてはM3が最後にextendされている
■ モジュールのメソッドを直接実行する
クラスに取り込まなくても、モジュールから直接メソッドを実行したり、関数のように実行したりすることもできる
module GuitarShop
def repair
puts "brush frets!!"
end
def purchase
puts "please sell it to me!!"
end
# repair メソッドだけ、「モジュール関数」として提供する
module_function :repair
end
class MyShop
include GuitarShop
def refresh
repair
puts "brush body!!"
end
end
# モジュール名を明示してメソッドを実行
GuitarShop.repair # これは module_function で モジュール関数として提供されているからOK
GuitarShop.purchase # これはNG! モジュール関数として提供されていない。
# モジュールをincludeし、関数のように実行する
include GuitarShop
repair # このやり方なら、
purchase # どちらのメソッドも実行できる(モジュール関数として提供されていなくても大丈夫)
# モジュールをincludeしたクラスから実行してみる
myShop = MyShop.new
myShop.refresh # これは、モジュール関数になっていないのでOK
myShop.repair # これはNG! モジュール関数に指定されているメソッドは private になるので、インスタンスから呼べない
■ モジュールをクラスの名前空間として利用する
同名のクラスを定義すると、Rubyだと既存クラスの定義が変更されてしまう
名前空間を使って、クラスをカテゴライズすれば安心。
module Http
class Input
def read; puts "from request" end
end
class Output
def write; puts "to browser" end
end
end
module Cli
class Input
def read; puts "from shell" end
end
class Output
def write; puts "to shell" end
end
end
# 名前空間内のクラスは、モジュール名::クラス名 でアクセスする
Http::Input.new.read # from request
Cli::Output.new.write # to shell
■ Rubyの既存モジュールの利用例
● Comparable モジュールで大小比較可能なクラスを定義する
class Bass
# Comparable モジュールを搭載
# Bassオブジェクトは大小を比較することができるよー
include Comparable
attr_reader :scale
def initialize(scale)
@scale = scale
end
# <=>(other) メソッドの実装が必須
# Bassオブジェクトに比較系のメソッド( > とかの比較演算子)が指定されたら、このメソッドが呼ばれるよー
def <=>(other)
# このクラスではネックのスケール長で大小を比較することにするよー
@scale <=> other.scale
end
end
mustang = Bass.new(30) # ムスタングってショートスケールで可愛いよねー
jazzbass = Bass.new(34) # 王道ジャズベはやっぱりロングスケールっしょ
p (jazzbass > mustang) # true ムスタングよりジャズベのがおっきいな
● Enumerable モジュールで繰り返し処理が可能なクラスを定義する
- 配列にあるような繰り返し系の処理(each_with_index とか map とか include? とか、配列の章で紹介したようなメソッド群)を実行できるようになる
- 繰り返し系の処理は、すべて each メソッド経由で呼ばれるため、eachメソッドの実装が必須。
class JukeBox
# Comparable モジュールを搭載
# JukeBoxオブジェクトは繰り返し処理ができるよー
include Enumerable
def initialize(songs)
@songs = songs
end
# eachメソッドの実装が必須
# each_with_index とか map とか、繰り返し系の処理はこのメソッド経由で実行される
def each
# このクラスでは、ジュークボックスが保持している歌たちをループすることにするよー
@songs.each do |item|
yield(item) # ここで、each_with_index とか all とか map とかを呼んでいる
end
end
end
jb = JukeBox.new(["whats going on", "dock of the bay", "i want you back"])
# JukeBox のオブジェクトから、配列で使うような繰り返し系の処理を実行できるようになる
p jb.include?("dock of the bay") # true
jb.map { |item| puts "sing " + item }
# sing whats going on
# sing dock of the bay
# sing i want you back
jb.each_with_index do |item, i|
puts "#{i+1}. #{item}"
end
# 1. whats going on
# 2. dock of the bay
# 3. i want you back