Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rails の隠れた宝石:ActiveSupport StringInquirer を徹底解説

Railsは非常に大きなフレームワークで、毎年さらに成長し続けています。そのため、便利な機能の一部が知られないまま埋もれてしまうことも少なくありません。このシリーズでは、特定のタスクに役立つRails組み込みのあまり知られていない機能を紹介していきます。

記念すべき第1回では、Rails.env.test? を呼び出したときに、内部で実際に何が起こっているのかを、ActiveSupportに含まれるあまり知られていない StringInquirer クラスを通して見ていきます。さらに一歩進んで、StringInquirer のソースコードを読み解きながら、その仕組みを確認します。ネタバレになりますが、これはRuby特有の method_missing メソッドを使ったメタプログラミングのシンプルな実例となっています。

Rails.env ヘルパーとは

Railsの環境をチェックするコードを目にしたことがある方は多いでしょう。

if Rails.env.test?
  # ハードコードされた値を返す...
else
  # 外部APIに接続する...
end

しかし、test? は実際には何をしているのでしょうか?そしてこのメソッドはどこから来ているのでしょうか?

コンソールで Rails.env を確認すると、文字列のように振る舞います。

=> Rails.env
"development"

この文字列は、RAILS_ENV 環境変数に設定されている値です。しかし、これは単なるStringではありません。コンソールでクラスを調べてみると:

=> Rails.env.class
ActiveSupport::StringInquirer

Rails.env の正体は、実はStringInquirerだったのです。

StringInquirer の正体

StringInquirerのコードは短く、しかも丁寧にドキュメント化されています。ただし、Rubyのメタプログラミングに慣れていないと、その動作原理は一見わかりにくいかもしれません。ここで順を追って解説していきましょう。

class StringInquirer < String
...
end

まず注目すべきは、StringInquirerがStringのサブクラスであるという点です。これが Rails.env がStringのように振る舞う理由です。Stringを継承することで、すべてのString的な機能が自動的に使えるようになるため、通常の文字列として扱えます。Rails.env.upcase はもちろん、ActiveRecordModel.find_by(string_column: Rails.env) のような使い方も問題なく動作します。

Rails.env はStringInquirerの手軽な組み込み例ですが、自分で作ることも自由自在です。

def type
  result = "old"
  result = "new" if @new

  ActiveSupport::StringInquirer.new(result)
end

すると、戻り値に対して疑問符付きのメソッドが使えるようになります。

=> @new = false
=> type.old?
true
=> type.new?
false
=> type.testvalue?
false

=> @new = true
=> type.new?
true
=> type.old?
false

method_missing の魔法

ここでの本当の秘訣は method_missing です。Rubyでは、オブジェクトに対してメソッドを呼び出すと、Rubyはまずそのオブジェクトの祖先(基底クラスやincludeされたモジュール)の中から該当するメソッドを探します。見つからなかった場合、Rubyは method_missing を呼び出し、探していたメソッド名と引数を渡します。

デフォルトでは、このメソッドは単に例外を発生させます。NoMethodError: undefined method 'test' for nil:NilClass のようなエラーメッセージは誰もがお馴染みでしょう。私たちは、例外を発生させない独自の method_missing を実装できます。それこそまさに StringInquirer が行っていることです。

def method_missing(method_name, *arguments)
  if method_name.end_with?("?")
    self == method_name[0..-2]
  else
    super
  end
end

? で終わるあらゆるメソッド名に対して、self(つまり文字列そのもの)の値を、? を除いたメソッド名と比較します。言い換えれば、StringInquirer.new("test").long_test_method_name? を呼び出すと、返されるのは "test" == "long_test_method_name" の評価結果ということです。

一方、メソッド名が疑問符で終わっていない場合は、元の method_missing(例外を発生させる方)にフォールバックします。

respond_to_missing? の役割

ファイル内にもうひとつメソッドがあります。それが respond_to_missing? です。これは method_missing の相棒と言える存在です。method_missing によって機能自体は実現できますが、Rubyに対して「こういう疑問符付きメソッドを受け付けますよ」と伝える手段も必要になります。

def respond_to_missing?(method_name, include_private = false)
  method_name.end_with?("?") || super
end

これはオブジェクトに対して respond_to? を呼び出したときに効いてきます。これがないと、StringInquirer.new("test").respond_to?(:test?) の結果は false になってしまいます。明示的に test? というメソッドを定義していないからです。しかし Rails.env.respond_to?(:test?) が true を返すことを期待するなら、これは明らかに誤解を招く挙動です。

respond_to_missing? があることで、「はい、そのメソッドは処理できます」とRubyに伝えられるのです。メソッド名が疑問符で終わらない場合は、スーパークラスの実装に委ねられます。

実践的なユースケース

StringInquirerの仕組みがわかったところで、どのような場面で役立つのか具体的な例を見ていきましょう。

1. 環境変数の判定

環境変数は、StringInquirerとの相性が抜群の2つの条件を満たしています。第一に、取り得る値のセットが限られていて既知であることが多い(enum のようなもの)点。第二に、その値に依存する条件分岐ロジックを書くことが多い点です。

例えば、アプリが決済APIに接続しており、認証情報を環境変数に保存しているとしましょう。本番システムでは当然リアルなAPIを使いますが、ステージングや開発環境ではサンドボックスAPIを使いたいはずです。

# ENV["PAYMENT_API_MODE"] = sandbox/production

class PaymentGateway
  def api_mode
    # ENV.fetch を使うことで、値が欠落していた場合に例外を発生させられる
    @api_mode ||= ENV.fetch("PAYMENT_API_MODE").inquiry
  end

  def api_url
    # プロのコツ: MODE == 'production' のとき*だけ*本番URLを使い、
    # それ以外はすべてサンドボックスにデフォルト設定する。
    # こうすることで、タイプミスや誤った値でも本番URLを使ってしまう事故を防げる
    if api_mode.production?
      PRODUCTION_URL
    else
      SANDBOX_URL
    end
  end
end

上の例では、ActiveSupplyの String#inquiry メソッドを使っています(※正しくはActiveSupportのメソッドです)。このメソッドを使えば、Stringを手軽にStringInquirerへ変換できます。

2. APIレスポンスの状態判定

先ほどの決済APIの例を続けましょう。APIからのレスポンスには、成功/失敗の状態が含まれています。これも「取り得る値が限られている」「その値をテストする条件分岐がある」という2つの条件を満たすため、StringInquirerの候補となります。

class PaymentGateway
  def create_charge
    response = JSON.parse(api_call(...))

    result = response["result"].inquiry

    if result.success?
      ...
    else
      ...
    end

    # result は引き続き文字列としても扱える
    Rails.logger.info("Payment result was: #{result}")
  end
end

まとめ

StringInquirerは、ポケットに忍ばせておくと面白いツールですが、個人的には頻繁に使うものではないと考えています。用途はありますが、大抵の場合、オブジェクトに明示的なメソッドを定義すれば同じ結果が得られます。明示的なメソッド名にはさらなる利点もあります。値を変更する必要が出た場合に更新箇所が1か所で済むだけでなく、開発者がメソッドを探す際にコードベース全体を検索しやすくなります。

この記事はStringInquirerに焦点を当てていますが、本来の狙いは method_missing を通じてRubyのメタプログラミング機能へのやさしい入門を提供することです。アプリケーションコードで method_missing を使うことは、おそらく推奨されません。しかしこれはRailsのようなフレームワークや、gemが提供するDSLでよく使われる手法です。そのため、問題に遭遇したときに「裏側で何が起きているのか」を理解しておくことは、とても役立つはずです。


  1. Google Playが選ぶ2022年「ヒドゥン ジェム」部門ベストアプリTOP5

    Google Playストアには、ゲーム、ソーシャル、スマホの最適化、バッテリー節約など、あらゆるカテゴリで数百万ものアプリが登録されています。その中には、有名アプリほど注目されていないものの、実際に使ってみると非常に便利で「これなしでは困る」と感じられるアプリが数多く存在します。Google Playは毎年、こうした「隠れた名作=ヒドゥン ジェム」カテゴリで上位にランクインしたアプリを発表しています。今回は、2022年に同部門で受賞したAndroidアプリの中から、特におすすめの5本をご紹介します。Socratic – Math Answers & Homework HelpSocr

  2. Windows 11の隠れた便利機能15選!今すぐ使えるヒントと設定を徹底解説

    Windows 11は対応デバイス向けに無料アップグレードとして提供されており、数多くの新機能と改善が含まれています。再デザインされたスタートメニューとタスクバー、Androidアプリに対応した強化されたMicrosoft Store、統合されたMicrosoft Teams、スナップレイアウト、ウィジェットなど、目立つ新機能は多いものの、Redmond(マイクロソフト本社)が公式に発表した大きなニュース以外にも、一見すると気づきにくい小さな変更点がたくさん隠れています。この記事では、知っておくべきWindows 11の隠れた機能を厳選してご紹介します。 スタートメニューを左側に戻す方法 W