0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

モジュール [Rubyチェリー本 8章まとめ]

Posted at

モジュールの概要

モジュールの主な用途

継承を使わずにクラスにインスタンスメソッドを追加する、もしくは上書きする(ミックスイン)。
複数のクラスに対して共通の特異メソッド(クラスメソッド)を追加する(ミックスイン)。
クラス名や定数名の衝突を防ぐために名前空間を作る
関数的メソッドを定義する。
シングルトンオブジェクトのように扱って設定値などを保持する。

モジュールの定義

module モジュール名
 # モジュールの定義
end

クラスとは異なる点

  1. インスタンスを作成することはできない
  2. 他のモジュールやクラスを継承することはできない

ミックスイン - 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)

名前空間でグループやカテゴリを分ける

クラスが何十、何百もあるような大きなプログラムになってくると、カテゴリ別にモジュール(名前空間)を作って整理しないと、どれが何のクラスなのかぱっと把握しにくくなるためです。

ネストなしで名前空間付きクラスを定義する

  1. 名前空間として利用するモジュールを定義する
  2. クラスの定義の際の構文を変更する
# 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内における共通のメソッドの利用、について学んだ。

test/deep_freezable_test.rb
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
lib/deep_freezable.rb
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
lib/team.rb
require '../lib/deep_freezable.rb'

class Team
  extend DeepFreezable

  COUNTRIES = deep_freeze(['Japan', 'US', 'India'])
end
lib/bank.rb
require '../lib/deep_freezable.rb'

class Bank
  extend DeepFreezable

  CURRENCIES = deep_freeze({'Japan' => 'yen', 'US' => 'doller', 'India' => 'rupee'})
end
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?