Rubyのクラス定義を深く理解する
概要
この記事は、「メタプログラミングRuby 第2版」第5章「クラス定義」を読み学習した内容を個人学習用にまとめ直したものです。
Rubyのクラス定義は単なる設計図ではなく、実行可能なコードです。本記事では、カレントクラス、特異メソッド、特異クラス、クラスマクロ、メソッドラッパーなど、クラス定義にまつわる技法・仕組みについて解説します。
クラス定義
クラス定義の基本
クラス定義はメソッドを定義する場所ではなく、実際にはそれ以外のあらゆるコードを記述できる。
また、メソッドやブロックと同じように、クラス定義も最後に評価された式の値を返す。
result = class MyClass
self
end
result # => MyClass
上記のように、クラス定義の中ではクラスがカレントオブジェクトselfとなる。
クラスとモジュールはそれぞれClassクラスとModuleクラスのオブジェクトである。
カレントクラス
Rubyのプログラムは、常にカレントオブジェクトselfをもち、同様に常にカレントクラス(あるいはカレントモジュール)も持っている。
- プログラムのトップレベルでは、カレントクラスはmainのクラスとなる
Objectクラスとなる -
classキーワードでクラスをオープンすると、そのクラスがカレントクラスになる- クラス定義の中ではカレントオブジェクト
selfはそのクラス自身なので、カレントクラスと同じとなる
- クラス定義の中ではカレントオブジェクト
- メソッドの中ではカレントオブジェクトのクラスがカレントクラスになる
class_eval
Module#class_evalを使用することでレシーバのクラスをオープンし、そのコンテキストでブロックを評価できる。
def add_method_to(a_class)
# 引数のクラスをオープンしメソッドを定義する
a_class.class_eval do
def m
"Hello!"
end
end
end
add_method_to(String)
"abc".m # => "Hello!"
クラスインスタンス変数
クラス定義の中でそのクラス自身となるClassオブジェクトがselfとなる場所に定義されているインスタンス変数をクラスインスタンス変数と呼ぶ。
クラスインスタンス変数とそのクラスから作成されたオブジェクトのインスタンス変数は別物なので注意。
class MyClass
# MyClassというClassオブジェクトのインスタンス変数(クラスインスタンス変数)
# MyClassから作成されるオブジェクトのインスタンス変数ではない
@my_var = 1
def self.read
@my_var # このクラス自身のクラスインスタンス変数を読み取る
end
def write
@my_var = 2 # クラス定義から作成したオブジェクトの属性を更新
end
def read
@my_var # クラス定義から作成したオブジェクトの属性を読み取る
end
end
obj = MyClass.new
obj.read # => nil
obj.write
obj.read # => 2
MyClass.read # => 1
特異メソッド
Rubyでは特定のオブジェクトにだけメソッドを追加できる。これを特異メソッドと呼ぶ。
str = "just a regular string"
# strオブジェクトにだけ特異メソッドを定義
def str.title?
self.upcase == self
end
str.title? # => false
# 別のStringオブジェクトには特異メソッドは存在しない
"another string".title? # => NoMethodError
クラスメソッド
クラスはClassオブジェクトであり、クラスメソッドはそのClassオブジェクトの特異メソッドとなる。
class MyClass
# 以下の2つは同じ意味
def self.hello
"Hello from #{self}!"
end
end
def MyClass.goodbye
"Goodbye from #{self}!"
end
MyClass.hello # => "Hello from MyClass!"
MyClass.goodbye # => "Goodbye from MyClass!"
クラスマクロ
アクセサを一気に定義できるModule#attr_*メソッドは実態はただのメソッド呼び出し(Moduleのインスタンスメソッド)だが、見た目が言語組み込みのキーワードのように記述される。
このようなメソッドのことをクラスマクロと呼ぶ。
class MyClass
# 以下はキーワードではなく、Moduleのインスタンスメソッド
attr_accessor :my_attribute
end
クラスマクロの活用例:
メソッド名を変更したいが、旧メソッド名も一定期間残して警告を出したい場合、deprecateクラスマクロを自作することで宣言的に非推奨化できる。
class Module
# 古いメソッド名を受け取って動的定義し、その中で警告メッセージとともに新メソッドを呼び出す
def deprecate(old_method, new_method)
define_method(old_method) do |*args|
warn "警告: #{old_method}は非推奨です。#{new_method}を使ってください。"
send(new_method, *args)
end
end
end
class Book
def title
"メタプログラミングRuby"
end
# 自作クラスマクロ: GetTitleを非推奨にしtitleへ転送する
deprecate :GetTitle, :title
end
Book.new.GetTitle
# => 警告: GetTitleは非推奨です。titleを使ってください。
# => "メタプログラミングRuby"
Railsではクラスマクロが多用されている。以下はその代表例。
| クラスマクロ | 役割 |
|---|---|
attr_accessor :name |
ゲッター・セッターの自動生成 |
has_many :posts |
アソシエーション定義 |
validates :name, presence: true |
バリデーション定義 |
before_action :authenticate! |
コールバック定義 |
scope :active, -> { where(active: true) } |
スコープ定義 |
これらはすべてクラス定義の中で呼ばれるクラスメソッドであり、内部ではメソッドの動的生成やコールバック・バリデーションの登録などを行っている。
特異クラス
そのオブジェクトのみが固有で持つ特異メソッドは、そのオブジェクトのクラスや親クラスには定義が存在せず、オブジェクトの裏に存在する**特異クラス(メタクラス、シングルトンクラス)**という特別なクラスに定義される。
特異メソッドとは、特異クラスに定義されたインスタンスメソッドに他ならない。
特異クラスは以下のような特徴を持つ。
- インスタンスを一つしか持てない
-
class <<やObject#singleton_classという特別な構文を使わなければ通常は不可視
obj = Object.new
singleton_class = class << obj # 特異クラスのスコープにアクセスする構文
self
end
singleton_class.class # => Class
# または
obj.singleton_class # => #<Class:#<Object:0x331df0>>
メソッド探索における特異クラスと継承の関係
Rubyがメソッドを探索する際、特異クラスはそのオブジェクトの通常のクラスよりも先に探索される。
オブジェクトの場合
obj.hello を呼ぶと...
obj → #<Class: obj>(特異クラス) → MyClass → Object → Kernel → BasicObject
↑ まずここを探す ↑ 次にここ
class MyClass
def hello
"通常のhello"
end
end
obj = MyClass.new
def obj.hello
"特異メソッドのhello"
end
obj.hello # => "特異メソッドのhello"(特異クラスが先に探索される)
クラスの場合
クラスメソッドはClassオブジェクトの特異メソッドであるため、クラスの特異クラスに定義される。
MyClass.my_method を呼ぶと...
#<Class: MyClass>(特異クラス) → #<Class: Object> → #<Class: BasicObject> → Class → Module → Object
↑ クラスメソッドはここにいる
# クラスメソッドの定義方法は3つ
def MyClass.my_method
"クラスメソッド"
end
class MyClass
def self.my_method
"クラスメソッド"
end
end
class MyClass
class << self # 特異クラスをオープンして定義
def my_method
"クラスメソッド"
end
end
end
MyClass.singleton_class.instance_methods(false) # => [:my_method]
二つの継承チェーン
Rubyの内部には二つの継承チェーンが並行して存在し、レシーバに応じてどちらを辿るかが決まる。特異クラスの継承により、クラスメソッドも親クラスから引き継がれる。
インスタンスメソッド用(obj.hello):
#<Class: obj> → Child → Parent → Object → Kernel → BasicObject
クラスメソッド用(Child.hello):
#<Class: Child> → #<Class: Parent> → #<Class: Object> → ... → Class → Module → Object
class Parent
def self.greet
"Hello from Parent"
end
end
class Child < Parent
end
Child.greet # => "Hello from Parent"(#<Class: Parent> から継承)
継承の関係をまとめると以下の通り。
- オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである
- クラスの特異クラスのスーパークラスは、クラスのスーパークラスの特異クラスとなる
モジュールからクラスメソッドをincludeする方法
モジュールにクラスメソッドを定義して単にクラスにincludeするのみでは、クラスメソッドとしてクラスに定義することはできない。
モジュールのincludeで手に入るのは、モジュールのインスタンスメソッドのみだからである。
module MyModule
def self.my_method
"hello"
end
end
class MyClass
include MyModule
end
MyClass.my_method # NoMethodError!
クラス拡張
モジュールのメソッドをクラスメソッドとしてincludeするには、以下の手順を踏む。
- モジュールでメソッドを単なるインスタンスメソッドとして定義する
- 挿入したいクラスの特異クラスでモジュールを
includeする
特異クラスのインスタンスメソッド == 特異クラスのオブジェクトの特異メソッド(クラスメソッド)なので、
この方法で定義できる。この手法をクラス拡張と呼ぶ。
module MyModule
def my_method
"hello"
end
end
class MyClass
# 特異クラスをオープンしてincludeする
class << self
include MyModule
end
end
MyClass.my_method # "hello"
オブジェクト拡張
クラス拡張の手法は通常のオブジェクトにも適用できる(オブジェクト拡張)。
module MyModule
def my_method
"hello"
end
end
obj = Object.new
class << obj
include MyModule
end
obj.my_method # => "hello"
obj.singleton_methods # => [:my_method]
extend
クラスやオブジェクトを拡張するために特異クラスをオープンするのはあまり自然なことではないので、クラスやオブジェクトを拡張する専用のショートカットメソッドObject#extendが存在する。
module MyModule
def my_method
"hello"
end
end
obj = Object.new
obj.extend MyModule
obj.my_method # => "hello"
class MyClass
extend MyModule
end
MyClass.my_method # => "hello"
メソッドラッパー
アラウンドエイリアス
Module#alias_methodでメソッドに別名をつけることができる。
class MyClass
def my_method
"hello"
end
# 新しい名前を先に、元の名前を後に書く
# シンボルか文字列で渡す
alias_method :m, :my_method
end
obj = MyClass.new
obj.my_method # => "hello"
obj.m # => "hello"
alias
aliasはRubyのキーワード、alias_methodはModuleのメソッドである。alias_methodは動的に変数を渡せるがメタプログラミング向き。トップレベルスコープではModuleのメソッドであるalias_methodは使えないため、aliasを使う必要がある。
def greet = "hello"
alias original_greet greet # OK(キーワードなのでどこでも使える)
# alias_method :original_greet, :greet # NoMethodError(Moduleのメソッドなので使えない)
さらに、以下の手順を踏むことで、メソッドを同じ名前で再定義し、動作を拡張することができる。
この手法をアラウンドエイリアスと呼ぶ。
- メソッドにエイリアスをつける
- メソッドを再定義する
- 新しいメソッドから古いメソッドを呼び出す
class MyClass
def greet
"こんにちは"
end
# 1. メソッドにエイリアスをつける
alias_method :original_greet, :greet
# 2. メソッドを同じ名前で再定義する
def greet
# 3. 新しいメソッドから古いメソッド(エイリアス)を呼び出す
result = original_greet
"★ #{result} ★"
end
end
obj = MyClass.new
obj.greet # => "★ こんにちは ★"
obj.original_greet # => "こんにちは"
Refinementsラッパー
Refinementsを使うと、refineブロック内でメソッドを再定義し、superで元のメソッドを呼び出すことでメソッドラッパーを作ることができる。
class MyClass
def greet
"こんにちは"
end
end
module GreetWrapper
refine MyClass do
def greet
# superで元のMyClass#greetを呼び出す
result = super
"【 #{result} 】"
end
end
end
# usingしなければ元のまま
MyClass.new.greet # => "こんにちは"
# usingするとそのスコープ以降でラッパーが有効になる
using GreetWrapper
MyClass.new.greet # => "【 こんにちは 】"
Refinementsはrefineでクラスを拡張し、usingで有効化するスコープ限定の仕組みである。アラウンドエイリアスと違いグローバルに影響しない。usingはファイルやモジュール定義のスコープ単位で有効になり、メソッド内では使用できない。
Prependラッパー
prependでモジュールをインクルーダー(クラス)の直後に挿入し、superで継承チェーン上のインクルーダーのメソッドを呼び出すことでラッパーにできる。
Refinementsラッパーやアラウンドエイリアスよりも明示的で綺麗な手法とされている。
class MyClass
def greet
"こんにちは"
end
end
module GreetWrapper
def greet
# superで継承チェーン上の次のメソッド(MyClass#greet)を呼び出す
result = super
"《 #{result} 》"
end
end
MyClass.prepend(GreetWrapper)
MyClass.new.greet # => "《 こんにちは 》"
3つのメソッドラッパー手法の比較
| 手法 | 仕組み | 特徴 |
|---|---|---|
| アラウンドエイリアス |
alias_methodで退避し再定義 |
シンプルだが再実行に弱い |
| Refinementsラッパー |
refine+usingでスコープ限定 |
安全だがスコープ制約がある |
| Prependラッパー |
prependで継承チェーンに挿入 |
最もクリーンで推奨される手法 |
まとめ
- Rubyのクラス定義は実行可能なコードであり、最後に評価された式の値を返す。クラス定義の中では
selfがそのクラス自身となる -
Module#class_evalを使うことで、変数に格納されたクラスをオープンしてメソッドを動的に定義できる - 特異メソッドは特定のオブジェクトだけが持つメソッドであり、オブジェクトの裏に存在する特異クラスに定義される
- メソッド探索では特異クラスが通常のクラスより先に探索され、クラスメソッドもクラスの特異クラスに定義された特異メソッドである
- メソッドラッパーにはアラウンドエイリアス・Refinements・prependの3手法があり、prependが最もクリーンで推奨される
参考文献
この記事は以下の情報を参考にして執筆しました。