「良いエンジニアになるには良いコードをたくさん読むべし」と偉い人が言っていました。
しかし、良いコードとは何でしょうか。そして、それはどこにあるのでしょうか。
自分は「良いコード」とは「広く世界で使われているもの」を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
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のコードを読んでみると、自分の知らないメソッドや実装方法に出会うことができるので、非常に面白いです。
みなさんもぜひチャレンジしてみてください。