2015年2月19日木曜日

【Ruby】 クラスインスタンス変数の使いどころ

TwitterでストリーミングAPIを利用する際は、gemの tweetstream さんに大変お世話になっています。
ところでこのtweetstreamさん、コンシューマーキーやアクセストークンをリスト1のように設定します。

require 'tweetstream'

TweetStream.configure do |config|
  config.consumer_key       = 'consumer_key'
  config.consumer_secret    = 'consumer_secret'
  config.oauth_token        = 'oauth_token'
  config.oauth_token_secret = 'oauth_token_secret'
  config.auth_method        = :oauth
end

TweetStream::Client.new
# => #<TweetStream::Client:0x00000001525a98
#     @auth_method=:oauth,
#     @consumer_key="consumer_key",
#     @consumer_secret="consumer_secret",
#     @oauth_token="oauth_token",
#     @oauth_token_secret="oauth_token_secret",
#     以下略

するとなんとインスタンスを生成した時点で TweetStream.configure でセットした各値がインスタンス変数に反映されているではありませんか。今まで特に疑問を抱いたことが無かったのですが、そう言えばこれはどのように実装されているのかふと気になって今回調べてみました。


クラス変数とクラスインスタンス変数

少し話が変わりますが、ここでクラス変数とクラスインスタンス変数の違いについて言及します。

クラスインスタンス変数とは、クラスオブジェクト(Classクラスのインスタンス)が持つインスタンス変数のことで、見た目はインスタンス変数と紛らわしく、機能はクラス変数と紛らわしいという大変紛らわしいやつです。

クラスインスタンス変数
  • self がクラスオブジェクト自身を指すコンテキストでしか参照、変更できない(リスト2)
  • 継承先のクラスからは参照、変更できない(クラス変数は継承先でもアクセスできる)

class HogeClass
  # self # => HogeClass
  @class_instance_val = "class_instance_val"

  def self.class_instance_val
    # self # => HogeClass
    @class_instance_val
  end
end

HogeClass.class_instance_val # => "class_instance_val"

本などを読んでも「そのクラスだけの閉じた情報を保持するために使う」とあるだけで、クラス変数との使い分けや具体的な利用場面がよくわからないままでした。


ここで冒頭のリスト1に戻りますが、TweetStream.configure に渡しているブロックのブロック変数 config をおもむろに確認してみると、実は TweetStream になっていることがわかります。ということは TweetStream というオブジェクトに対して @consumer_key などのインスタンス変数があって、少なくともセッターが用意されているということですね。

これはもしかしなくてもクラスインスタンス変数なのではないでしょうか?
まだこれまでにそんなに大量のコードを読んできたわけではありませんが、初めてクラスインスタンス変数が使われているのを見た気がします。(あるいは今まで見かけてもそれと気づいていなかったのか…)


モジュールでクラスインスタンス変数を使う

クラスオブジェクトがインスタンス変数をもつことができるということは、当然モジュールオブジェクトにも同様のことができます。そして本来ならそれも同様にクラスインスタンス変数と呼ぶのかもしれませんが、紛らわしいのでここではモジュールインスタンス変数と呼びます。呼ばせてください。

何故モジュールで使うことも考えるかというと TweetStream もモジュールオブジェクトだからです。

モジュールの場合も同様に、selfがモジュールオブジェクト自身を指すコンテキストでインスタンス変数のように「@」で始まる変数を定義すればよいので、リスト3のようになります。
module HogeModule
  # self # => HogeModule
  @module_instance_val = "module_instance_val"

  def module_instance_val
    # self # => HogeModule
    @module_instance_val
  end

  def module_instance_val=(val)
    # self # => HogeModule
    @module_instance_val = val
  end
  extend self
end

HogeModule.module_instance_val # => "module_instance_val"
HogeModule.module_instance_val = "changed_val"
HogeModule.module_instance_val # => "changed_val"
ちゃんと HogeModule が値を保持していますね。

ゲッター/セッターを書きましたが、これはこのコンテキストで attr_accessor を使えばモジュールインスタンス変数のためのゲッター/セッターを用意してくれるのでは?(リスト4)
module HogeModule
  # self # => HogeModule
  attr_accessor :module_instance_val
  extend self
end

HogeModule.module_instance_val = "module_instance_val"
HogeModule.module_instance_val # => "module_instance_val"
素晴らしいっぽいですね。

思うにクラス変数と機能が似ていながら、あえてクラスインスタンス変数を使うメリットは、このようにゲッター/セッターを簡単に用意できる点にあるのではないでしょうか。(railsにはクラス変数用の attr_accessor 的なはたらきをするやつがあるらしいですが)


クラスインスタンス変数を利用してみる

これまでのことを踏まえた上で、リスト1のような機能を実現するための簡単な例がリスト5になります。

module HogeModule
  module Configuration

    OPTIONS_KEYS = [
      :username,
      :password,
    ].freeze

    attr_accessor(*OPTIONS_KEYS)

    def configure
      # self => HogeModule
      yield self
    end

    def options
      Hash[*OPTIONS_KEYS.collect { |key| [key, send(key)] }.flatten]
    end
  end

  extend Configuration

  class Client
    attr_accessor(*Configuration::OPTIONS_KEYS)

    def initialize
      options = HogeModule.options
      Configuration::OPTIONS_KEYS.each do |key|
        send("#{key}=".to_sym, options[key])
      end
    end
  end
end

HogeModule.configure do |config|
  config.username = "hoge"
  config.password = "poyo"
end

client = HogeModule::Client.new
client.username # => "hoge"
client.password # => "poyo"

HogeModule.configure で設定した値が client というオブジェクトのインスタンス変数にしっかり代入されていますね。

ConfigurationモジュールをHogeModuleモジュールに extend することによってHogeModuleにモジュールインスタンス変数(クラスインスタンス変数)のゲッター/セッターとその他のメソッドを用意します。
  • HogeModule.configure はself(HogeModule)をブロック変数として渡されたブロックを実行します
  • HogeModule.options はキーがモジュールインスタンス変数名で対応する値がその変数の値というハッシュを生成しています

Clientクラスの initialize ではその HogeModule.options で取得したハッシュを利用してインスタンス変数に値を代入しています。これによってモジュールインスタンス変数がもっている値を別のクラスのインスタンス変数に渡すということが実現されているわけですね。

OPTIONS_KEYS定数の要素を増やすだけで簡単にオプションの項目を増やせる柔軟な設計で、何が言いたいかというとクラスインスタンス変数すごい。


おわりに

自分が不勉強なだけでこれは案外よく使われている方法なのかも知れませんが、クラスインスタンス変数の理解を深めるために今回このようにまとめました。

大量のインスタンスを生成する際、毎回 new にオプションをハッシュで渡すのがめんどくさい場面でオプションのデフォルト値を設定するために利用できそうですね。

ご意見ご指摘等ありましたらお待ちしています。

参考サイト

github tweetstream
Rubyを始めたけど今ひとつRubyのオブジェクト指向というものが掴めないという人、ここに来て見て触って!

0 件のコメント:

コメントを投稿