attr_accessor でインスタンス変数のアクセサは簡単に作れるけど、initialize の引数で値の初期化をしていると変更があった時に面倒。。。と思って書いたメモです。
ふーん、こいつはこんなことしたんだ、くらいに適当にお読みください。
カスケードパターン
recevier.message(arg1, arg2, ... argN)
上は Ruby のメソッド呼び出しの形です。これは send メソッドを使って以下のように書けます。
receiver.send :message, arg1, arg2, ... argN
「モノ(オブジェクト)とモノがメッセージのやり取りをする」(メッセージパッシング)をよく表しているスタイルだと思います。
メッセージパッシングは、オブジェクト指向の重要な概念だと思います。
カスケードパターンは、レシーバ(メッセージを受け取るオブジェクト)に一度に複数のメッセージを投げるデザインパターンです。
他言語ですが、CoffeeScript (jQueryを使用) の例です。(雰囲気だけ見ていただればいいです)
$ '#stage' # オブジェクト「$ 'stage'」に対して(レシーバに対して)
.text 'Drop file here.' # text メソッドを呼び出し(メッセージを送る)
.on 'dragover', dnd() # on メソッドを呼び出し(メッセージを送る)
.on 'drop', dnd (e) -> console.log e # on メソッドを呼び出し(メッセージを送る)
カスケードパターン(Cascade) は以下の書籍で紹介されています。
- 『ケント・ベックの Smalltalk ベストプラクティス・パターン』
- ピアソンエデュケーション (2003/03)
- ISBN-10: 4894717549
- ISBN-13: 978-4894717541
アクセサと attr_accessor
アクセサはインスタンス変数にアクセスするためのメソッドです。
アクセサには getter と setter があります。
getter は、レシーバに変数の値を取得する、というメッセージを投げます。(値を取得するメソッド)
setter は、レシーバに変数の値を設定する、というメッセージを投げます。(値を設定するメソッド)
Ruby にはアクセサを簡単につくる attr_accessor メソッドがあります。
attr_accessor はクラス(定義)文の中で以下のように使えます。
class C
attr_accessor :foo
end
上のコードは下のコードと同じことです。(アクセサが定義されます)
class C
def foo # getter
@foo
end
def foo=(val) # setter
@foo = val
end
end
attr_accessor の引数がシンボル「:foo
」の場合、アクセサとインスタンス変数の名前は、以下のようになります。
- getter は「
foo
」になる。(シンボルの頭の「:
」を取った名前) - setter は「
foo=
」になる。(シンボルの頭の「:
」を取って、末尾に「=
」を付けた名前) - インスタンス変数は「
@foo
」になる。(シンボルの頭の「:
」を「@
」に替えた名前)
アクセサは以下のように呼び出されます。
obj = C.new
obj.foo = 'hello' # setter 呼び出し: obj.foo=('hello') とも書き換えられる
p obj.foo # getter 呼び出し: obj.foo() とも書き換えられる
参考。
インスタンス変数の初期化
インスタンス変数は未定義であってもエラーにならずアクセスできます。(定義はされません)
その時に返る値は nil です。
インスタンス変数の初期値をクラスを使うユーザに設定させたい場合があります。
例えば、インスタンスを new する時に以下のように設定させたいとします。
c = C.new('hello')
上の要求は、initialize メソッドを以下のように実装することで実現できます。
class C
attr_accessor :foo
def initialize(foo)
@foo = foo
end
end
余談ですが、上のコードは下のように書き換えられます。(この書き方がいいかどうかは別問題です)
class C
attr_accessor :foo
alias initialize foo= # この場合の initialize の形は foo= と同じなので alias しちゃう
end
カスケードパターン initializer
ここからが、本題です。
アクセサをもったインスタンス変数(以降、プロパティと呼びます)が、だんだん多くなってくると、その度に initialize のインタフェース設計を変更する必要があり面倒です。
(面倒でなくても、ここでは面倒と思ってください)
class C
attr_accessor :foo1, :foo2, :foo3, ... (略)
def initialize(foo1, foo2, foo3, ...) # プロパティが増える度に変更する必要がある...(;_;)
@foo1 = foo1
:
end
そこで、カスケードパターンの応用です。
new にはブロックを渡させます。そのブロックは new されるオブジェクト自身を渡して呼び出します。
そうすると、ブロックの中でオブジェクトに対して、任意に setter を呼び出してまとめてプロパティの設定を行うことができます。
# 定義側 ....... プロパティが増えても initialize を変更しないでも、だいたい間に合う
class C
attr_accessor :foo1, :foo2
def initialize
yield(self) if block_given? # ブロックが与えられた場合のみ実行するため if block_given? をつける
end
end
# 呼び出し側 ....... 設定したいプロパティは、ブロック内で任意に設定する
c = C.new do |obj|
obj.foo1 = 'hello' # 任意の setter を呼び出す
obj.foo2 = 'world'
end
p c.foo1 #=> "hello"
p c.foo2 #=> "world"
instance_eval を使う
定義する方は幸せになりましたが、呼び出しのブロックで毎回レシーバを指定しているのはちょっとアレです。
instance_eval を使ってみましょう。
instance_eval は与えられたブロックを評価しますが、その際にブロックの self を自分自身にします。
# 定義側
class C
attr_accessor :foo1, :foo2
def initialize(&block)
instance_eval(&block) if block
end
end
これで、呼び出し側もすっきりします。(してますよね?)
c = C.new do
foo1 = 'hello'
foo2 = 'world'
end
そして確認。
p c.foo1 #=> nil # 値が設定されていない!!
p c.foo2 #=> nil # 値が設定されていない!!
。。。。失敗です。値が設定されていません。
これはブロックの中の「foo1 = 'hello'」等が setter 呼び出しでなく、ローカル変数への代入と解釈されたからです。
残念です。
attr_accesor 代替メソッド
instance_eval は残念でした。。。と、諦めたらそこで試合終了です。
諦めず、attr_accessor の代替メソッドを作りました。名前は property としました。
property は attr_accessor と同じくアクセサを作ります。
アクセサメソッドは getter/setter 兼用で、引数が与えられた場合は setter、与えられない場合は getter として機能します。
setter が以下のように書けて、Ruby にもメソッド呼び出しと解釈されます。
obj = C.new { foo 'hello' }
以下が property の実装です。実装の説明は割愛します。(本題でないし、長くなってきたので。。。)
module Property
def property(name)
module_eval %-
def #{name}(*arg)
if arg.empty?
@#{name} # getter
else
@#{name} = arg.first # setter
end
end
-
end
end
使い方の例示も兼ねて、propery を使ってクラス定義してみます。
# 定義側
class C
extend Property # Propery モジュールを extend する
property :foo1 # attr_accessor の代わりに propery を使う
property :foo2 # 〃
def initialize(&block)
instance_eval(&block) if block
end
end
呼び出し側も変更します。
c = C.new do
foo1 'hello' # setter 呼び出し: foo1('hello') とも書き換えられる
foo2 'world' # setter 呼び出し: foo2('world') とも書き換えられる
end
そして確認。
p c.foo1 #=> "hello"
p c.foo2 #=> "world"
できました。OKです。
完成形
instance_eval ではブロックの self がオブジェクト自身になります。
ですが、クロージャを渡したい時など self はそのままの方がいい場合も考えられます。
なので、渡されたブロックに
- 引数がある場合はオブジェクト自身を引数で渡し
- 引数が無い場合は instance_eval を使う
ことにします。
以下が、完成形です。
module Property # 前出と同じです
def property(name)
module_eval %-
def #{name}(*arg)
if arg.empty?
@#{name} # getter
else
@#{name} = arg.first # setter
end
end
-
end
end
# 定義側
class C
extend Property # Propery モジュールを extend する
property :foo1 # attr_accessor の代わりに propery を使う
property :foo2 # 〃
def initialize(&block)
case block.arity # ブロックの引数の数で場合分け
when 0 ; instance_eval &block
else ; yield self
end if block
end
end
確認です。
# 引数なしブロックの場合
c1 = C.new do
foo1 'hello' # setter 呼び出し: foo1('hello') とも書き換えられる
foo2 'world' # setter 呼び出し: foo2('world') とも書き換えられる
end
p c1.foo1 #=> "hello"
p c1.foo2 #=> "world"
foo1 = 'have'
foo2 = 'fun'
# 引数ありブロックの場合
c2 = C.new do |obj|
obj.foo1 foo1 # セットしているのは、ブロックの外の foo1
obj.foo2 foo2 # セットしているのは、ブロックの外の foo2
end
p c2.foo1 #=> "have"
p c2.foo2 #=> "fun"
OK です。
ここでおしまいです。
おわりに
本稿内容の動作確認は以下の環境で行っています。
- Ruby 2.1.5 p273
property と cascade は Gem にしていますが、Gem 版は仕様・実装が異なります。
(2014/12/27現在 テストコード、ドキュメントなど整備できてません)