LoginSignup
83
80

More than 5 years have passed since last update.

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

Posted at

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

83
80
2

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
83
80