【OSSコードリーディング入門】gimeiはどのように人名を返しているのか

「良いエンジニアになるには良いコードをたくさん読むべし」と偉い人が言っていました。
しかし、良いコードとは何でしょうか。そして、それはどこにあるのでしょうか。
自分は「良いコード」とは「広く世界で使われているもの」を1つの答えだと考えています。
つまり広く世界で使われているOSSのコードを読んでみることが、良いエンジニアになるための助けになります。
OSSのコードリーディングを通して、さまざまな設計・パターン・技法を学ぶことができます。

OSSのコードを読む心構え

とはいえ、いきなりOSSのコードを読むのは敷居が高いです。
自分が1年ほど、さまざまなOSSのコードを読んできた中で、感じた大切なことは4つあります。

  • 全てを理解しようとしないこと(膨大な量のコードを全て把握するのは作者でも難しい)
  • 適切なボリューム感を選択すること(巨大なコードベースを持つOSSを最初に選ぶと辛い)
  • 普段、触れている技術に近いOSSを選択すること
  • 可能な限り、使い慣れたものを選択すること

以上、4つを踏まえて、N2iと親和性のあるRubyで書かれたOSSのコードを読んでみたいと思います。 適切なボリュームかつ実際にN2iで使用しているgimeiというOSSを選択しました。

github.com

gimeiについて

OSSのコードを読む前に最低限の使い方を把握しておくのが良いです。
githubのREADME.mdを参考にしつつgimeiについて簡単に紹介しておきます。
gimeiは日本人の名前や、日本の住所をランダムに返すライブラリです。

gimei = Gimei.name
gimei.kanji          #=> "斎藤 陽菜"
gimei.hiragana       #=> "さいとう はるな"
gimei.katakana       #=> "サイトウ ハルナ"

ここでは Gimei.nameを最初に呼び出していることを覚えておいてください。

引用元: gimei/README.md at main · willnet/gimei · GitHub

いざコードを読んでみる

コードをgithubからクローンするかブラウザを使ってファイルを閲覧するでも何でもOKです。
コード内を何度も検索することになるので、個人的にはコードをクローンしてお気に入りのエディタで読み進めるのがおすすめです。

gemの場合、最初に開くべきファイルはlib/gimei.rbです。
なぜこのファイルなのかというと、gemはファイル構造が決まっておりlib直下にgem名と同名のファイルが必ず配置されています。 全ての処理はこのファイルから始まります。
他言語のOSSであっても、最初に開くべきファイルを把握しておくのが重要です。

require 'forwardable'
require 'yaml'
:

class Gimei
  extend Forwardable
  GENDERS = [:male, :female].freeze

  def_delegators :@name,
  :
end

gimei/lib/gimei.rb at main · willnet/gimei · GitHub

先ほどGimei.nameを最初に呼び出していたことを思い出してください。
このファイルではGimeiクラスが定義されています。Gimei.nameの呼び出しが可能ということはnameがクラスメソッドとして定義されているはずです。

class Gimei
  class << self
    def name(gender = nil)
      Name.new(gender)
    end
  end
end

ありました...。
nameが呼び出しされるとNameクラスのインスタンスを作成して返しています。
次はrequire 'gimei/name'を頼りにNameクラスの実装を見てみます。

Nameクラスの実装

Nameクラスのインスタンスを作成しているためinitializeが呼び出されます。
ここでは3つのインスタンス変数が定義されていることが分かります。@genderには引数に指定がなければ、性別(maleかfemale)がランダムで束縛されます。
残りの@firstにはFirstクラス、@lastにはLastクラスのインスタンスがそれぞれ束縛されています。

class Gimei::Name
  def initialize(gender = nil)
    @gender = gender || Gimei::GENDERS.sample(random: Gimei.config.rng)
    @first = First.new @gender
    @last = Last.new
  end
end

同じファイルにFirstとLastクラスが定義されています。
First(名前)では性別が重要になります。男性なら倫太郎、女性ならまゆりとかでしょうか。

class First
  def initialize(gender = nil)
    @gender = gender || Gimei::GENDERS.sample(random: Gimei.config.rng)
    @name = NameWord.new(Gimei.names['first_name'][@gender.to_s].sample(random: Gimei.config.rng))
  end
end

Last(氏名)では性別は関係ないので、Firstクラスと比べるとシンプルですね。

class Last
  def initialize
    @name = NameWord.new(Gimei.names['last_name'].sample(random: Gimei.config.rng))
  end
end

どちらのクラスも@nameにNameWordクラスのインスタンスを束縛しています。
インスタンス作成時にGimei.names['last_name']を指定しています。一体、どのような値が指定されているのでしょうか。

gimei/lib/gimei/name.rb at main · willnet/gimei · GitHub

Gimei.namesと人名一覧の読み込み

Gimei.namesが返す値を知るために、再びlib/gimei.rbのコードを見てみます。
標準ライブラリのyamlを利用して/data/names.ymlの中身を読み込んでいることが分かります。 Gimeiクラスではクラスメソッドとインスタンス変数を使ってシングルトンクラスを実装しています。
@names ||= ...と組み合わせることで状態を記録して、YAMLファイルの読み込みを、一度だけ行うように上手く作られていますね。

class Gimei
  class << self
    def names
      @names ||= YAML.load_file(File.expand_path(File.join('..', 'data', 'names.yml'), __FILE__))
    end
  end
end

🖱: クラスメソッドとインスタンス変数を使ったシングルトンクラスの実装例

class Sample
  attr_reader :state
  
  def self.set_variable(value)
    @state ||= value
  end
  
  def self.get_variable
    puts "state: #{@state}"
  end
end

Sample.get_variable
Sample.set_variable(1)
Sample.get_variable
Sample.set_variable(2)
Sample.get_variable

# state: 
# state: 1
# state: 1


names.ymlには以下のように人名の情報が「漢字、ひらがな、カタカナ」の順に記録されています。

first_name:
  male:
    - ['愛斗', 'あいと', 'アイト']
    - ['愛登', 'あいと', 'アイト']
  female:
    - ['阿愛', 'ああい', 'アアイ']
    - ['安唯', 'あい', 'アイ']
last_name:
  - ['佐藤', 'さとう', 'サトウ']
  - ['林', 'はやし', 'ハヤシ']

ymlファイルから読み込んだ値はハッシュとして扱えるため、以下のように人名の一覧を取得することが可能です。

irb(main):015:0> Gimei.names['first_name']['male'].class
=> Array
irb(main):016:0> Gimei.names['first_name']['male'].first
=> ["愛斗", "あいと", "アイト"]

NameWordのインスタンス作成時に指定していたNameWord.new(Gimei.names['last_name'].sampleの場合、人名の一覧からランダムに1件が選択されます。

gimei/lib/gimei.rb at main · willnet/gimei · GitHub

人名を返す

いよいよ最終段階です。
NameWordクラスでは指定された人名の情報(漢字、ひらがな、カタカナ)を配列として保持しており、取得したい情報に対応するメソッドが定義されています。

class NameWord
  def initialize(name)
    @name = name
  end

  def kanji
    @name[0]
  end

  def hiragana
    @name[1]
  end

  def katakana
    @name[2]
  end
  :
end

処理を遡り、Gimei.name.kanjiを実行して、人名の漢字表記を取得する場合の動きを見てみます。
Gimei::Nameクラスのインスタンス作成時に@first@lastが宣言されており、それぞれのインスタンス変数にはymlファイルから読み込んだ人名の情報がランダムに束縛されているのでした。

class Gimei::Name
  attr_reader :first, :last, :gender

  def initialize(gender = nil)
    @gender = gender || Gimei::GENDERS.sample(random: Gimei.config.rng)
    @first = First.new @gender
    @last = Last.new
  end

  def kanji
    "#{last.kanji} #{first.kanji}"
  end
end

gimei/lib/gimei/name.rb at main · willnet/gimei · GitHub

メソッドの移譲

kanjiメソッドでは@first@lastクラスのkanjiメソッドを呼び出しています。
しかしFirst、Lastクラスにはkanjiメソッドを定義している箇所はありません。どういうことなのでしょうか...。
ここでは「移譲」という方法が採用されています。メソッドの移譲がFirstクラスの2~3行目にてForwardableモジュールを使って設定されています。

class First
  extend Forwardable
  def_delegators :@name, :kanji, :hiragana, :katakana, :to_s, :romaji
end

docs.ruby-lang.org

Forwardableに定義されたdef_delegatorsを使用すると、別クラスのメソッドを代わりに呼び出すようになります。
ここでは@nameに束縛されているNameWordクラスのkanji, hiragana, katakana...が、それぞれ該当します。

class NameWord
  def kanji
    @name[0]
  end
  :
end

こうしてFirst、Lastクラスから人名の情報を取得することが可能となりました。
そして最後に、取得した氏名と名前を組み合わせて人名を返すというのがgimeiの内部で行われていることでした。

"#{last.kanji} #{first.kanji}"
# 岡部 倫太郎

まとめ

  • GimeiクラスからGimei::Nameクラスのインスタンスを作成する
  • Gimei::NameではFirstとLastそれぞれのインスタンスを保持している
  • インスタンス作成時、ymlファイルを読み込みランダムな情報を1件取得している
  • FirstとLastクラスはそれぞれNameWordクラスのインスタンスを保持している
  • FirstとLastクラスはメソッドをNameWordクラスに移譲して人名を返す

大量の情報は外部ファイルに置いておく、メソッドを移譲するなど、新たな学びがありました。
このようにOSSのコードを読んでみると、自分の知らないメソッドや実装方法に出会うことができるので、非常に面白いです。

みなさんもぜひチャレンジしてみてください。