Railsでクラスインスタンス変数とクラス変数を利用する前に、設計を見直そう

下記のようなクラスインスタンス変数を定義した後に、値を確認する方法としてAPI::APIBase.class_eval { puts @remote_host }があります。class_evalは、Moduleクラスのインスタンスメソッドでブロック内でレシーバ定義式の中であるように振舞います。

ENV['API_HOST'] = "http://host.docker.internal:3000"

class APIBase
    @remote_host = ENV['API_HOST']
end

class_eval以外にクラスインスタンス変数を取得する方法を社内で尋ねた結果クラスインスタンス変数を利用することは、今回のケースでは必要ないと思いました。

やりたいことは継承した時に、小クラスが親クラスで設定したENV['API_HOST']の値を取得できることです。4つの案を紹介します。

クラス変数

クラスメソッド内でクラス変数を定義する方法です。クラス変数は、スレッドセーフでないため変数が意図とは反する上書きをしてしまう可能性があります。今回のユースケースでは、クラス変数を上書きする必要がないため問題はありませんが危険です。[実験]リクエスト・スレッド間の、変数の種類によるスコープの違いについてを参考に、実験してみるとクラスインスタンス変数においてもスレッドセーフではないようです。

class APIBase
 def self.remote_host
  @@remote_host ||= ENV['API_HOST']
 end
end

cattr_accessor

cattr_accessorは、クラスとインスタンス変数へのgetter,setterを生成します。クラス継承すると、小クラスへの参照することできます。しかし、クラス変数であるためスレッドセーフの問題は解消していません。

cattr_accessorは、mattr_accessorのaliasです。

class APIBase
  cattr_reader :remote_host, instance_accessor: false
end

class APIBase
  def self.remote_host
    @remote_host ||= ENV['API_HOST']
  end
end

ActiveSupport::Configurable

ActiveSupport::Configurableというモジュールをincludeすると、Railsのconfig/application.rbと同じようにconfigを設定できます。複数の設定項目があるならば利用してもよさそうですが、単にAPI_HOSTを定義したい今回のケースに関しては大袈裟なコードです。

class APIBase
  def self.configuration
    @configuration ||= Configuration.new
  end

  def self.configure
    yield configuration
  end
  
  class APIConfiguration
    include ActiveSupport::Configurable
    config_accessor(:remote_host, instance_accessor: false) { ENV['API_HOST'] }
  end
end

APIBase::APIConfiguration.remote_host

定数

素直に定数を使う方法です。遭遇したケースでは、この方法がもっとシンプルな実装なので採用しました。

class APIBase
  REMOTE_HOST ||= ENV['API_HOST']
end

まとめ

  • クラス変数とクラスインスタンス変数以外で実装できないかを考える
  • ActiveSupport::Configurableは、複数の変数を設定する時に便利

参考文献

Method: Module#cattr_accessor

How to use ActiveSupport::Configurable with Rails Engine

パーフェクト Ruby on Rails

パーフェクト Ruby on Rails