Ruby
リファクタリング

リファクタリング: Ruby エディション 第10章

はじめに

Amazon: https://www.amazon.co.jp/dp/4048678841

第6章から第11章まではリファクタリングの具体的なテクニックがまとめられています。
今回は リファクタリング: Rubyエディション第10章 メソッド呼び出しの単純化 で紹介されているいくつかのテクニックについて、まとめました。

※ 以下のサンプルコードは書籍で紹介されているコードを参考に、自分で作成したコードを載せています。(著作権を侵害しないように)

テクニック一覧

テクニック詳細

Rename Method

メソッド名からメソッドの目的や意図がわからないときに、メソッド名を変更するテクニック

サンプル
# リファクタリング前
class Person
  attr_reader :age

  def more_then_18_years_old?
    age >= 18
  end
end

# リファクタリング後
class Person
  attr_reader :age

  def adult?
    age >= 18
  end
end

メソッド名は処理の内容ではなく、意図が伝わるように命名すべきです。コードを修正するのは人間なので、人間にとって読みやすいコードにすることが大切です。

最初から正しく命名できるとは限りません。仕様が変わるにつれて、適切なメソッド名に変えることが必要になることもあります。そのときに、「たかが名前でしょ」と変更せずに放置してしまうことも、これまで何度も経験してきました。

しかし、この積み重ねがどんどん開発スピードを遅くしていってしまうことにつながりかねません。効率よく開発できるように、メソッド名がわかりづらいと感じたら、後回しにせずに名前を変更するべきなのだと思いました。

Remove Parameter

メソッドが引数を使わなくなったときに、その引数を削除するテクニック

サンプル
# リファクタリング前
class Booking
  BASE_PRICE = 3000

  def price(room_type, service_fee)
    case room_type
      when :economy
        BASE_PRICE + service_fee
      when :standard
        BASE_PRICE * 1.2 + service_fee
      when :sweet
        BASE_PRICE * 3.0 + service_fee
    end
  end
end


# リファクタリング後
class Booking
  attr_accessor :room

  def price(service_fee)
    room.price + service_fee
  end
end

class Room
  attr_accessor :room_type

  BASE_PRICE = 3000

  def price
    case room_type
      when :economy
        BASE_PRICE
      when :standard
        BASE_PRICE * 1.2
      when :sweet
        BASE_PRICE * 3.0
    end
  end
end

開発が進むと実装当初は必要としていた引数が、仕様の変更に伴い、不要になることは多いかと思います。メソッドにもクライアントコードにも余分な引数を残したままであれば、引数を消さなくても一応動きます。

しかし引き数を残したままだと、そのメソッドを使って実装しようとする開発者全員の仕事を増やすことになります。引数を削除することは簡単なリファクタリングなので、交換条件としてよくないといえます。

「引数を削除しなくても動くから。。。」といった悪魔のささやきには耳を貸さずに、しっかりとリファクタリングすることが大事だと思います。

Replace Parameter with Explicit Methods

引数によって異なるコードが実行するメソッドがあるとき、引数の値ごとに異なるメソッドを作成するテクニック

サンプル
# リファクタリング前
def set_value(name, value)
  if name == "weight"
    @weight = value
  elsif name == "height"
    @height == value
  end
end

# リファクタリング後
def height=(value)
  @height = value
end

def width=(value)
  @weight = value
end

条件分岐によって引数の値をテストし、異なる処理を実行しているときに適用すれば、条件分岐をなくし、シンプルな実装にすることができます。

また引数が必要なメソッドは、クラスのメソッドを確認するだけでなく、引数がどういった値を期待しているかまで確認しなければなりません。可能な限り引数を少なくする(なければない方がよい)という主張は納得がいきます。

Switch.set_state(false) # ← 実装1
Switch.turn_off         # ← 実装2
# 実装1よりも実装2の方がわかりやすいし、引き数を確認する必要もない

Preserve Whole Object

オブジェクトのいくつかの値を取り出し、それらを別々の引数としてメソッドに渡しているとき、オブジェクトをそのまま引数として渡すようにするテクニック

サンプル
# リファクタリング前
height = person.height
weight = person.weight
calculator.bmi(height, weight)

# リファクタリング後
calculator.bmi(person)

ひとつのオブジェクトに含まれる複数のデータを引数として渡しているメソッドを見つけたら、このテクニックを適用するチャンスだと言えます。

メソッドの引数が増えてしまった場合、そのメソッドを呼び出している箇所をすべて変更する手間が発生してしまいます。オブジェクトをそのまま渡しておけば、必要なデータが増えたとしてもメソッド修正だけで済みます。

Replace Parameter with Method

オブジェクトのあるメソッドの結果を別メソッドの引数として渡しているとき、引数を取り除き、前者のメソッドを後者のメソッド内で呼び出すようにするテクニック

サンプル
# リファクタリング前
base_price = @room_price * @stays
discount_rate = set_discount_rate
payment_price = discounted_price(base_price, disscount_rate)

# リファクタリング後
base_price = @room_price * @stays
payment_price = discounted_price(base_price)

引数は少なければ少ないほど良いので、引数に値を渡す以外の方法で値が取得できるなら、その方法で実装した方が良いです。

理由は引数があるとインターフェースが変わることで、大量のコードを書き換えなくてはならなくなる可能性が高まるからです。大量のコードを書き換えるのは苦痛ですし、効率良い開発の妨げとなってしまいますね。

Introduce Parameter Object

特定の引数のグループが一緒に渡させる傾向あるときに、それらをひとつのオブジェクトにまとめ、そのオブジェクトを引数として渡すようにするテクニック

サンプル
# リファクタリング前
class Booking
  def payment_price(base_price, discount_rate)
    @price = base_price - base_price * discount_rate
  end
end

# リファクタリング後
class Booking
  def payment_price(room)
    course.payment_price
  end
end

class Room
  attr_accessor :base_price, :discount_rate

  def initialize(base_price, discount_rate)
    @base_price = base_price
    @disccount_rate = discount_rate
  end

  def payment_price
    @base_price - @base_price * @discount_rate
  end
end

特定の引数のグループが一緒に渡される傾向があるとわかったときに、このテクニックを適用するチャンスです。グループとした引数が渡されている、全てのメソッドの引数を減らすことができます。

引数が減ることで確認すべき箇所も減りますし、効率の良い開発を続けることができます。

Remove Setting Method

作成時に設定した後には変更すべきでないフィールドがあるときに、設定メソッドをすべて削除するテクニック

サンプル
# リファクタリング前
class Booking
  attr_writter :amount

  def initialize(amount)
    @amount = amount
  end

  def amount=(value)
    @amount = amount
  end
end

# リファクタリング後
class Booking
  def initialize(amount)
    @amount = amount
  end
end

値を変更したくないフィールドがあるときは、設定メソッド(例: def amount=(value)) や attr_writter を削除することで、その意図を明白に表現することができます。

値を変更したくないのに設定メソッドを残したままだと、後で混乱を招きます。混乱によって開発スピードが遅くなることは避けたいですね。

Hide Method

メソッドが他のどのクラスからも使われていないとき、メソッドを非公開にするテクニック

サンプル
# なし

他のどのクラスからも使われていないメソッドが公開されているときは、非公開にして隠蔽することで、そのメソッドが他のクラスでは呼ばれていないことを明示することができます。

メソッドが使われている範囲が特定できれば、他のクラスで呼び出し部分を探すことはなくなるし、誤って他のクラスで値を変更することもなくなるはずです。

隠蔽できるメソッドは可能な限り隠蔽することで、より効率のよい開発ができるのだろうと思いました。

考察

今回紹介したテクニックに共通するのは、 いかに引数を減らすか ということだと思います。

メソッドが引数を持つということは、引数としてどういった値が期待されているかを確認しなければなりません。また引数が簡単に増減してしまうような設計(例: 引数が複数のデータを含むオブジェクトではない)をしてしまうと、メソッドが呼び出されている箇所を全て変更しなければなりません。

効率のよい開発を行うために、できる限り変更時のコストが低くなるように設計することが大事であると考えます。

余談

本当はもっと紹介したいテクニックがあったのですが、私の実力不足もあり、まとめることを断念しました。。。

Introduce GatewayIntroduce Expression Builder など、また別の機会に紹介できたらと思います。

関連