はじめに
Amazon: https://www.amazon.co.jp/dp/4048678841
第6章から第11章まではリファクタリングの具体的なテクニックがまとめられています。
今回は リファクタリング: Rubyエディション
の 第8章 データの構成
で紹介されているいくつかのテクニックについて、まとめました。
※ 以下のサンプルコードは書籍で紹介されているコードを参考に、自分で作成したコードを載せています。(著作権を侵害しないように)
テクニック一覧
- Replace Data Value with Object
- Replace Array with Object
- Change Bidirectional Association to Unidirectional
- Encapsulate Collection
- Replace Type Code with Polymorphism
- Replace Type Code with Module Extension
- Replace Type Code with State/Strategy
テクニック詳細
Replace Data Value with Object
単純なデータ項目に特別なふるまいが必要になったとき、データ値をオブジェクトに変更するテクニック
# リファクタリング前
class Booking
attr_accessor :guest
def initialize(guest)
@guest = guest
end
end
class ...
def self.number_of_bookings_for(booking, guest)
bookings.select { |booking| booking.guest == guest }.size
end
end
# リファクタリング後
class Booking
def initialize(guest_name)
@guest = Guest.new(guest_name)
end
def guest_name
@guest.name
end
def guest=(guest_name)
@guest = Guest.new(guest_name)
end
end
class Guest
attr_reader :name
def initialize(name)
@name = name
end
end
class ...
def self.number_of_bookings_for(booking, guest_name)
bookings.select { |booking| booking.guest_name == guest_name }.size
end
end
設計初期段階では、単純なデータとして表現していたけれど、開発が進むにつれて特別なふるまいが必要になるということはよくあるようです。そういった場合にはデータ値をオブジェクトに変更することで解決することができます。
ただ気をつけなければならないこともあります。もともと単純な値(オブジェクト)であるので、値オブジェクトとしてリファクタリングする必要があります。
サンプルのリファクタリング後のコードにおいて、セッターで新しい Guest を作成するようにしているのは、値オブジェクトは原則としてイミュータブル(書き換え不能)でなければならないからです。
Replace Array with Object
一部の要素が別の意味を持つ Array があるとき、 Array の各要素をフィールドとするオブジェクトに置き換えるテクニック
# リファクタリング前
row = []
row[0] = "Sakuraba Kazushi"
row[1] = 26
# リファクタリング後
row = Record.new
row.name = "Sakuraba Kazushi"
row.wins = 26
私はあまり見たことがない(単に読んだコードの量が少ないだけ笑)のですが、 Array に複数の異なるものが格納されてしまうとデータを扱いづらくなってしまうと思います。
「Array の第1要素は名前で、第2要素は勝ち数」というルールを作ったとしても、人間が覚えないといけなくなります。また、コメントとしてルールを記述したとしても、コードが増えるだけでなく、ルールを思い出すためにコードを行ったり来たりしなければなくなるでしょう。
こういった Array はオブジェクトにして、フィールドを与えてデータを格納することで、ルールを覚える必要もなくなりますし、コメントを残す必要もなくなります。
Change Bidirectional Association to Unidirectional
双方向リンクが作られている片方のクラスが、もう片方のクラスのメンバを使わなくなったとき、不要なリンクを取り除くテクニック
# リファクタリング前
class Booking
attr_reader :guest
def discounted_price
if guest.seats >= 2
base_price - ( 500 * guest.seats )
end
end
end
class Guest
attr_reader :booking
end
# リファクタリング後
class Booking
def discounted_price(guest)
if guest.seats >= 2
base_price - ( 500 * guest.seats )
end
end
end
class Guest
attr_reader :booking
end
# サンプルは Guest がいない限り Booking もないという前提になっている
双方向リンクを作ることで便利になる一方で、その代償があるのも事実です。双方向リンクを作ると否応でも相互依存関係ができます。つまり、一方を変更したら、片方も変更するように気をつけなければならなくなります。
この関係が無数に増えてくると、ある変更が意図しないことを引き起こし、不具合が発生する可能性を高めてしまいます。だからこそ、双方向リンクを作るのは本当に必要な時だけに留め、もし双方向リンクを維持する必要がなくなったときは、積極的にリンクを削除すべきだと理解しました。
Encapsulate Collection
メソッドがコレクションを返しているとき、直接コレクションを変更できないようにカプセル化するテクニック
# リファクタリング前
class Club
def initialize(name)
@name = name
end
end
class Person
attr_accessor :clubs
end
bob = Person.new
bob.clubs = []
soccer_club = Club.new("Soccer Club")
bob.clubs << soccer_club
bob.clubs.count # => 1
bob.clubs.delete(soccesr_club)
bob.clubs.count # => 0
# リファクタリング後
class Club
def initialize(name)
@name = name
end
end
class Person
def initialize
@clubs = []
end
def clubs
@clubs.dup
end
def add_club(club)
@clubs << club
end
def delete_club(club)
@clubs.delete(club)
end
end
bob = Person.new
soccer_club = Club.new("Soccer Club")
bob.clubs << soccer_club
bob.clubs.count # => 0
bob.add_club
bob.clubs.count # => 1
bob.delete_club(soccesr_club)
bob.clubs.count # => 0
クラスのメソッドがインスタンスのコレクションを返す実装を、私もよく見かけます。しかし、そうしてしまうとオーナークラスが知らないうちに、クライアントがコレクションの内容を変更できてしまいます。意図しない変更をできてしまうということは、意図しない不具合を発生させてしまう可能性が高くなります。
よって、コレクションの操作を行う専用のメソッドを定義し、且つコレクション自体を返すのではなく、コレクションのコピーを返すようにします。そうすることで、クライアントが直接コレクションを操作できないようになります(カプセル化)。
上記のように実装することで、意図しない変更を防ぎ、意図しない不具合が発生することを防ぐことができるのだと思います。
Replace Type Code with Polymorphism
クラスの主要な機能がタイプコードによって実装されているとき、タイプコードの値1つに1つのクラスを作って書き換えるテクニック
# リファクタリング前
class Room
def initialize(room_type)
@room_type = room_type
@base_price = 3000
end
def price
when @room_type
case :economy
@base_price
case :standard
@base_price * 1.2
case :sweet
@base_price * 3.0
end
end
end
# リファクタリング後
module Room
def initialize
@base_price = 3000
end
end
class EconomyRoom
include Room
def price
@base_price
end
end
class StandardRoom
include Room
def price
@base_price * 1.2
end
end
class SweetRoom
include Room
def price
@base_price * 3.0
end
end
タイプによって処理を変更するという機能がクラスの主要部分を担っているときは、クラスを抽出してダックタイピングを利用し、条件構文を取り除くというテクニックです。
こうすれば条件が無くなり、コードの見通しがよくなります。またテスタブルなコードになるので、保守性が向上します。
条件分岐がなくなることでコードの見通しがよくなったり、テスタブルなコードになって恩恵を受けるのは私たち開発者です。より効率的な開発を続けられるようになるのだと思います。
Replace Type Code with Module Extension
クラスにタイプコードによって実装されている機能がある(注:それ以外の機能も多い)とき、タイプコードを動的モジュールのextendに書き換えるテクニック
# リファクタリング前
class Room
def initialize(room_type)
@room_type = room_type
@base_price = 3000
end
def price
when @room_type
case :economy
@base_price
case :standard
@base_price * 1.2
case :sweet
@base_price * 3.0
end
end
end
# リファクタリング後
class Room
def initialize
@base_price = 3000
end
def room_type=(mod)
extend(mod)
end
end
module EconomyRoom
def price
@base_price
end
end
module StandardRoom
def price
@base_price * 1.2
end
end
module SweetRoom
def price
@base_price * 3.0
end
end
条件分岐を取り除くという目的は Replace Type Code with Polymorphism と同じですが、タイプコードによる実装がクラスの一部であり、タイプコードが処理の途中で変更されないときに使えるテクニックである。
これも条件分岐が少なくなるため、コードの見通しがよくなります。さらにクラスの一部に対しても使用できるので、リファクタリングできる機会が多いように思えます。
ただ気をつけなければならないのが、一度モジュールをミックスインしてしまうと、モジュールのふるまいを取り除くことは難しいです。例えば、タイプコードが処理の中で変更されて、別のモジュールがミックスインされると前にミックスインしたモジュールのふるまいが残ってしまいます。ふるまいを取り除く心配がないときに使うと良さそうです。
Replace Type Code with State/Strategy
クラスにタイプコードによって実装されている機能があり、処理の途中でタイプコードが変更されるような実装のとき、タイプコードを State オブジェクトに書き換えるテクニック
# リファクタリング前
class Room
def initialize(room_type)
@room_type = room_type
@base_price = 3000
end
def price
when @room_type
case :economy
@base_price
case :standard
@base_price * 1.2
case :sweet
@base_price * 3.0
end
end
end
# リファクタリング後
require 'forwardable'
class Room
extend Forwardable
def_delegators :@room_type, :price
def initialize(room_type)
@room_type = room_type
end
def upgrade_to_starndard
@room_type = StandardRoom.new
end
def upgrade_to_sweet
@room_type = SweetRoom.new
end
end
class EconomyRoom
def initialize
@base_price = 3000
end
def price
@base_price
end
end
class StandardRoom
def initialize
@base_price = 3000
end
def price
@base_price * 1.2
end
end
class SweetRoom
def initialize
@base_price = 3000
end
def price
@base_price * 3.0
end
end
条件分岐を取り除くという目的は Replace Type Code with Polymorphism や Replace Type Code with Module Extension と同じですが、タイプコードによる実装がクラスの一部であり、タイプコードが処理の途中で変更されるときに使えるテクニックである。
これも条件分岐が少なくなるため、コードの見通しがよくなります。さらにクラスの一部に対しても使用できるので、リファクタリングできる機会が多いように思えます。またタイプコードが処理の中で変更されても、ふるまいを変更することができます。
今回は State パターンでの実装を書きましたが、状況によって Starategy パターンを使って実装することもできます。
考察
同じ目的(タイプコードによる条件分岐を削除する)でも、設計によって適切な対応方法があり、それぞれに長所と短所があるため、既存のコードを理解し、どの対応方法を選択するか時間をかけることも必要だと考えました。
ただ一方で、仕様やコードの特徴が変われば、それに合わせて適切な(メンテナンスしやすい)コードに変えていけるというリファクタリングの良さであるので、対応方法の選択に時間をかけすぎないことも大切だと考えます。
コードを理解した上で適切な対応方法をじっくり考えること。うまくいかなかったらまたリファクタリングすればよいと(ときには)勢いにまかせること。両者のバランスをうまく保ちながらリファクタリングをしていくことが大切なのではないかと思いました。