はじめに
Amazon: https://www.amazon.co.jp/dp/4048678841
第6章から第11章まではリファクタリングの具体的なテクニックがまとめられています。
今回は リファクタリング: Rubyエディション
の 第9章 条件式の単純化
で紹介されているいくつかのテクニックについて、まとめました。
※ 以下のサンプルコードは書籍で紹介されているコードを参考に、自分で作成したコードを載せています。(著作権を侵害しないように)
テクニック一覧
テクニック詳細
Decompose Conditional
複雑な条件文があるときに、条件部と then 部、 else 部からメソッドを抽出するテクニック
# リファクタリング前
if WINTER_START < date < WINTER_END
price = base_price * @winter_rate + @winter_service_price
else
price = base_price * @summer_rate
end
# リファクタリング後
if is_winter?(date)
price = winter_price(base_price)
else
price = summer_price(base_price)
end
注意しておかないとプログラムに条件分岐を加えることで、あっという間にコードが長くなってしまいますよね。長いコードはそれだけで読みにくいですが、条件分岐が入ることでさらに可読性が低くなってしまいます。
問題は、どういう条件でどういった処理が行われるかはコードを見ればわかるかもしれませんが、なぜそういった処理を実行するのか、意図がぼやけてしまいやすいということです。
だからこそ、条件部や分岐先の処理を意図が伝わりやすいメソッドに置き換えることが大切だと感じました。
Introduce Null Object
nil 値チェックを繰り返し実行しているとき、 nil 値の代わりに null オブジェクトを導入するテクニック
# リファクタリング前
class Resident
attr_reader :name, :age
def initialize(params)
@name = params[:name]
@age = params[:age]
end
end
class Room
attr_reader :resident
def initialize(params)
@resident = params[:resident]
end
def resident_name
@resident ? @resident.name : "empty"
end
def resident_age
@resident ? @resident.age : "empty"
end
end
# リファクタリング後
class Resident
attr_reader :name, :age
def initialize(params)
@name = params[:name]
@age = params[:age]
end
def self.new_missing
MissingResident.new
end
end
class MissingResident
def name
"empty"
end
def age
"empty"
end
end
class Room
def initialize(params)
@resident = params[:resident]
end
def resident
@resident || Resident.new_missing
end
def resident_name
resident.name
end
def resident_age
resident.age
end
end
nil 値のチェックが繰り返し行われるような処理が増えてきた場合、nullオブジェクトの導入によるリファクタリングが有効とのことでした。
上記のサンプルでは、部屋(Room
)に住人(Resident
)がいない場合に nil を返すのではなく、存在しない住人(MissingResident
)というクラスのインスタンス(= nullオブジェクト)を返すようにしています。
新しく作成した null オブジェクトのクラス(サンプルでいうところの MissingResident
クラス)に null オブジェクトにも処理できるようにしたいメソッドだけを定義しています。このように実装することで、クライアントコードから nil 値チェックを無くし、スッキリさせることができます。
たしかにクライアントコードを綺麗にすることはできますが、メンテナンスコストが高くなるという印象を受けました。上記のサンプルでいえば、 Resident
クラスにメソッドが増えてきた場合、 MissingResident
の方も気にしながら実装しなければならなくなるような気がします。
Introduce Assertion
ある条件を前提としているコードがあるとき、アサーションを導入して前提条件を明確にするテクニック
# リファクタリング前
class Employee
def initialize(params)
@project = params[:project]
@salary = params[:salary]
end
def salary
# salary か project が必要
@salary ? @salary : @project.price
end
end
class Project
attr_reader :price
def initialize(price)
@price = price
end
end
# リファクタリング後
module Assertions
class AssertionsFailedError < StandardError; end
def assert(&condition)
raise AssertionsFailedError.new("Assertion Failed") unless condition.call
end
end
class Employee
include Assertions
def initialize(params)
@project = params[:project]
@salary = params[:salary]
end
def salary
assert { (!@salary.nil?) || (!@project.nil?) }
@salary ? @salary : @project.price
end
end
class Project
attr_reader :price
def initialize(price)
@price = price
end
end
特定の条件が前提となっているコードというのは私もよく見かけます。コメントで前提条件が書く場合もあるかと思います。しかし、コメントで書いたりするよりもアサーションを用いて前提条件を明確にした方がよいということが書かれています。
アサーションとは、常に真でなければならない条件のことを指し、アサーションが偽を返すということはプログラミングエラーを意味するので、必ず例外を生成するようにします。
これにより、前提条件がコードとして明確になるだけでなく、デバックも楽になります。コードの可読性が向上し、デバックがしやすくなることで、効率の良い開発に繋がるという長所があります。
考察
今回紹介したテクニックにもそれぞれ特徴があり、積極的に活用していけるものと、よく注意して活用すべきものに分けられると思います。
Decompose Conditional は前者であると考えていて、条件文や分岐先の処理を意図が伝わりやすいメソッドとすることは、開発者にとってメリットが多く、デメリットが少ないように感じます。
Introduce Null Object は後者であると考えていて、 null オブジェクトを導入することで多くのメリットを享受できる一方で、よく考えて導入しないとリファクタリング箇所が多すぎてバグを生み出してしまう可能性が高かったり、導入後のメンテナンスコストが高くなってしまうようにも感じます。
テクニックのメリット、デメリットを理解した上で活用していくことが大切だと思います。