1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

リファクタリング: Ruby エディション 第8章

Last updated at Posted at 2018-10-13

はじめに

Amazon: https://www.amazon.co.jp/dp/4048678841

第6章から第11章まではリファクタリングの具体的なテクニックがまとめられています。
今回は リファクタリング: Rubyエディション第8章 データの構成 で紹介されているいくつかのテクニックについて、まとめました。

※ 以下のサンプルコードは書籍で紹介されているコードを参考に、自分で作成したコードを載せています。(著作権を侵害しないように)

テクニック一覧

テクニック詳細

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 PolymorphismReplace Type Code with Module Extension と同じですが、タイプコードによる実装がクラスの一部であり、タイプコードが処理の途中で変更されるときに使えるテクニックである。

これも条件分岐が少なくなるため、コードの見通しがよくなります。さらにクラスの一部に対しても使用できるので、リファクタリングできる機会が多いように思えます。またタイプコードが処理の中で変更されても、ふるまいを変更することができます。

今回は State パターンでの実装を書きましたが、状況によって Starategy パターンを使って実装することもできます。

考察

同じ目的(タイプコードによる条件分岐を削除する)でも、設計によって適切な対応方法があり、それぞれに長所と短所があるため、既存のコードを理解し、どの対応方法を選択するか時間をかけることも必要だと考えました。

ただ一方で、仕様やコードの特徴が変われば、それに合わせて適切な(メンテナンスしやすい)コードに変えていけるというリファクタリングの良さであるので、対応方法の選択に時間をかけすぎないことも大切だと考えます。

コードを理解した上で適切な対応方法をじっくり考えること。うまくいかなかったらまたリファクタリングすればよいと(ときには)勢いにまかせること。両者のバランスをうまく保ちながらリファクタリングをしていくことが大切なのではないかと思いました。

関連

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?