やわらかテック

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

【ええな〜コード】フェイクデータ作成gemのfakerはどのようにして作られているのか

僕は技術のキャッチアップのために、定期的にgithubのトレンドを見ています。
最近、Rubyのカテゴリで「faker」という昔から使われているフェイクデータを作成するgemがよくトレンドに上がっており、リリースも今現在(2023年6月)も活発にされているようです。

github.com

fakerのようなフェイクデータの作成を行うライブラリは様々な言語で公開されており、単体テストを書く際に重宝しています。ユーザー名やメールアドレスを作成してもらえるのでデータの用意が非常に楽になります。fakerの存在自体は前から知っていたのですが、どのように実装されているのか、裏側で何が処理されているのかまでは知りませんでした。

せっかくトレンドで遭遇したので、fakerがどのように作られているのか裏側を見てみようと思います。

まずはfaker.rbから

gemのコードを読む場合はlib直下に配置されているgem名.rbのファイルから見ていくと流れが追いやすいです。
今回はlib/faker.rbがあるので、このファイルから見ていきます。ざーっと目を通してみると設定値を保持させているFaker::ConfigモジュールとFaker::Baseクラスの二つが定義されていました。Faker::Baseクラスに定義されている関数はどんな役割を持っているかはまだ分からないため、一旦、飛ばします。

lib/faker.rb

module Faker
  module Config
    class << self
      def locale=(new_locale)
        Thread.current[:faker_config_locale] = new_locale
      end
      :
    end
  end

  class Base
    class << self
      attr_reader :flexible_key
      :
      def fetch(key)
        :
      end
      :
    end
  end
end

faker/faker.rb at main · faker-ruby/faker · GitHub

fakerの関数が呼び出される時

次は実際にfakerの関数が呼び出される際の処理の流れをコードを見て追ってみます。
なんとスタジオジブリの映画に登場するキャラクターの名前を取得できる関数があるので、こちらを見ていきます。

Faker::JapaneseMedia::StudioGhibli.character #=> "Chihiro"

lib/faker/japanese_mediaの配下にstudio_ghibli.rbというファイルが定義されています。
呼び出しているのはこの関数で間違いないようです。

lib/faker/japanese_media/studio_ghibli.rb

# frozen_string_literal: true

module Faker
  class JapaneseMedia
    class StudioGhibli < Base
      class << self
        :
        def character
          fetch('studio_ghibli.characters')
        end
        :
      end
    end
  end
end

faker/studio_ghibli.rb at main · faker-ruby/faker · GitHub

このファイルと、他のファイルも合わせて見てみると共通点が見えてきます。
lib/faker配下のファイルは全てmodule Fakerの内部にデータ名を表すクラス(eg: StudioGhibli)を定義しており、どのクラスもfaker.rbで定義されていたBaseクラスを継承しています。moduleではなくクラスを定義しているのはクラスメソッドとして呼び出せるのが楽という点と、Baseクラスを継承したいからだと思われます。

新しくjapanese_mediaにコードギアス(code_geass)を追加したければ、faker/japanese_media/code_geass.rbを作成し、Baseクラスを継承させるだけでコードの対応は完了です。非常に拡張性に富んだ設計になっていますね...。

  • ジブリ: faker/japanese_media/studio_ghibli.rb
  • スターウォーズ: lib/faker/movies/star_wars.rb
  • 住所: lib/faker/default/address.rb
  • (NEW) コードギアス: faker/japanese_media/code_geass.rb

そして、多くの場合にはデータ名を表すクラスはBaseクラスに定義されているfetch関数を呼び出しています。

faker/japanese_media/studio_ghibli.rb

def character
  fetch('studio_ghibli.characters')
end

ということで、次はBaseクラスのfetch関数を見ていきましょう。

Baseクラス: fetch関数について

fetch関数は引数として受け取った値(eg: studio_ghibli.characters)をtranslate関数からsample関数へ受け渡しているようです。 sample関数とtranslate関数も同じくBaseクラスに定義されている関数です。

lib/faker.rb

def fetch(key)
  fetched = sample(translate("faker.#{key}"))
  if fetched&.match(%r{^/}) && fetched&.match(%r{/$}) # A regex
    regexify(fetched)
  else
    fetched
  end
end

translate関数

translate関数は内部でi18nという国際化対応のgemを使用しています。
i18nを使うことでymlファイルに定義された値を読み取る事が可能となり、コード内に大量を値を定義せずに済みます。またコードの修正をせずにymlファイルだけを変更すれば値の追加・削除が可能というのも嬉しいポイントです。

lib/faker.rb

def translate(*args, **opts)
  opts[:locale] ||= Faker::Config.locale
  opts[:raise] = true
  I18n.translate(*args, **opts)
rescue I18n::MissingTranslationData
  :
end

translate関数は引数の受け取り方が面白いです。
第1引数は配列、第2引数はハッシュとして可変長に値が受け取れるため、呼び出し側で複数のキーやオプションをまとめて指定することが可能となっています。

def translate(*args, **opt)
  puts args.class # Array
  puts args # studio_ghibli.characters
  puts opt.class # Hash
  puts opt # {}
end

translate('studio_ghibli.characters')

先ほど、fetch関数に指定されていたstudio_ghibli.charactersに対応するymlファイルがありました。
中を見てみるとquotes(名言)の項目には「バルス」だったり「親方!空から女の子が!」が定義されていて、とても面白いですね。

lib/locales/ja/studio_ghibli.ym

ja:
  faker:
    studio_ghibli:
      quotes:
        - "親方!空から女の子が!"
        - "バルス"
        - "40秒で仕度しな!"

faker/studio_ghibli.yml at main · faker-ruby/faker · GitHub

I18n.translateについて

深掘ると長くなってしまうので、手短に紹介します。
この関数は与えられたkeyに合致した値の一覧を配列で返してくれます。

translate('studio_ghibli.characters')
# ["荻野 千尋", "ススワタリ", "湯婆婆"]

配列で値が返ってくるのは、i18n側で第1引数が配列で与えられた場合に配列を返すように実装されているからです。

# i18nのtranslate関数
def translate(key = nil, throw: false, raise: false, locale: nil, **options) # TODO deprecate :raise
  :
  if key.is_a?(Array)
    key.map do |k|
      translate_key(k, throw, raise, locale, backend, options)
    end
  else
    translate_key(key, throw, raise, locale, backend, options)
  end
end
alias :t :translate

sample関数とその後

sample関数は名前の通り、取得した値の一覧からランダムに値を1件取得してくれる関数です。

sample(["荻野 千尋", "ススワタリ", "湯婆婆"])
# "ススワタリ"

値の取得後、正規表現を使った条件によって処理が分岐します。この条件は値が/で始まり、/で終わっている時に真となります。

fetched&.match(%r{^/}) && fetched&.match(%r{/$})

# /foo/
# /foo/hoge/

条件に合致しなければ、取得した値(fetched)がそのまま返されてfetch関数の処理は終わりです。
これでfakerを使って値が取得されるまでの処理の流れを追い切ることができました。

まとめ

  • faker.rbには多くのクラスが継承するBaseクラスが定義されている
  • データ名を表すクラスはBaseクラスを継承しており、Baseクラスに定義された関数を呼び出している
  • ファイル構成に規則性があるため、新たなデータの追加が簡単
  • 内部ではi18nを使っており、値をymlファイルに定義している
  • ymlファイルで値を管理しているため、値の追加・削除が簡単

構造や処理がシンプルながらも拡張性を確保するために上手く作られており、とても勉強になりました。
fakerのように共通する処理を持つ多種多様なクラスを定義する必要がある場合・大量の値を定義する必要がある場合にはfakerの実装が参考になりそうです。

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