ベルリンのITスタートアップで働くジャバ・ザ・ハットリの日記

日本→シンガポール→ベルリンへと流れ着いたソフトウェアエンジニアのブログ

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

移転しました。

ここで言う「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ボタンを押す。
f:id:tango_ruby:20160628224245p:plain:W400

3.すると自分のアカウントにRailsが入ってくる。
f:id:tango_ruby:20160706130020j:plain:W300


4. クローンしてローカルに入れる。
自分のアカウントに入れたRailsで左上にある緑のボタンを押す。
git@github.com:<自分のアカウント名>/rails.gitをコピー
f:id:tango_ruby:20160701184627p:plain:W400

$ git clone git@github.com:<自分のアカウント名>/rails.git

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

5. これから読むつもりのモジュールのREADMEを確認しておく。できればIssueなんかも確認しておくを尚よし。
今回はActive Model
f:id:tango_ruby:20160701205554p:plain:W300

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

1. Rails new する

$ rails new CodeReading

2. 以下のようなフォルダ構成になっているはず

..
|+CodeReading/
|+rails/

3. 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'

4. bundle installする

$ bundle install

5. Userモデルを作る

$ rails g model User name:string

6. 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

2. 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の中身のコードをコードリーディングする方法」でした。

tango-ruby.hatenablog.com

tango-ruby.hatenablog.com

tango-ruby.hatenablog.com

tango-ruby.hatenablog.com

tango-ruby.hatenablog.com