Ruby学習 「プロを目指す人のためのRuby入門」 8章

はじめに

プロを目指す人のためのRuby入門Rubyを学習しています。
大事そうな部分や理解が難しかった部分、忘れそうなことについて学習メモを書きます。
今回は第8章「モジュールを理解する」について書いていきます。

記事中のプログラムはirbで動作を確認しました。

モジュールの概要

  • モジュールは下記のように定義する
module TestModule
end
  • モジュールからインスタンス作成はできない
  • モジュールやクラスを継承できない

モジュールのミックスイン(includeとextend)

  • クラスでモジュールをincludeすると、モジュールで定義したメソッドを使用できる(これをミックスインという)
  • モジュールのメソッドがpublicメソッドの場合、include先でもpublicメソッドとなるので必要がなければprivateとして定義しておく
  • モジュールをextendでミックスインするとモジュール内のメソッドを特異メソッドにできる
  • クラス構文の中でextendした場合、クラス構文の直下でextendしたメソッドを使用可能

ミックスインについてもっと詳しく

  • include?メソッドで引数で渡したモジュールがincludeされているかわかる
  • included_modulesメソッドはincludeされているモジュールの配列を返す
  • ancestorsメソッドはモジュールだけでなくスーパクラスも配列で返す
  • classメソッドをかませることでクラスのインスタンスからもincludeされているモジュール情報を取得できる
module TestModule
  puts 'test'
end
>test
=> nil

class Test
  include TestModule
end
=> Test

Test.include?(TestModule)
=> true
Test.included_modules
=> [TestModule, Kernel]
Test.ancestors
=> [Test, TestModule, Object, Kernel, BasicObject]
test = Test.new
=> #<Test:0x00000000b1dcd0>
test.class.include?(TestModule)
=> true
test.class.included_modules
=> [TestModule, Kernel]
  • ダックタイピングの考え方はモジュールにも適用でき、include先のメソッドを使用するメソッドをモジュールに定義することも可能
  • Enumerableモジュールをincludeして、定義されたメソッドを使えるようにするにはinclude先のクラスにeachメソッドを実装しておく必要がある
  • Comparableモジュールをincludeして、定義されたメソッドを使えるようにするには<=>演算子(UFO演算子とも呼ばれる)を実装しておく必要がある
  • puts、require、loopといったメソッドはkernelモジュールが持つメソッド
  • kernelモジュールは全クラスの頂点にあるObjectクラスでincludeされているため、どこでもkernelモジュールのメソッドを利用できる
  • Rubyではクラス構文やモジュール構文に囲まれていない一番外側の部分をトップレベルといい、mainというObjectクラスのインスタンスがselfとして存在している
  • Rubyではクラスやモジュールもオブジェクトであり、Objectクラスを継承しているため内部でkernelモジュールのメソッドを利用できる
  • モジュール内のメソッドでインスタンス変数を読み書きすると、include先のクラスのインスタンス変数を読み書きするが、インスタンス変数は未定義の状態でも代入、参照が可能なので不具合の原因となりやすい
  • モジュールからinclude先で未定義のメソッドを呼び出した場合はエラーが発生するので、インスタンス変数が存在することを前提にするよりメソッドが存在することを前提とした方がいい(setter、getterを利用する)
  • モジュールはオブジェクトに特異メソッドとしてミックスインすることもできる

モジュールを使用した名前空間の作成

  • 大規模プログラムや外部公開ライブラリを作成するときはクラス名の重複が発生する可能性が高くなる
  • モジュール内部にクラスを定義するとそのモジュールに属するクラスになり、同名のクラスがあってもモジュール名が異なっていれば名前が衝突しなくなる(名前空間として利用)
  • モジュール内のクラスを参照する際は、モジュール名::クラス名と記述する
  • 名前空間はクラスのグループ分けやカテゴリ分けとして使う場合もある
  • 名前空間として使うモジュールがすでに定義済みの場合は、モジュール内にクラス構文をネストしなくても、モジュール名::クラス名の形式でクラス定義が可能
module TestModule
end
=> nil

class TestModule::TestClass
end
  1 module TestModule
=> nil
  • 明示的にトップレベルのクラスやモジュールを指定するには、クラス名やモジュール名の前に::をつける

関数や定数を提供するモジュールの作成

  • モジュールに特異メソッドを定義すると、ミックスインしなくても直接モジュール名.メソッド名でそのメソッドを呼び出せる(
module Loggable
  ## class << self の形式でも特異メソッドを定義可能
  def self.log(text)
    puts "[LOG] #{text}"
  end
end
=> :log

Loggable.log('Hello.')
[LOG] Hello.
=> nil
  • モジュールをミックスインとしても、特異メソッドとしても使いたい場合は、module_functionメソッドで対象のメソッドを指定する(これをモジュール関数と呼ぶ)
  • モジュール関数をミックスインすると自動的にprivateメソッドになる
  • モジュールにもクラスと同様の方法で定数を定義できる

状態を保持するモジュールの作成

  • モジュールでもクラスインスタンス変数を利用できる
  • gemではライブラリの設定値をモジュール自身に保持させることがよくある
  • ライブラリ実行に必要な設定値等はアプリケーション全体で共通の値になることが多いが、それを実現するためには設定値はアプリケーションで唯一一つだけであることが望ましい(シングルトンパターン)
  • シングルトンパターンはsingletonモジュールを使用すると便利
  • singletonモジュールをincludeすると、そのクラスのnewメソッドがprivateメソッドになり、クラスの特異メソッドとしてinstanceメソッドが定義され、このメソッドで唯一一つだけのインスタン鵜を取得できる
  • モジュールは複数の用途を兼ねる場合もある

モジュールに関する高度な話題

  • 通常メソッドが探索される順番は自クラス、includeされたmodule、スーパクラスとなる
  • モジュールに別のモジュールをincludeすることもでき、そのモジュールをincludeするとそのモジュールがincludeしているモジュールもincludeしたことになる
  • prependでミックスインするとミックスインしたクラスよりも先にモジュールのメソッドが呼ばれる
  • prependを利用することでエイリアスメソッドを使わずに既存メソッドの置き換えが可能
  • refinementsを使うと独自の変更の有効範囲を限定することができる
  • ::は名前空間の区切りや定数参照、メソッド呼び出しに使用できる
  • .はメソッド呼び出しで使用できるが、名前空間の区切りや定数参照には使用できない
  • 基本的には名前空間の区切りや定数参照に::を使用し、メソッド呼び出しに.を使用する

感想

モジュールはJavaでいうパッケージのようなものかと考えていたが、
実際にはメソッドの追加・変更や共通の特異メソッドの追加など複数の用途があることが分かった。

参考文献

この記事は以下の情報を参考にして執筆しました。