はじめに
Amazon: https://www.amazon.co.jp/dp/4048678841
第6章から第11章まではリファクタリングの具体的なテクニックがまとめられています。
今回は リファクタリング: Rubyエディション
の 第10章 メソッド呼び出しの単純化
で紹介されているいくつかのテクニックについて、まとめました。
※ 以下のサンプルコードは書籍で紹介されているコードを参考に、自分で作成したコードを載せています。(著作権を侵害しないように)
テクニック一覧
- Rename Method
- Remove Parameter
- Replace Parameter with Explicit Methods
- Preserve Whole Object
- Replace Parameter with Method
- Introduce Parameter Object
- Remove Setting Method
- Hide Method
テクニック詳細
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 Gateway や Introduce Expression Builder など、また別の機会に紹介できたらと思います。