やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【ええな〜コード】OSSのコードから学ぶ設定値(configure)の作り方

こちらは「ええな〜コード」の記念すべき第一本目の記事です。「ええな〜コード」とはOSSのコードから勉強・参考になる箇所を抜粋して、記事として紹介するという趣旨の不定期シリーズです。

今回はよくある設定値(configure)について「ええな〜」と思うコードを見つけたので紹介していこうと思います。

OSSの紹介

今回、コードを読んでいくのはこちらのOSSです。

stitches

github.com

読み方は「スティッチ」で良いでしょうか。ちなみに、あの青色のエイリアンとは全く関係がありません。stitchesはWEBフレークワークでRailsライクの書き方でシンプルなマイクロサービスAPIを作成することが出来ます。7年前から開発されており、かなり御長寿なOSSとなっています。stitchesでは設置値をブロック引数を経由して設定することが出来ます。

GitHub - stitchfix/stitches: Create a Microservice in Rails with minimal ceremony

Stitches.configure do |config|
  config.max_cache_ttl = 5  # seconds
  config.max_cache_size = 100  # how many keys to cache
end

どのようにして設置値を記録しているか

さて、ここからが本題です。実際にコードを見ていきます。まずはlib直下のstitches.rbから。ここでは関連ファイルを読み込んでいるだけでした。

stitches/railtie.rb at main · stitchfix/stitches · GitHub

require 'stitches_norailtie'
require 'stitches/railtie'

※ちなみにrailtie(れいるてぃず)はrailsのコアライブラリの名称だそうです。

d.hatena.ne.jp

先にネタバレになってしまいますが、設定値の記録はstitches_norailtie.rbによって行われています。長いので部分的に省略しますが、重要なのはmodule Stichesと定義された関数です。

stitches/stitches_norailtie.rb at main · stitchfix/stitches · GitHub

module Stitches
  def self.configure(&block)
    block.(configuration)
  end

  def self.configuration
    @configuration ||= Configuration.new
  end
end
require 'stitches/configuration'
# : 
# :
require 'stitches/valid_mime_type'

Stitchesモジュールに定義されたconfigure関数の呼び出し方は先ほど、記載したサンプルコードの通りです。

Stitches.configure do |config|
  config.max_cache_ttl = 5  # seconds
  config.max_cache_size = 100  # how many keys to cache
end

configure関数が呼び出されることで、ブロック実行の引数にconfiguration関数の実行結果が受け渡されます。つまりdo |config|configに渡されるのは@configurationです。初期値が||=演算子によって束縛されるのでConfigurationクラスのインスタンスということになります。

ここで重要なのはConfigurationクラスのインスタンスが@configurationに記録されたという点です。ここだけ覚えておいてください。

moduleにself.関数名を定義すると何が起きるか

先ほどのコードを見てみるとmodule Stitchesの内部でself.configureと何やら見慣れない定義の仕方をしています。

module Stitches
  def self.configure(&block)
    block.(configuration)
  end
end

こうするとシングルトンクラスのようにモジュールを使うことが出来ます。以下のサンプルをご覧ください。

module Sample
  def self.set_name(name)
    @name = name
  end
  
  def self.puts_name
    puts "登録された名前: #{@name}さん"
  end
end

Sample.set_name('OKB')
Sample.puts_name() 

実行結果

登録された名前: OKBさん

インスタンスを作った訳ではないのに@nameにOKBが保持されたままになっており、putsで出力が出来てしまいました。stitchesにもこの仕組みが使われています。よくあるテクニックですが使い方を間違えると、とんでもないことを引き起こす諸刃の剣なので、使い方には注意が必要です。

Configurationクラスの処理

@configurationに記録されたConfigurationクラスのインスタンスは設定値を記録するための責務を担っています。自分だったら先ほどのStitchesモジュールにそのまま、それぞれの設定値に対応したインスタンス変数を定義してしまうでしょう。。。

stitches/configuration.rb at main · stitchfix/stitches · GitHub

# 長くなるため部分的に割愛
class Stitches::Configuration

  def initialize
    reset_to_defaults!
  end

  # Mainly for testing, this resets all configuration to the default value
  def reset_to_defaults!
    @allowlist_regexp = nil
    @custom_http_auth_scheme = UnsetString.new("custom_http_auth_scheme")
    @env_var_to_hold_api_client_primary_key = NonNullString.new("env_var_to_hold_api_client_primary_key","STITCHES_API_CLIENT_ID")
    @env_var_to_hold_api_client= NonNullString.new("env_var_to_hold_api_client","STITCHES_API_CLIENT")
    @max_cache_ttl = NonNullInteger.new("max_cache_ttl", 0)
    @max_cache_size = NonNullInteger.new("max_cache_size", 0)
    @disabled_key_leniency_in_seconds = ActiveSupport::Duration.days(3)
    @disabled_key_leniency_error_log_threshold_in_seconds = ActiveSupport::Duration.days(2)
  end
  # :
  # :
end

すでにお気づきの方もいるでしょうが、このクラスでインスタンス変数として定義されている値が先程のconfigure関数で設定できる項目名に対応しています。それぞれの項目の初期値をインスタンス作成時に記録後、ブロック引数経由でユーザーが必要な項目だけ上書き可能な作りになっています。それぞれの項目にゲッター・セッターが定義されているので、config.max_cache_ttl = 5のような記述が出来るわけですね。

# getter
def max_cache_ttl
  @max_cache_ttl.to_i
end

# setter
def max_cache_ttl=(new_max_cache_ttl)
  @max_cache_ttl = NonNullInteger.new("max_cache_ttl", new_max_cache_ttl)
end

ここで面白いのが、記録する値の制約、バリデーションをそれぞれクラスとして表現している点です。3つのクラスが独自に定義されています。

  • NonNullInteger
  • NonNullString
  • UnsetString

別にセッターでnilの場合にraiseさせたりすれば良いのですが、このようにクラスとして定義しておくことで、判定の共通化、メソッドの定義といった利点が生まれます。ええな〜。

# private化して外部に公開しないというのもええな〜
private

class NonNullInteger
  def initialize(name, value)
    # バリデーション処理
    unless value.is_a?(Integer)
      raise "#{name} must be an Integer, not a #{value.class}"
    end

    @value = value
  end

  def to_i
    @value
  end

  alias to_integer to_i
end

こうしてブロック引数で渡された処理が、クラスのインスタンス変数に記録されているわけですね。非常に洗礼された見事なコードでした。

まとめ

今回、紹介したStitchesでの設定値(configure)の作り方のまとめです。

  • 設定値はブロック引数を経由して受け取る
  • モジュールにself.関数名を定義することで設定値を記録するシングルトンクラスを作成している
  • 設定値の項目一覧と記録は別クラス(Configuration)へ責務を分割している
  • 設定値はそれぞれセッターを使用して上書きが可能
  • 設定値の制約、バリデーションは独自のクラスを定義することで表現している

少しでも「ええな〜」と思ったらイイネ!・シェア!・はてなブックマークを頂けると励みになります。

参考文献