Posted at

Ruby: インスタンス変数の初期化とカスケードパターン

More than 3 years have passed since last update.

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() とも書き換えられる

参考。

- instance method Module#attr_accessor (Ruby 2.1.0 リファレンスマニュアル)


インスタンス変数の初期化

インスタンス変数は未定義であってもエラーにならずアクセスできます。(定義はされません)

その時に返る値は 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現在 テストコード、ドキュメントなど整備できてません)