空飛ぶたまごエンジニア

韓国進出を夢見るエンジニアのブログ

10月6日の日記

今日の学び

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

Self Encapsulate Field

フィールドとの密結合により、フィールドへの直接アクセスが不都合になっているとき、アクセサを使って関節アクセスするようにする

# リファクタリング前
def salary
  @base_salary * @bonus_rate
end

# リファクタリング後
attr_reader :base_salary, :bonus_rate

def salary
  base_salary * bonus_rate
end

スーパークラスはフィールドに直接アクセスしているが、サブクラスでは計算した値を返すためにフィールドへのアクセスをオーバーライドしたくなったときが絶好のタイミング

class Employee
  attr_accessor :base_salary, :bonus_rate

  def initialize(base_salary, bonus_rate)
    @base_salary = base_salary
    @bonus_rate  = bonus_rate
  end

  def salary
    base_salary * (1 + bonus_rate)
  end
end

class Manager < Employee
  attr_reader :position_allowance

  def initialize(base_salary, bonus_rate, position_allowance)
    super(base_salary, bonus_rate)
    @position_allowance = position_allowance
  end

  def total
    super + position_allowance
  end
end

大事なのは、リファクタリングによって直接変数アクセスも関節変数アクセスにも切り替えることができること。

Change Value to Reference

同値のインスタンスをいくつも生成するクラスがあり、それらのオブジェクトを同じものと判断したいとき、そのオブジェクトを参照オブジェクトに変更する

# リファクタリング前
class Guest
  attr_reader :name

  def initialize(name)
    @name = name
  end
end

guest1 = Guest.new("Bob")
guest2 = Guest.new("Ken")
guest1 == guest2 # => false

# リファクタリング後
class Guest
  attr_reader :name

  def initialize(name)
    @name = name
  end

  Instances ={}

  def self.load_guests
    new("Bob").store
  end

  def store
    Instances[name] = self
  end

  def self.create(name)
    Instances[name]
  end
end

Guest.load_guests
guest1 = Guest.create("Bob")
guest2 = Guest.create("Bob")
guest1 == guest2 # => true

オブジェクト導入時は、イミュータブルな値を持つ単純な値オブジェクトという位置付けだったけれども、後で変更可能なデータが追加したり、概念的に同じオブジェクトを参照する部分に変更による効果を与えたくなる場合がある。

そういった場合に上記のように、ファクトリメソッドを定義し、オブジェクトの作成処理を管理するようにして実装する必要がある。

つまりは、単純な値オブジェクトから参照オブジェクトに変更しなければならないということである。

Change Reference to Value

イミュータブルな参照オブジェクトの管理が面倒になってきたとき、オブジェクトを値オブジェクトに変更するテクニック

# リファクタリング前
class CurrencyRate
  attr_reader :code

  def initialize(code)
    @code = code
  end

  def self.get(code)
    ...レジストリから CurrencyRate インスタンスを返す
  end
end

jpy1 = CurrencyRate.get("JPY")
jpy2 = CurrencyRate.get("JPY")
jpy1 == jpy2

CurrencyRate.new("JPY") == CurrencyRate.new("JPY") # => false
CurrencyRate.new("JPY").eql?(CurrencyRate.new("JPY")) # => false

# リファクタリング後
class CurrencyRate
  attr_reader :code

  def initialize(code)
    @code = code
  end

  def self.get(code)
    ...レジストリから CurrencyRate インスタンスを返す
  end

  def eql?(other)
    self == (other)
  end

  def ==(other)
    other.equal?(self) ||
      (other.instance_of?(self.class) &&
       other.code == code)
  end
end

jpy1 = CurrencyRate.get("JPY")
jpy2 = CurrencyRate.get("JPY")
jpy1 == jpy2

CurrencyRate.new("JPY") == CurrencyRate.new("JPY") # => true
CurrencyRate.new("JPY").eql?(CurrencyRate.new("JPY")) # => true

Change Value to Reference とは全く逆のことをしている。

イミュータブルな参照オブジェクトの管理が面倒になってきた時に、オブジェクトを値オブジェクトに変更することで、管理コストを下げることができる。

しかし、そのためには eql? メソッドと == メソッドをオーバライドする必要があるので、注意が必要。

調べたこと

値オブジェクト

以下のような特徴を持ったオブジェクトを「値オブジェクト」という

  • インスタンス自体よりも、保持するデータ値が重要な意味をもつ。
  • インスタンスが異なっても、同じ値を保持していれば同じオブジェクトとみなす。
  • 必要に応じて自由にコピーを作ることができる。
  • 原則としてイミュータブルでなければならない。

参照オブジェクト

以下のような特徴を持ったオブジェクトを「参照オブジェクト」という

  • インスタンス自身が重要な意味をもつ。
  • 同じデータを持っていても、インスタンスが異なれば違うオブジェクトと見なす。
  • 通常コピーは作らない。
  • イミュータブルでなくてもよい。