モジュールの概要
モジュールの主な用途
継承を使わずにクラスにインスタンスメソッドを追加する、もしくは上書きする(ミックスイン)。
複数のクラスに対して共通の特異メソッド(クラスメソッド)を追加する(ミックスイン)。
クラス名や定数名の衝突を防ぐために名前空間を作る。
関数的メソッドを定義する。
シングルトンオブジェクトのように扱って設定値などを保持する。
モジュールの定義
module モジュール名
# モジュールの定義
end
クラスとは異なる点
- インスタンスを作成することはできない
- 他のモジュールやクラスを継承することはできない
ミックスイン - include
Rubyは単一継承を利用している
コードが重複しているからといって安易に継承を使ったりしてはいけません。「製品はユーザである」または「ユーザは製品である」という関係(isaの関係)が成り立たないのであれば、継承の使用は避けるべきです。
共通の機能は持たせたいが、継承は使うべきでない、そんな時に最適なのがモジュール
モジュールで定義したメソッドが、インスタンスメソッドとして呼び出せるようになります。以下のようにこの機能を扱い、この機能の名前をミックスイン、と言う。
module Loggable
def log(text)
puts "[LOG]#{text}"
end
end
class Product
include Loggable
def title # logメソッドはLoggableモジュールで定義したメソッド
log 'title is called.'
'A great movie'
end
end
class Use
include Loggable
def name # Loggableモジュールのメソッドが使える
log 'nameiscalled.'
'Alice'
end
end
product=Product.new
product.title
# =>[LOG]titleiscalled.
# "A great movie"
user=User.new
user.name
# =>[LOG]name is called.
# "Alice"
インスタンスメソッドをprivate
にしたい場合は、module内でprivate
しておく。
extend
extendを使うと、モジュール内のメソッドをそのクラスの特異メソッド(つまりクラスメソッド)にすることができます。
ミックスインに関してもっと詳しく
includeされているモジュールを確認する
include?(モジュール名)
Product.include?(Loggable)#=>true
include_modules
Product.included_modules#=>[Loggable,Kernel]
ancestors
モジュールだけではなく、スーパークラスの情報も含まれる
Product.ancestors#=>[Product,Loggable,Object,Kernel,BasicObject]
include先に特定のメソッドが定義されている前提のモジュール
これはRubyがダックタイピングという考え方で構成されている為に適用される話。
Productクラスのpriceメソッドと連携して目的の処理を実行する
module Taggable
def price_tag
"#{price}円" # priceメソッドはinclude先で定義されているはず、という前提
end
end
class Product
include Taggable
def price
1000
end
end
product=Product.new
product.price_tag # =>"1000円"
# 呼び出しているのはmodule内のメソッド、ただそのメソッド内でクラスのメソッドを活用している
上記の利用方法を採用したRubyの具体的なモジュール
Enumerableモジュール
Enumerableモジュールは配列やハッシュ、範囲(Range)など、何かしらの繰り返し処理ができるクラスにincludeされているモジュールです。
map
, select
, find
, count
など
Enumerableモジュールをincludeして、モジュールに定義されたメソッドを使えるようにする条件はたった1つだけです。それはinclude先のクラスでeachメソッドが実装されていることです。Enumerableモジュールのメソッドはいずれもinclude先のクラスに実装されたeachメソッドを使います。
Comparableモジュール
Comparableモジュールのメソッドを使えるようにするための条件は、include先のクラスで<=>演算子を実装しておくことです。
<, ≤, ==, >, ≥など
Kernelモジュール
puts, p , print, require, loopなどについて
これらの当たり前に利用していたメソッドが何故これまで利用できていたか?
それはKernelモジュールをObjectクラスがincludeしているから。
トップレベルでは、mainという名前のオブジェクト
Rubyのirbを起動した直後、
またclassやmoduleの定義の外側、これらの場所はどういう扱い?
$ irb
irb(main):001:0>ここはどこ?私は誰?
================================================================================
# ここはどこ?誰?
class User
# Userクラス内
end
これらの一番外側の部分をトップレベルという。
トップレベルにはmainという名前のObjectクラスのインスタンスがselfとして存在しています。つまり、「私は誰?」の答えは「Objectクラスのインスタンス」になります。
モジュール内での、インスタンス変数の扱い
モジュール内で定義したメソッドの中でインスタンス変数を読み書きすると、include先のクラスのインスタンス変数を読み書きしたことと同じになります。
ただし、モジュールとミックスイン先のクラスでインスタンス変数を共有するのはあまり良い設計ではありません。なぜなら、インスタンス変数は未定義の状態でも自由に代入したり参照したりできるからです。
一方、メソッドであれば未定義のメソッドを呼び出したときにエラーが発生します。
なので、ミックスイン先のクラスと連携する場合は特定のインスタンス変数の存在を前提とするより、特定のメソッドの存在を前提とするほうが安全です。
モジュールを利用した名前空間の作成
名前空間の衝突を防ぐ
モジュール構文の中にクラス定義を書くと「そのモジュールに属するクラス」という意味になる
module Baseball
class Second
def initialize(player, uniform_number)
@player = player
@uniform_number = uniform_number
end
end
end
クラス名の衝突を防止する名前空間として使われています。モジュールに属するクラスを参照する際は“モジュール名::クラス名”のように、::でモジュール名とクラス名を区切ります
Baseball::Second.new('Alice', 13)
名前空間でグループやカテゴリを分ける
クラスが何十、何百もあるような大きなプログラムになってくると、カテゴリ別にモジュール(名前空間)を作って整理しないと、どれが何のクラスなのかぱっと把握しにくくなるためです。
ネストなしで名前空間付きクラスを定義する
- 名前空間として利用するモジュールを定義する
- クラスの定義の際の構文を変更する
# moduleを定意義しておく
module Baseball
end
# 名前空間を定義
class Baseall::Second
end
モジュールを単体で使う
特異メソッドを定義する
わざわざ他のメソッドに組み込むことなく、直接モジュール名.メソッド名
とする事で、メソッドを呼び出す事ができる。
ただし、モジュールではインスタンスが作成できない為、「単なる関数の集まり」を作りたいケースに向いている。
また、クラスと同様に定数を定義することもできる。
module_function
単体の特異メソッドとしての利用、かつモジュールをミックスインとしても利用できるようにするメソッド。
module Loggable
def text
# 処理
end
module_function :log # どちらでも利用可能に
end
状態を保持する為に、モジュール自身に設定値を持たせる
クラスインスタンス変数を使って、クラス自身にデータを保持する方法を説明しました。この方法はモジュールでも使うことができます。外部ライブラリ(gem)では、そのライブラリを実行するための設定値(config値)をモジュール自身に保持させたりすることがよくあります。
# AwesomeApiという外部ライブラリ
AwesomeApi.base_url # => "http://example.com"
AwesomeApi.debug_mode # => false
インスタンスを作って何か操作する必要がないのであれば、モジュールにしておいたほうがほかの開発者に変な勘違いをさせる心配がありません。
ところで、ライブラリの実行に必要な設定値などはアプリケーション全体で共通の値になることが多いです。
そのため、アプリケーション内ではいつでもどこでも同じ設定値を設定したり、取得したりする必要があります。
そのためには設定値の情報はアプリケーション内で「唯一、1つだけ」の状態になっていることが望ましく、「唯一、1つだけ」のオブジェクトを作る手法のことを、シングルトンパターンと呼びます。
その他モジュールについて
1.メソッド探索のルール
同名のメソッドを扱う際に、どのモジュールやクラス内のメソッドを参照しているか調べる方法が記載されている
2.moduleにmoduleをinclude
3.prepend
prependの特徴は同名のメソッドがあったときに、ミックスインしたクラスよりも先にモジュールのメソッドが呼ばれるところです。
この活用方法は、既存のメソッドを置き換える際に活用することができる。具体例を見た方が早い・再読必要
4.refinements - 有効範囲を限定
moduleとclassに跨る形で、それぞれrefinements
, using
を利用することで、module内のメソッドの利用可能範囲を限定する事ができる。具体例を見た方が早い・再読必要
例題 - deep_freezeメソッドの作成
ここでは基本的なモジュールの理解として、複数のclass内における共通のメソッドの利用、について学んだ。
require 'minitest/autorun'
require '../lib/team.rb'
require '../lib/bank.rb'
class DeepFreezableTest < Minitest::Test
def test_deep_freeze_to_array
assert_equal ['Japan', 'US', 'India'], Team::COUNTRIES
assert Team::COUNTRIES.frozen?
assert Team::COUNTRIES.all? { |country| country.frozen? }
end
def test_deep_freeze_to_hash
assert_equal(
{'Japan' => 'yen', 'US' => 'doller', 'India' => 'rupee'},
Bank::CURRENCIES,
)
assert Bank::CURRENCIES.frozen?
assert Bank::CURRENCIES.all? { |key, value| key.frozen? && value.frozen? }
end
end
module DeepFreezable
def deep_freeze(array_or_hash)
case array_or_hash
when Array
array_or_hash.each do |element|
element.freeze
end
when Hash
array_or_hash.each do |key, value|
key.freeze
value.freeze
end
end
array_or_hash.freeze
end
end
require '../lib/deep_freezable.rb'
class Team
extend DeepFreezable
COUNTRIES = deep_freeze(['Japan', 'US', 'India'])
end
require '../lib/deep_freezable.rb'
class Bank
extend DeepFreezable
CURRENCIES = deep_freeze({'Japan' => 'yen', 'US' => 'doller', 'India' => 'rupee'})
end