stringオブジェクトがクラス名となるクラスを生成する

FacebookTwitterで異なる写真のURLを一元的に取り扱うために、PhotoUrlクラスを定義して、それをFacebookPhotoUrlやTwitterPhotoUrlに継承するようにしたのはいいけど、結局、ケースごとにインスタンス化するクラスを分けるコードが残って、なんか違うだろうという感じだった。

  def photo_tag(user, options = {:size => :normal})
    case user.provider
    when "twitter"
      photo_url = TwitterPhotoUrl.new(user, options)
    when "facebook"
      photo_url = FacebookPhotoUrl.new(user, options)
    else
      photo_url = LocalPhotoUrl.new(user, options)
    end
    :
    :
  end

で、これを解消するために、クラスの命名規則を決めうちにして、userモデルのproviderフィールドの文字列からクラス名を作ってインスタンス化するようにしてみた。

class PhotoUrl
  def PhotoUrl.generate(user, options)
    # You need to name classes like so: ProviderPhotoUrl
    provider = user.provider
    klass =
      if provider
        provider.capitalize + "PhotoUrl"
      else
        "LocalPhotoUrl"
      end
    Kernel.const_get(klass).new(user, options)
  end

  def initialize(user, options)
    @user, @options = user, options
  end
  :
  :

klassは文字列オブジェクト。これをそのままklass.newとは当然できない。Classクラスのオブジェクトである各種クラスのオブジェクトは、トップレベルの定数として参照を持っているので、Kernel#const_get(klass).new とすればよい。

これでクライアント側のコードは、

  def photo_tag(user, options = {:size => :normal})
    photo = PhotoUrl.generate(user, options)
    :
    :
  end

と、1行になった。