外側のスコープへのアクセスを保持しながら、コードを Ruby スクリプトのメソッドに分解するにはどうすればよいでしょうか?

概要

サービスのリストを記述する YAML を生成する次のような Ruby スクリプトがあるとします。

require 'yaml'

environment_slug = ENV.fetch('CI_ENVIRONMENT_SLUG')

YAML.dump([
  {
    'name' => 'service1',
    'url' => "https://service1.#{environment_slug}.example.com",
  },
  {
    'name' => 'service2',
    'url' => "https://service2.#{environment_slug}.example.com",
  },
], STDOUT)

ここで、サービス記述の生成を考慮に入れたいと思います。次の単純な試みは機能しません。environment_slug は外部スコープのローカル変数であり、したがってサービス メソッド内では使用できないためです。

require 'yaml'

environment_slug = ENV.fetch('CI_ENVIRONMENT_SLUG')

def service(name)
  {
    'name' => name,
    'url' => "https://#{name}.#{environment_slug}.example.com",
  }
end

YAML.dump([
  service('service1'),
  service('service2'),
], STDOUT)

考えられる解決策の 1 つは、define_method を使用することです。

require 'yaml'

environment_slug = ENV.fetch('CI_ENVIRONMENT_SLUG')

define_method :service do |name|
  {
    'name' => name,
    'url' => "https://#{name}.#{environment_slug}.example.com",
  }
end

YAML.dump([
  service('service1'),
  service('service2'),
], STDOUT)

ただし、define_method はかなり低レベルの機能であるように私には思えます。

別の可能な解決策は、定数を使用することです。

require 'yaml'

ENVIRONMENT_SLUG = ENV.fetch('CI_ENVIRONMENT_SLUG')

def service(name)
  {
    'name' => name,
    'url' => "https://#{name}.#{ENVIRONMENT_SLUG}.example.com",
  }
end

YAML.dump([
  service('service1'),
  service('service2'),
], STDOUT)

一貫性を保つために、おそらく (より複雑な例では) すべてのトップレベル変数を定数にするでしょう。ただし、すべて大文字で入力するのはかなり人間工学的ではなく、すべての「変数」に使用すると見た目も良くありません。

慣用的な Ruby コードを生成し、小さなスクリプト用に多くの定型文を作成したり、物事がより複雑になったときにすべてを再構築したりする必要がないスクリプトを作成するときの良い戦略は何でしょうか?

解決策

これはさまざまな方法で解決できますが、私見では、これをクラスにラップし、アクセサーを使用して Service#environment_slug を呼び出すのが最善です。 「url」キーの値を変更して、代わりにインスタンス変数 @environment_slug をレンダリングすることもできますが、これは主に好みとプログラマの意図の問題です。

require "yaml"

class Service
  attr_reader :environment_slug

  def initialize
    @environment_slug = ENV.fetch("CI_ENVIRONMENT_SLUG")
  end

  def service(name)
    {
      "name" => name,
      "url" => "https://#{name}.#{environment_slug}.example.com",
    }
  end

  # Send your YAML to standard output and also return a value.
  # Kernel#p will quote the output, so it costs you an extra
  # line in your method to do them as separate operations.
  def print
    puts yml = [service('service1'), service('service2')].to_yaml
    yml
  end
end

if __FILE__ == $0
  # Avoid KeyError when testing, or set a sensible default for
  # ENV#fetch in your code above.
  ENV["CI_ENVIRONMENT_SLUG"] = "foo"

  Service.new.print
end

これにより、次が標準出力に出力されます。

---
- name: service1
  url: https://service1.foo.example.com
- name: service2
  url: https://service2.foo.example.com

また、値も返します。これは、メソッドのテストや連鎖メソッドに適していることがよくあります。