Rubyでのリファクタリング方法

はじめに

「リファクタリングRubyエディション」を読んで、Rubyでのリファクタリングの基礎をまとめてみました。

リファクタリングとは

リファクタリングとは、コードの外から見た振る舞いを変えずに、内部構造を改良するようにして、ソフトウェアシステムを変えていくプロセスのことです。

紹介するリファクタリング方法

  1. メソッドの抽出(Extract Method)
  2. メソッドのインライン化(Inline Method)
  3. 一時変数のインライン化(Inline Temp)
  4. 一時変数から問合せメソッドへ(Replace Temp with Query)

リファクタリングの方法は他にもたくさんありますが、今回は基礎的かつ、実際にすぐに取り入れやすい上記4つをリファクリングRubyから要約してみたいと思います。

実行環境

この記事は以下の動作環境で動作確認しています。

  • Rails: 6.0.3
  • Ruby:2.7.1
  • Macbook Pro

メソッドの抽出(Extract Method)

肥大化したメソッドから細かい粒度でメソッドを切り出すことで、スッキリとした読みやすいコードにする。

def print_owing(amount)
  outstanding = 0.0

  # バナーを出力 (print banner)
  puts "*************************"
  puts "***** Customer Owes *****"
  puts "*************************"
  
  # 勘定を計算 (calculate outstanding)
  @orders.each do |order|
    outstanding += order.amount
  end
   
  # 詳細を表示 (print details)
  puts "name: #{@name}"
  puts "amount: #{amount}"
end

上記メソッドを「メソッドの抽出」を用いてリファクタリングすると以下のようになる。

def print_owing(amount)
  print_banner
  outstanding = calculate_outstanding
  print_details(outstanding)
end

def print_banner
  puts "*************************"
  puts "***** Customer Owes *****"
  puts "*************************"
end

def calculate_outstanding
  outstanding = 0.0
  @orders.each do |order|
    outstanding += order.amount
end
  outstanding
end

def print_details(outstanding)
  puts "name: #{@name}"
  puts "amount: #{amount}"
end

理由

メソッドの粒度が細かくできていれば、他のメソッドがそのメソッドを使える可能性が高くなる。

メソッドを多数用意すると高水準のメソッドがコメントの連続のような感じで読めるようになる。

ポイント

「メソッドの抽出」は長すぎるメソッドや、目的を理解するためにコメントが必要なコードに使うと効果を発揮する。

抽出したいコードが、1個のメッセージや関数の呼び出しなど、非常に単純な場合でも、抽出するメソッド名がコードの意図をよりよく伝えられるなら、抽出を行うべきである。 ※ 意味のある名前が思いつかないなら、抽出するべきではない。

手順

  1. 新しいメソッドを作成し、メソッドの目的に基づいて名前をつける。
  2. ソースメソッドからターゲットメソッドに抽出しようとしているコードをコピーする。
  3. 抽出したコードの中で、ソースメソッドのローカルスコープの変数が参照されている箇所を探す。具体的にはソースメソッドのローカル変数、引数である。
  4. 抽出したコードの中だけで使われている一次変数があるかどうかをチェックする。ある場合には、ターゲットメソッド内で一次変数として宣言する。
  5. 抽出したコードがこれらのローカルスコープの変数を書き換えているかどうかをチェックする。書き換えている場合には、抽出したコードの問合せメソッドとして扱って、戻り値をその変数に代入できるかどうかをチェックする。
  6. 抽出したコードが読み出しているローカルスコープの変数をターゲットメソッドの引数として渡す。
  7. ソースメソッドの抽出されたコードの部分をターゲットメソッド呼び出しに置き換える。
  8. テストする。

メソッドのインライン化(Inline Method)

メソッドの本体を呼び出し、元の本体に組み込み、メソッドを削除する。

def get_rating
  more_than_five_late_deliveries ? 2 : 1
end

def more_than_five_late_deliveries
  @number_of_late_deliveries > 5
end

def get_rating
  @number_of_late_deliveries > 5 ? 2 : 1
end

理由

メソッド名と同じくらいコードが読みやすい場合、メソッドとして呼び出すより、コード自体を呼び出しても可読性は変わらないため。コードの記述量を減らすことができる。

ポイント

メソッドの本体が名前と同じくらいわかりやすい場合に、適応できる。

手順

  1. メソッドがポリモーフィックでないことをチェックする。(サブクラスがオーバーライドしているメソッドをインライン化してはならない。メソッドがなくなってしまったら、オーバーライドできなくなる。)
  2. そのメソッドに対する全ての呼び出しを探す。
  3. 個々の呼び出しを取り除き、メソッド本体のコードを埋め込んでいく。
  4. テストする。
  5. メソッドの定義を取り除く。

一時変数のインライン化(Inline Temp)

単純な式で1度だけ代入されている一次変数があり、その一次変数が他のリファクタリングの邪魔になっている。
その一次変数に対する全ての参照を取り除き、式にする。

base_price = an_order.base_price
return (base_price > 1000)

return (an_order.base_price > 1000)

理由

「一時変数のインライン化」はたいていの場合、「一時変数から問合せメソッドへ」の一部として使われるので、これを行う本当の理由は、そちらにある。

手順

  1. 一時変数を参照している全ての箇所を検索し、参照を消して代入文の右辺に置き換える。
  2. 変更を行うたびにテストを行う。
  3. 一時変数の宣言と代入文を取り除く。
  4. テストする。

一時変数から問合せメソッドへ(Replace Temp with Query)

一時変数を使って式の結果を保存している。
式をメソッドにする。一時変数の全ての参照箇所を式に置き換える。新しいメソッドは他のメソッドからも使える

base_price = @quentiry * @item_price

if (base_price > 1000)
  base_price * 0.95
else
  base_price * 0.98
end

if (base_price > 1000)
  base_price * 0.95
else
  base_price * 0.98
end

def base_price
  @quentiry * @item_price
end

理由

一時変数の問題点は、一時的でローカルだということにある。使われているメソッドのコンテキストの中でしか参照できないので、一時変数にアクセスするためにはメソッドを長くする以外に方法がなく、そのため一時変数はメソッドの長大化を助長してしまう。一時変数を問合せメソッドに置き換えれば、クラス内の全てのメソッドがその情報にアクセスできるようになる。これは、クラスのコードをクリーンにするために非常に役に立ちます。

手順

  1. 代入文の右辺を抽出してメソッドにする
    => メソッドは、最初は非公開とする。後で他の用途が見つかったら、保護属性はその時に
  2. テストをする
  3. 一時変数に対して「一時変数のインライン化」をかける。

まとめ

以上が基礎的な4つのリファクタリング方法です。
これはリファクタリングRubyエディションでも多く使われていて、実際の現場でも積極的に取り入れたいリファクタリング方法です。
もっといろいろなリファクタリング方法が知りたいという方はぜひ「リファクタリングRubyエディション」を読んでみてください。