読みやすいコードとRubyらしいコード

株式会社Misoca

黒曜(@kokuyouwind)

$ whoami

  • 森 俊介 / 黒曜

  • @kokuyouwind

  • 株式会社Misoca

    • 名古屋の会社

    • 懇親会スポンサー

    • @mugi_uno のご縁で富山に

📢 宣伝

フルリモートで働けるRubyの会社です

読みやすいコードとRubyらしいコード

例えばコードレビュー

こう書いたほうが
読みやすくなる

んじゃないかな

こう直したほうが
Rubyっぽい書き方

になってよさそう

どんなコードが読みやすいんだろう?

Rubyらしいとなぜ良いんだろう?

この2つは関係しているのかな?

このふたつの関係を
考えてみよう

アジェンダ

  • 読みやすいコード」を考える

  • Rubyらしいコード」を考える

  • 読みやすさRubyらしさの関係を考える

  • まとめ

読みやすいコード」を
考える

🙋読んだことある人?

コードは理解しやすくなければいけない。

 

コードは他の人が最短時間で理解できるように書かなければいけない。

「リーダブルコード」 pp.2-3

他の人が
素早く理解できる
のが
読みやすいコード

理解するとは?

「コードを理解する」というのは、

変更を加えたり バグを見つけたりできる

という意味だ。

「リーダブルコード」 p.3

「コードを理解する」というのは、

変更を加えたり バグを見つけたりできる

という意味だ。

「リーダブルコード」 p.3

「コードを理解する」というのは、

変更を加えたり バグを見つけたりできる

という意味だ。

「リーダブルコード」 p.3

コードの意図(なにをしたいか)が読み取れる

「コードを理解する」というのは、

変更を加えたり バグを見つけたりできる

という意味だ。

「リーダブルコード」 p.3

コードの意図(やりたいこと)が読み取れる

「コードを理解する」というのは、

変更を加えたり バグを見つけたりできる

という意味だ。

「リーダブルコード」 p.3

コードの意図(やりたいこと)が読み取れる

コードの挙動(どう動くか)が読み取れる

コードの意図と挙動が
読み手に伝わる
のが
読みやすいコード

第I部 表面上の改善

  • 良い名前で意図を伝える

    • ​getPageではなくfetchPage

  • 意図が誤解されない名前を使う

    • ​filterではなくselect, exclude

  • 適切なコメントで意図を伝える

第II部 ループとロジックの単純化

  • 制御フローを簡潔にする

    • 挙動を追いやすくする

  • 巨大な式を説明変数や要約変数で分割する

    • 挙動を追いやすくする

    • 部分式の意図を伝える

第III部 コードの再構成

  • 無関係の下位問題を抽出する

    • ​重要な挙動に注目できる

    • レベルが揃い意図を伝えやすくなる

  • ​一度に1つのことを

    • 挙動意図を(ry

「読みやすいコード」を考える

  • 意図挙動が伝わるのが読みやすいコード

    • なにがやりたいのか

    • どう動くのか

  • 詳しくは​リーダブルコードを読もう!

    • ​細かいテクニックがいろいろ載っている

アジェンダ

  • 読みやすいコード」を考える

  • Rubyらしいコード」を考える

  • 読みやすさRubyらしさの関係を考える

  • まとめ

Rubyらしいコード」を
考える

Ruby-ish

Rubyらしいってどういうこと?

Ruby らしい」とはどういうことでしょうか。

「○○らしい」とは「他と似ている」ということです。

Ruby らしい」書き方だとまわりのコードと似たような記述になります。

「ライブラリー開発者になろう」 

Rubyでよく使われる
書き方と似ている

Rubyらしいコード

※「Rubyらしさ」の定義としては微妙だが、
 「Rubyらしいコード」であるかの基準として有用

アジェンダ

  • 読みやすいコード」を考える

  • Rubyらしいコード」を考える

  • 読みやすさRubyらしさの関係を考える

  • まとめ

読みやすさ
Rubyらしさ
関係を考える

Rubyらしいコード例:
ブロックを使う

ブロックを使う

# これよりも
file = File.open(path)
file.read
file.close

# こっちのほうがRubyらしい
File.open(path) do |file|
  file.read
end

ブロックを使う

# これよりも
file = File.open(path) # 前処理
file.read                       # やりたいこと
file.close                      # 後処理

# こっちのほうがRubyらしい
File.open(path) do |file|
  file.read # やりたいこと
end

ブロックを使う

  • 前処理・後処理を分離できる

    • ブロックの中に意図が集まる

    • 「無関係の下位問題を抽出する」​

  • 変数のスコープが短くなる

    • 挙動を追いやすくなる

Rubyらしいコード例:
for文を使わない

for文を使わない

# これよりも
for i in 1..10 do
  print i
end

# こっちのほうがRubyらしい(?)
(1..10).each do |i|
  print i
end

for文を使わない

# これよりも
for i in 1..10 do
  print 'hello'
end

# こっちのほうがRubyらしい!
10.times do
  print 'hello'
end

for文を使わない

# これよりも
arr = [1,2,3]
for i in 0...arr.length do
  arr[i] = arr[i] ** 2
end

# こっちのほうがRubyらしい!
arr.map do |i|
  i ** 2
end

for文を使わない

# これよりも
sum = 0
for i in 1..10 do
  sum += i
end

# こっちのほうがRubyらしい
(1..10).reduce(0) do |sum, i|
  sum + i
end

for文を使わない

  • for文はいろんな用途に使えてしまう

    • 意図が伝えづらい

    • 複雑なことをすると挙動も追いづらい

  • なるべくコレクションメソッドを使う

    • 名前から意図が伝わる

    • 定形処理を任せるので挙動も追いやすい

Rubyらしいコード例:
メタプログラミング

メタプログラミング

# これよりも
class Test
  def field1; @field1; end
  def field2; @field2; end
end

# こっちのほうがRubyらしい
class Test
  attr_reader :field1, :field2
end

メタプログラミング

# これよりも……
class Test
  def set_status_hoge
    @status = :hoge
  end

  def set_status_fuga
    @status = :fuga
  end
end

メタプログラミング

# こっちのほうがRubyらしい?
class Test
  status_setter :hoge, :fuga

  def status_setter(*statuses)
    statuses.each do |status|
      define_method "set_status_#{status}" do
        @status = status
      end
    end
  end
end

メタプログラミング

  • 似た処理を柔軟に共通化し、名前を付けられる

    • 意図をより明確にできる

    • 挙動追いにくくなる(!)

  • 挙動が信頼できると安心して使える

    • コアやフレームワーク

    • 自前で書くときは読みやすさに注意する

「Rubyらしいコード」を考える

  • Rubyの言語機能や標準メソッドを活用した
    読みやすいコードRubyらしいコード

    • ブロックで下位問題を追い出す

    • 意図に合わせた名前のメソッドを使う

  • メタプログラミングは意図を伝えやすいが
    挙動が追いづらくなる諸刃の剣

余談: メタプロ社内事例

  • YAMLファイルから読み込んだ値で定数を定義

    • 知らないと定義にたどり着けない

  • リファクタ時に利用箇所がgrep漏れ

  • Hogeと相互変換する際に必要なメソッドを
    include Convertible[Hoge]にする提案

    • 意図はわかるが挙動が難しいとrejectされた

アジェンダ

  • 読みやすいコード」を考える

  • Rubyらしいコード」を考える

  • 読みやすさRubyらしさの関係を考える

  • まとめ

まとめ

  • 意図挙動が伝わるのが読みやすいコード

    • ​リーダブルコードは実現のテクニック集

  • Rubyの機能を活かした読みやすいコード
    Rubyらしいコード

    • ​よく使われるイディオムには
      読みやすくする要素が詰まっている

読みやすくRubyらしい
コードを書こう!

予備スライド
(練習したら全然収まらなかった)

アジェンダ

  • 「読みやすいコード」を考える

  • 「Rubyらしいコード」を考える

  • 実例から読みやすさを考える

  • まとめ

Misocaの文書変換

見積書からは変換できる
見積書への変換はできない

請求書と納品書は
相互に変換できる

Misocaの文書変換

変換元文書種別 変換先文書種別
変換元文書ID 変換先文書ID

文書変換テーブル

Misocaの文書変換

class DocumentConversion < ActiveRecord::Base
  def self.source_document_of(document)
    find_by(converted_document: document)
  end
  
  def self.converted_document_of(document)
    find_by(source_document: document)
  end
end

class Invoice < ActiveRecord::Base
  # 請求書クラスは文書変換について知らない
end

問題点

  • 各文書から「変換元文書」「変換先文書」に関連を持っていない

    • includesが使えずN+1問題が発生する

    • 挙動が追いづらい

  • 「見積書へは変換できない」制約がコードで表現されていない

案1. 普通に関連を持たせる

class DocumentConversion < ActiveRecord::Base
  belongs_to :source_document, polymorphic: true
  belongs_to :converted_document, polymorphic: true
end

class Invoice < ActiveRecord::Base
  # 中間テーブル(文書変換)への関連
  has_many :document_conversions, as: :converted_document
  # 変換元の見積書への関連
  as_many :source_estimates, through: :document_conversions, 
        source: :source_document, 
        source_type: 'Estimate'
  
  # 変換先の納品書への関連も同じ感じ
end

案1の改善点

  • 変換できない文書間では関連を持たない

    • 制約がコードに反映されている

  • Railsモデルの標準的な書き方

    • 挙動がわかりやすい

案1の問題点

  • 各文書のクラス内に「文書変換」の知識が
    ​分散している

    • 各文書を見比べないと、
      見積書に変換できない意図が読み取れない

  • 他の処理とまぎれて扱いづらい

案2. モジュールにまとめる

class Invoice < ActiveRecord::Base
  # 文書変換できることを明示するが、詳細な実装はモジュールに隠す
  include DocumentConvertible::Invoice
end

module DocumentConvertible
  module Invoice
    has_many :document_conversions, as: :converted_document
    as_many :source_estimates, through: :document_conversions, 
        source: :source_document, 
        source_type: 'Estimate'
  end
  module Estimate; ...; end
  module DeliverySlip; ...; end
end

案2の改善点

  • 文書変換の知識がモジュールに集まった

    • モジュールを見れば意図が読み取れる

  • Rubyの標準的なクラス分割方法

    • 挙動がわかりやすい

案2の問題点

  • 文書の定義側に「変換できる文書」の知識が
    ​現れていない

    • 文書だけを見ると意図が少しわかりにくい

  • 関連定義で似たコードが繰り返す

    • 同じ処理である、という意図を表したい

案3. メタプロで宣言的にする

class Invoice < ActiveRecord::Base
  # 見積書から変換できる
  include DocumentConvertible::From[Estimate]
  # 納品書に変換できる
  include DocumentConvertible::To[DeliverySlip]
end

module DocumentConvertible
  module From
    module_function
    def [](*class_names)
      # 受け取ったクラスを変換元文書として参照するために
      # 必要な関連・メソッドなどを定義する
    end
  end
  # module To も同様に定義

案3の改善点

  • 各文書のモデルに制約が明確に書かれている

    • 意図が非常にわかりやすい

  • クラス名から関連・メソッドが定義される

    • 各文書で似た定義にしている意図が明確

案3の問題点

  • 定義される関連・メソッドが分かりづらい

    • 挙動が追いづらい

    • IDEの補完やコードジャンプが効かない

意見を集めた結果

  • 案2の「モジュールに集約」がわかりやすい
    という意見が多数だった

    • ​文書変換の知識が1モジュールに集約

    • 多少のコード重複はあるが追いやすい

  • 案3の「メタプログラミング」は、
    制約がわかりやすいが挙動が追いにくい

意図を表現するために
挙動のわかりやすさを
犠牲にするべきではない