はじめに
Amazon: https://www.amazon.co.jp/dp/4048678841
第6章から第11章まではリファクタリングの具体的なテクニックがまとめられています。
今回は リファクタリング: Rubyエディション
の 第11章 一般化の処理
で紹介されているいくつかのテクニックについて、まとめました。
※ 以下のサンプルコードは書籍で紹介されているコードを参考に、自分で作成したコードを載せています。(著作権を侵害しないように)
テクニック一覧
- Pull Up Method
- Push Down Method
- Extract Module
- Inline Module
- Extract Subclass
- Introduce Inheritance
- Collapse Hierarchy
テクニック詳細
Pull Up Method
どのサブクラスでも同じ結果になるメソッドがあるときに、メソッドをスーパークラスに移すテクニック
# リファクタリング前
class Room
end
class SingleRoom < Room
def base_price(date)
date * 3000
end
end
class SweetRoom < Room
def base_price(date)
date * 3000
end
end
# リファクタリング後
class Room
def base_price(date)
date * 3000
end
end
class SingleRoom < Room
end
class SweetRoom < Room
end
重複するメソッドがあると、片方を修正して、もう片方を修正し忘れるというリスクを抱えることになります。つまり、将来のバグの温床になる可能性が少なくありません。
同じクラスを継承するサブクラスに、同じ処理を実行するメソッドがあるときは、メソッドをスーパークラスに移動することで重複を取り除き、バグを産むリスクを低くしましょう。
Push Down Method
スーパークラスのメソッドが一部のサブクラスでしか使われていないとき、メソッドをサブクラスに移すテクニック
# リファクタリング前
class Employee
def bonus
monthly_salary
end
end
class PartTimeEmployee < Employee
end
class FullTimeEmployee < Employee
end
# リファクタリング後
class Employee
end
class PartTimeEmployee < Employee
end
class FullTimeEmployee < Employee
def bonus
monthly_salary
end
end
Pull Up Method とは逆のテクニックで、スーパークラスのメソッドが一部のサブクラスでしか使われていないときは、そのメソッドをサブクラスにさせます。
上記のサンプルだと、スーパークラスに bonus メソッドを定義していては、 PartTimeEmployee(アルバイト)
に対しても、ボーナスを支給する会社だと誤解を与えてしまいそうですね。
Extract Module
複数のクラスに重複するメソッドがあるときに、モジュールをインクルードするようにして、そのモジュールにメソッドを移すテクニック
# リファクタリング前
class Deposit
attr_accessor :account_id
def capture_account_id(account)
self.account_id = account.id
end
end
class Withdraw
attr_accessor :account_id
def capture_account_id(account)
self.account_id = account.id
end
end
# リファクタリング後
module AccountIdCapture
def capture_account_id(account)
self.account_id = account.id
end
end
class Deposit
include AccountIdCapture
attr_accessor :account_id
end
class Withdraw
include AccountIdCapture
attr_accessor :account_id
end
Pull Up Method で先述しましたが、メソッドの重複は将来のバグの温床になりかねません。このテクニックも、そういった重複を取り除くためのテクニックです。
Pull Up Method とは違い、サブクラス・スーパークラスの関係に限らず、複数のクラスでメソッドが重複してしまっているときに、適用するとよいテクニックです。
ただ気をつけなければならないのは、モジュールはグループとして意味を持つものでないといけないということです。モジュールが特定の意味をもつグループではなくなると、ただメソッドをかき集めた入れ物になってしまい、モジュールが膨れ上がり、どこに何のメソッドがあるかわからなくなってしまいます。
これではリファクタリングの目的である、効率の良い開発ができなくなってしまいますね。
Inline Module
モジュールがメソッドの重複を避ける役割を果たしていないときに、モジュールをインクルードしているクラスにメソッドを移すテクニック
# なし
これは Extract Module とは逆のテクニックになります。モジュールをインクルードするということは、メソッドを探すときに、クラスを見て、そこになければインクルードしている関連のありそうなモジュールを特定し、そのモジュールの中を見るという手間が発生してしまいます。
もしモジュールが重複を避けるという役割を果たしていなかったときは、手間が増えるだけになってしまいます。そういったときはモジュールをインクルードしているクラスに、モジュール内のメソッドを移して、効率良く開発できるようにしましょう。
Extract Subclass
クラスが一部のインスタンスしか使わないフィールドをもっているときに、サブクラスを作ってリファクタリングするテクニック
# リファクタリング前
class JobItem
attr_accessor :unit_price, :quantity, :is_development_tool, :engineer
def initialize(unit_price, quantity, is_development_tool, engineer)
@unit_price = unit_price
@quantity = quantity
@is_development_tool = is_development_tool
@engineer = engineer
end
def unit_price
is_development_tool? ? unit_price * @engineer.rate : @unit_price
end
def is_development_tool?; @is_development_tool end
end
# リファクタリング後
class JobItem
attr_accessor :unit_price, :quantity
def initialize(unit_price, quantity)
@unit_price = unit_price
@quantity = quantity
end
end
class DevelopmentItem < JobItem
attr_accessor :engineer
def initialize(unit_price, quantity, engineer)
super(unit_price, quantity)
@engineer = engineer
end
def unit_price
@unit_price * @engineer.rate
end
end
このテクニックを適用するきっかけとしてもっとも大きいのは、クラスの一部のインスタンスしか使わないメソッドやフィールドがあるとわかったときです。
上記のサンプルは、開発用ツールに関してはエンジニアの階級による割引価格で購入できるという前提でコードを書いています。リファクタリング前のコードだと、 is_development_tool
が true
のときだけ、 engineer
の情報が必要になります。
JobItem
クラスに開発用ツールときだけ使用される、フィールドやメソッドがあることになります。こういった場合に、 Extract Subclass を適用して、サブクラスを抽出すれば、コードを綺麗にすることができますね。
Introduce Inheritance
似た機能を持つ2つ(複数)のクラスがあるときに、片方のクラスをスーパークラスにし、共通するフィールド・メソッドを移すテクニック
# リファクタリング前
class Room
attr_accessor :visitor, :base_price, :days
def initialize(visitor, base_price, days)
@visitor = visitor
@base_price = base_price
@days = days
end
def visitor_name
@visitor.name
end
def total_price
@base_price * @days
end
end
class SweetRoom
attr_accessor :visitor, :base_price, :days, :room_service_fee
def initialize(visitor, base_price, days, room_service_fee)
@visitor = visitor
@base_price = base_price
@days = days
@room_service_fee = room_service_fee
end
def visitor_name
visitor.name
end
def total_price
@base_price * @days + @room_service_fee
end
end
# リファクタリング後
class Room
attr_accessor :visitor, :base_price, :days
def initialize(visitor, base_price, days)
@visitor = visitor
@base_price = base_price
@days = days
end
def visitor_name
@visitor.name
end
def total_price
@base_price * @days
end
end
class SweetRoom < Room
attr_accessor :visitor, :base_price, :days, :room_service_fee
def initialize(visitor, base_price, days, room_service_fee)
super(visitor, base_price, days)
@room_service_fee = room_service_fee
end
def total_price
super + @room_service_fee
end
end
このテクニックも重複を避けるためのテクニックです。何度もしつこいようですが、重複するコードがあると不具合が発生する可能性が高くなります。
似たような機能をもつ2つのクラスがあるときに Introduce Inheritance を適用すれば、コードを重複を避けることができます。メソッド名が同じで中身が若干違うメソッドも super メソッドを使うことでシンプルに書けるかもしれません。
Collapse Hierarchy
スーパークラスとサブクラスに大差がないとき、両者を1つにするテクニック
# なし
開発を進み、気がつくとスーパークラスとサブクラスに大した差分が無くなっていたりすることがあるようです(私はまだ経験したことがありませんが。。)
それに気付いたときに放置するのではなく、両者を1つにしてしまいましょう。クラスが少なくなれば見る箇所も少なくなるので、効率よく開発ができるようなるはずです。
考察
11章では、コードの重複を避けるべきだという主張が何度もされています。重複したコードがあると、片方は修正したのに、もう一方を修正し忘れる可能性が高まり、不具合を発生する可能性を高めます。
また重複するコードがある箇所を修正するたびに、様々なファイルを探し回るという手間もかかってしまい、効率のよい開発の妨げとなってしまいます。
つまり、開発効率を向上するため、不具合を未然に防ぐために、重複しているコードをいかに一箇所にまとめるかが大切なのだと考えます。