はじめに
Amazon: https://www.amazon.co.jp/dp/4048678841
第6章から第11章まではリファクタリングの具体的なテクニックがまとめられています。
今回は リファクタリング: Rubyエディション
の 第6章 メソッドの構成方法
の中のいくつかのテクニックについて、まとめました。
※ 以下のサンプルコードは書籍で紹介されているコードを参考に、自分で作成したコードを載せています。(著作権を侵害しないように)
具体例一覧
- Extract Method
- Inline Method
- Replace Temp with Query
- Replace Temp with Chain
- Replace Method with Method Object
- Substitute Algorithm
- Replace Loop with Collection Closure Method
- Extract Surrounding Method
- Introduce Class Annotation
- Introduce Named Parameter
- Replace Dynamic Receptor with Dynamic Method Definition
具体例詳細
Extract Method
要約
複数の場所で同じ構造のコードがあるとき、対象のコードを抽出して、目的を的確に表現する名前をつける
例
# リファクタリング前
def print_boading_ticket(flight_number)
print_personal_info
puts "seats_number: #{@seats_number}"
puts "flight_number: #{flight_number}"
end
# リファクタリング後
def print_boading_ticket(flight_number)
print_personal_info
print_flight_info flight_number
end
def print_fligth_info
puts "seats_number: #{@seats_number}"
puts "flight number: #{flight_number}"
end
説明
- ソースメソッドから重複するコードを抽出することで、メソッドの粒度を細かく分けることができる
- メソッドの粒度が細かいと、再利用できる可能性が高まる
- 細かい粒度のメソッドを多数抽出し、短く適切な名前がつけることで、高水準なメソッドの可読性も上がる
- メソッドの粒度が細かければ、オーバライドもしやすい
備考
- メソッド名の長さより、メソッド名とメソッドの中身のギャップを重視する。
- メソッド名が長くても、メソッドの意味を適切に表現しているのであれば、問題ない。
参照場所
リファクタリング: Rubyエディション
P128 ~ P134
Inline Method
要約
メソッドの本体がメソッド名と同じくらいわかりやすいときは、メソッドの本体を呼び出し元に組み込んで、メソッドを削除する
例
# リファクタリング前
def get_point
more_than_three_orders ? 2 : 1
end
def more_then_three_orders
@number_of_orders > 3
end
# リファクタリング後
def get_point
@number_of_orders > 3 ? 2 : 1
end
説明
- 「メソッド抽出」など1つのものを2つに分解する度に、管理する場所が増えてしまう
- メソッド本体がメソッド名と同じくらいわかりやすいのであれば、分解しておく意味がない
- メンテナンスコストだけが高くなってしまう
参照場所
リファクタリング: Rubyエディション
P134 ~ P135
Replace Temp with Query
要約
式の結果を一時変数に保存しているときは、式をメソッドにし、一時変数の参照部分をメソッドに置き換える
例
# リファクタリング前
base_price = @entrance_fee * @visitors.count
if (base_price > 10000)
base_price * 0.80
else
base_price * 0.90
end
# リファクタリング後
if (base_price > 10000)
base_price * 0.80
else
base_price * 0.90
end
def base_price
@entrance_fee * @visitors.count
end
説明
- 一時変数は使用されているメソッド内でしか参照できないため、一時変数にアクセスするとメソッドが長くなる
- 一時変数を問い合わせメソッドに置き換えれば、クラス内の全てのメソッドがアクセスできるようになる
- ローカル変数は「メソッドの抽出」を妨げるので、一時変数は問い合わせメソッドに変更しておく
備考
- 一時変数は、ループ処理の結果を格納するために使われることが多い
- ループごとメソッドに抽出することで、まとまった行のコードを取り除ける
参照場所
リファクタリング: Rubyエディション
P137 ~ 140
Replace Temp with Chain
要約
一時変数を使って式の結果を保存しているときは、メソッドチェーンで書き換えて、一時変数を不要にする
例
# リファクタリング前
mock = Mock.new()
expectation = mock.receive(:method_name)
expectation.with("args")
expectation.and_return("result")
# リファクタリング後
mock = Mock.new()
mock.receive(:method_name).with("args").and_return("result")
説明
- メソッド呼び出しを連鎖的に行うことで、流れるように読みやすいコードを書くことができる
- メソッドチェーンで書き換えることで、不要な一時変数を削除することができる場合がある
参照場所
リファクタリング: Rubyエディション
P140 ~ 143
Replace Method with Method Object
要約
ローカル変数によって「メソッドの抽出」ができないとき、メソッドを独自のオブジェクトに変更し、すべてのローカル変数をそのオブジェクトのインスタンス変数にする
例
# リファクタリング前
class Rental
def fee(input_val)
primary_base_fee = input_val * 0.7
secondary_base_fee = input_val * 0.9
tertiary_base_fee = input_val
if primary_base_fee - secondary_base_fee > 200
tertiary_base_fee -= 200
end
end
end
# リファクタリング後
class Rental
def fee(input_val)
Fee.new(input_val).calculate
end
end
class Fee
attr_reader :input_val,
:primary_base_fee,
:secondary_base_fee,
:tertiary_base_fee
def calculate
primary_base_fee = input_val * 0.7
secondary_base_fee = input_val * 0.9
tertiary_base_fee = input_val
if primary_base_fee - secondary_base_fee > 200
tertiary_base_fee -= 200
end
end
end
説明
- ローカル変数によって「メソッドの抽出」ができないときがある
- 「一時変数から問い合わせメソッドへ」を使っても、メソッドの分解が難しくなることもある
- 上記のときは、メソッドをオブジェクトとして切り出すとよい
- ローカル変数はメソッドオブジェクトの属性となり、そこで「メソッドの抽出」が可能になる
参照場所
リファクタリング: Rubyエディション
P153 ~ 156
Substitute Algorithm
要約
複雑なメソッドを分解して単純な部品にした後に、より単純なアルゴリズムで書き換える
例
# リファクタリング前
def find_relatives(people)
relatives = []
people.each do |person|
if(person == "Ichiro")
relatives << person
end
if(person == "Jiro")
relatives << person
end
if(person == "Saburo")
relatives << person
end
end
return relatives
end
# リファクタリング後
def find_relatives(people)
people.select do |person|
%w(Ichiro Jiro Saburo).include? person
end
end
説明
- 問題の解決方法は1つではなく、他より簡単な方法が必ずある
- リファクタリングで複雑なメソッドを単純な部品に分解していくには限界がある
- アルゴリズム全体を書き換えることでより単純な部品にできるときもある
参照場所
リファクタリング: Rubyエディション
P156 ~ 157
Replace Loop with Collection Closure Method
要約
コレクションの要素をループで処理しているときは、コレクションクロージャメソッドを使用する
例
# リファクタリング前
women = []
people.each do |person|
women << person if person.female?
end
# リファクタリング後
women = people.select { |person| person.female? }
説明
- コレクションクロージャメソッドを使えば、ループ処理を簡単にできる
- 例: select, reject, map, collect
- コードも少なくなるし、可読性も上がる
参照場所
リファクタリング: Rubyエディション
P158 ~ 160
Extract Surrounding Method
要約
ほぼ同じコードのメソッドが複数あり、違いがメソッドの中頃にあるときは、重複部分を抽出して、ブロック付きメソッドにする
例
# リファクタリング前
def number_of_living_pets
pets.inject(0) do |count, pet|
count += 1 if pet.alive?
end
end
def number_of_pets_named(name)
pets.inject(0) do |count, pet|
count += 1 if pets.name == name
end
end
# リファクタリング後
def number_of_living_pets
count_pets_matching { |pet| pet.alive? }
end
def number_of_pets_named(name)
count_pets_metching { |pet| pet.name == name }
end
protected
def count_pets_matching(&block)
pets.inject(0) do |count, pet|
count += 1 if yield pet
end
end
説明
- ほぼ同じメソッド内の中央にユニークなコードがあるときに適用する
- 重複する処理はメソッドとして抽出し、異なる処理は Ruby のブロックを使って引数として渡すようにする
参照場所
リファクタリング: Rubyエディション
P160 ~ 164
Introduce Class Annotation
要約
実装手順がごく一般的で、安全に隠蔽できるメソッドがあるときは、クラス定義からクラスメソッドを呼び出してふるまいを宣言する
例
# リファクタリング前
class Person
def initialize(params)
@age = params[:age]
@weight = params[:weight]
@height = params[:height]
end
end
# リファクタリング後
module CustomInitializers
def hash_initializer(*attribute_names)
define_method(:initialize) do |*args|
data = args.first || {}
attribute_names.each do |attribute_name|
instance_variable_set "@#{attribute_name}", data[attribute_name]
end
end
end
end
Class.send :include, CustomInitializers # 全クラスで使用できるように Class クラスに include
class Person
hash_initializer :age, :weight, :height
end
説明
- 属性アクセッサの実装は非常に単純なので、クラスアノテーションに置き換えることができる
- 宣言的な構文でコードの目的が明確に掴めるときは、クラスアノテーションを導入することで、コードの意図を明確にできる
参照場所
リファクタリング: Rubyエディション
P164 ~ 166
Introduce Named Parameter
要約
メソッドの名前から引数の意味が推測できないときは、引数をハッシュで渡すようにして、ハッシュキーを引数の名前として使うようにする
例
# リファクタリング前
class Person
attr_reader :age, :weight, :height
def initialize(age, weight, height)
@age = age
@weight = weight
@height = height
end
end
Person.new(20, 60, 160)
# リファクタリング後
class Person
def initialize(params)
@age = params[:age]
@weight = params[:weight]
@height = params[:height]
end
end
Person.new(age: 20, weight: 60, height: 160)
説明
- 処理を委譲されているオブジェクトのメソッド名と引数の役割がうまく表現できていないときに適用する
- メソッド名と引数の役割がわからなければ、オブジェクトの実装を確認するために行ったり来たりしなければならない
- 引数に
Hash
を使えば、引数として何を渡しているか明確になり、メソッドの中身を推測しやすくなる
参照場所
リファクタリング: Rubyエディション
P166 ~ 171
Replace Dynamic Receptor with Dynamic Method Definition
要約
method_missing を使わずに、動的メソッド定義を使って必要なメソッドを定義する
例
# リファクタリング前
class Staff
def initialize(user)
@user = user
end
def method_missing(method_name, *args)
@user.send(method_name, *args)
end
end
# リファクタリング後
class Staff
def initialize(user)
user.public_methods(false).each do |method_name|
(class << self; self; end).class_eval do
define_method(method_name) do |*args|
user.send(method_name, *args)
end
end
end
end
end
説明
- method_missing を使った実装は、デバックが困難になりがちである(以下参考)
# 例のリファクタリング前の実装の場合
user.full_name = "test test"
staff = Staff.new(user)
# full_name を fullname とタイポすると...
staff.fullname
NoMethodError: undefined method 'fullname' for #<User:0x00007f904c973888>
# ↑メソッド呼び出しは Staff に対して行なっているのに、エラーを起こしているのは User になる
- 動的メソッド定義を使えば、method_missing を使用せずとも、同じようなふるまいを実現でき、デバックも容易になる
# 例のリファクタリング後の実装の場合
user.full_name = "test test"
staff = Staff.new(user)
# full_name を fullname とタイポすると...
staff.fullname
NoMethodError: undefined method 'fullname' for #<Staff:0x00007fbde9a1c9e0>
# ↑メソッド呼び出しが Staff に対して行なわれると、エラーを起こしすのも Staff になる
参照場所
リファクタリング: Rubyエディション
P182 ~ 185