ジャバ・ザ・ハットリ
Published on

初心者でもカンタンにRailsの中身のコードをコードリーディングする方法

Authors
  • avatar
    ジャバ・ザ・ハットリ

ここで言う「Rails の中身のコード」というのは Rails を使った Rails アプリのコードのことではない。Rails そのもののコード。DHH が書いた Rails のコード。$ rails new AppName とかのコマンドが動く仕組みが書かれたコードのこと。
これって職場の同僚と英語で話しててもいっつもゴチャゴチャと説明が要る。Rails アプリのコードと Rails の中身のコードを区別してそれが一発で分かってもらえる表現があったら教えて欲しい。

既にご存知の方はたくさん居ると思うがその Rails の中身のコードというのが巨大でなかなかにレベルが高い。初心者では読むのも一苦労でそこが遠ざけてしまう原因にもなっている。それでも優れたコードをコードリーディングすることはエンジニアにとってとてもいい勉強になるのでオススメ。

いかにコードリーディングが重要かは、いろんなブログなんかで優秀なエンジニアの方々が再三強調されている。

例えば

まつもとゆきひろのハッカーズライフ:第 10 回 ソースを読もう (1/2) - ITmedia エンタープライズ

ハッカーとしての能力を身に着けるのに優れた方法は、実際にコードを書くことと、ほかの人の書いた優れたソースコードを読むことだと思います。特にコードを読むことは普段あまり強調されませんが、他人のソースコードはいろんな意味で知恵と知識の源です。考えてみれば、わたし自身も他人のソースコードをたくさん読んで学んだように思います。

プログラミング初心者歓迎!「エラーが出ました。どうすればいいですか?」から卒業するための基本と極意(解説動画付き) - Qiita

熟練したプログラマだと、「何かあったらコードを読む」を習慣づけている人も多いです。
gem やフレームワークのコードを読むのは初心者の人には敷居が高いかもしれないが、こうしたスキルものちのち必要になってくるはず。早い内から慣れておく方が良い。

ペアプログラミングして気がついた新人プログラマの成長を阻害する悪習 - Qiita

なぜ、フレームワークのコードを調べないのか、問うたところ、「むずかしそうだから。早く終わらせたいから」ということらしかった。確かにフレームワークは複雑化していたり、全体を把握するのが難しいところはある。
しかし、それを読んでいくなかで、言語機能やライブラリを理解したり、知識が広がっていく。
この心理的抵抗を取っ払いコードを読んでいくことで、問題が早く解決するという経験をさせることが重要だ。

そうそう。とにかく質のいいコードを読みましょう、と。

カンタンに Rails の中身のコードをコードリーディングする方法

今回は Rails モデルの Validation の中身がどのように実装されているかをコードリーディングする。これはまつもとゆきひろ氏も言うように「全体を通して読む必要はありません。面白そうなところをつまみ食いして」にならっている。全部読むとか無理だし、モデルの Validation だったら、ほぼみんな知っていて馴染みがあると考えて選んだ。

class User < ActiveRecord::Base
  validates :name, length: { maximum: 30 }
end

とした場合にどうやって name の長さが 30 未満だけを受け付けて、30 以上だとエラーを返しているのかを見る。

Rails をフォークして自分のアカウントに入れる

  1. Rails のリポジトリーのページへ行く
    GitHub - rails/rails: Ruby on Rails

  2. 右上にある Fork ボタンを押す。

image

3.すると自分のアカウントに Rails が入ってくる。

image
  1. クローンしてローカルに入れる。
    自分のアカウントに入れた Rails で左上にある緑のボタンを押す。
    [email protected]:/rails.git をコピー
image
    $ git clone [email protected]:\<自分のアカウント名\>/rails.git

とするとローカルに Rails が入ってくる。

  1. これから読むつもりのモジュールの README を確認しておく。できれば Issue なんかも確認しておくを尚よし。
    今回は Active Model
image

確認用の Rails プロジェクトを作る

  1. Rails new する
    $ rails new CodeReading
  1. 以下のようなフォルダ構成になっているはず
..
|+CodeReading/
|+rails/
  1. CodeReading/Gemfile を編集する
    pry で処理を止めて確認するので pry を入れる。
    rails は path を設定して先ほど入れたローカルの rails を参照するようにする。バージョンは最新版。
# Gemfile
source 'https://rubygems.org'

gem 'rails', path: '../rails'
gem 'pg'
gem 'pry-rails'
gem 'pry-doc'
gem 'pry-byebug'
gem 'byebug'
  1. bundle install する
    $ bundle install
  1. User モデルを作る
    $ rails g model User name:string
  1. User モデルを編集して Validation を入れる
    validates の前に binding.pry を入れて止まるようにしておく。

class User < ActiveRecord::Base

binding.pry
  validates :name, length: { maximum: 30 }
end

これで確認用のプロジェクトは完成。

コードを読む

  1. rails c を実行して、そこからテキトーな name の入った user を作成すると、binding.pry で止まる。
$ rails c
[1] pry(main)> u = User.new(name: 'sample name')

From: /app/models/user.rb @ line 4 :

    1: class User < ActiveRecord::Base
    2:
    3: binding.pry
 => 4:   validates :name, length: { maximum: 3 }
    5: end
  1. step で中に入ると、ローカルに入れた Rails の該当コードに行く。
[1] pry(User)> step

From: /rails/activemodel/lib/active_model/validations/validates.rb @ line 105 ActiveModel::Validations::ClassMethods#validates:

    104: def validates(*attributes)
 => 105:   defaults = attributes.extract_options!.dup
    106:   validations = defaults.slice!(*_validates_default_keys)
    107:
    108:   raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
    109:   raise ArgumentError, "You need to supply at least one validation" if validations.empty?
    110:
    111:   defaults[:attributes] = attributes
    112:
    113:   validations.each do |key, options|
    114:     next unless options
    115:     key = "#{key.to_s.camelize}Validator"
    116:
    117:     begin
    118:       validator = key.include?('::'.freeze) ? key.constantize : const_get(key)
    119:     rescue NameError
    120:       raise ArgumentError, "Unknown validator: '#{key}'"
    121:     end
    122:
    123:     validates_with(validator, defaults.merge(_parse_validates_options(options)))
    124:   end
    125: end

これで/rails/activemodel/lib/active_model/validations/validates.rb の 104 行目に validates が定義されていることが分かる。詳しく見るなら直接 validates.rb のファイルをエディタで開く。

104: def validates(*attributes)
105:   defaults = attributes.extract_options!.dup
106:   validations = defaults.slice!(*_validates_default_keys)

105 行目の処理は attributes に extract_options!をして、柔軟な引数の指定に対応できるようにしている。
例えば今回の attributes はこのようになっている。

[1] pry(User)> attributes
=> [:name, {:length=>{:maximum=>30}}]

これに extract_options!を実行すると{:length=>{:maximum=>30}}となる。
extract_options!の定義元に行くとソースコードはこれ。

class Hash
  # By default, only instances of Hash itself are extractable.
  # Subclasses of Hash may implement this method and return
  # true to declare themselves as extractable. If a Hash
  # is extractable, Array#extract_options! pops it from
  # the Array when it is the last element of the Array.
  def extractable_options?
    instance_of?(Hash)
  end
end

class Array
  # Extracts options from a set of arguments. Removes and returns the last
  # element in the array if it's a hash, otherwise returns a blank hash.
  #
  #   def options(*args)
  #     args.extract_options!
  #   end
  #
  #   options(1, 2)        # => {}
  #   options(1, 2, a: :b) # => {:a=>:b}
  def extract_options!
    if last.is_a?(Hash) && last.extractable_options?
      pop
    else
      {}
    end
  end
end

ここでやってることはとても単純。配列の最後の要素が is_a?(Hash)かつ instance_of?(Hash) なら、配列の最後の要素(Hash)を取り出す、そうでなければ空の Hash を返す。

そうして取り出したハッシュを dup して defaults に代入している。dup する理由は後で defaults を変更した際にそれが attributes に影響しないようにするため。

    105: defaults = attributes.extract\_options!.dup

106 行では slice!を使って validations とその条件式を分けている。

    106: validations = defaults.slice!(\*\_validates\_default\_keys)

_validates_default_keys の内容は下の方のコードで定義されている。

protected

  # When creating custom validators, it might be useful to be able to specify
  # additional default keys. This can be done by overwriting this method.
  def _validates_default_keys # :nodoc:
    [:if, :unless, :on, :allow_blank, :allow_nil , :strict]
  end

つまりご覧のように条件式が付いていたら分ける、ということだ。
今回の例でもし条件付きにして

    validates :name, length: { maximum: 3 }, unless: "name.nil?"

としていたら、attributes に条件が Hash になって入ってくる。
そして 106 で slice!すると

[1] pry(User)> validations
=> {:length=>{:maximum=>30}}
[2] pry(User)> defaults
=> {:unless=>"name.nil?"}

とみごとに validations と defaults にそれぞれが代入されて分かれる。

108,109 行では読んで字のごとく、attributes や validations が空だったら ArgumentError を raise している。

108: raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
109: raise ArgumentError, "You need to supply at least one validation" if validations.empty?

と、これをずっとやっていけばどうやって validate してるのか分かる訳だが、こんな解説をずらずらと blog に書いてもなんかウケなさそうな気がしてきたのでこの辺でやめとく。(実はもっと奥の方まで解説するつもりだったけど、キリがないし辞めたわ。普通にコードが読める人だっら解説なんてウザいだけだし)

とにかく言いたいのはこうして気になる箇所を pry で一旦止めてひとつひとつ定義元に遡ってみていけば、なにも分からないことなんてないよね、ということ。Rails はレベルが高い!とか言ってもやっぱり可読性が十分に考慮された素晴らしいコードなので意味不明なことはまずない。

ということで「初心者でもカンタンに Rails の中身のコードをコードリーディングする方法」でした。

関連記事