3
3

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 1 year has passed since last update.

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

Last updated at Posted at 2023-04-09

「リファクタリング:Rubyエディション」の内容を要約し、普段使いしやすくすることで、自分や他人のコードをより綺麗にしていくことを目的とする。全75パターン存在する。

とはいえ、この要約だけで「リファクタリング:Rubyエディション」の真髄を余すところ説明できているわけではない。「リファクタリング:Rubyエディション」の真の威力を知りたい・発揮したい方は「リファクタリング:Rubyエディション」を今すぐ購入されたし。

6章 メソッドの構成方法

6.1 メソッドの抽出

コードの一部をメソッドにして、その目的を説明する名前をつける。

メリット

  • 詳細を見なくても、コードが何をしているかわかる
  • 短い関数は読みやすい
  • コメントを減らす

理由: メソッドの粒度が細ければ、流用性が高くなる。
注意: 意味のある良い名前が思いつかないなら、それは抽出すべきでない。
(感想: とても重要。<6.2 メソッドのインライン化>にもあるように。メソッドの粒度は粗すぎず細かすぎないように気をつける。)

6.1.1 メソッドの抽出::ローカル変数なし

リファクタ前
def sample_method
  puts "sample"
  # その他諸々の処理...
end
リファクタ後
def sample_method
  print_sample
end

def print_sample
  puts "sample"
  # その他諸々の処理...
end

6.1.2 メソッドの抽出::ローカル変数あり

リファクタ前
def sample_method
  local_variable = 0
  puts "instance_variable: #{@instance_variable}"
  puts "local_variable: #{local_variable}"
  # その他諸々の処理で、local_variableは使われる...
end
リファクタ後
def sample_method
  local_variable = 0
  print_sample_method(local_variable)
  # その他諸々の処理で、local_variableは使われる...
end


# ローカル変数は別メソッド間でアクセスできないため、引数で渡す必要がある。
def print_sample_method(local_variable)
  puts "instance_variable: #{@instance_variable}"
  puts "local_variable: #{local_variable}"
end

6.1.3 メソッドの抽出::ローカル変数への再代入

リファクタ前
def sample_method
  local_variable = 0
  @sample.each do |sample|
    local_variable += sample.num
  end
  # その他諸々の処理で、local_variableは使われる...
end
リファクタ後
def sample_method
  # ローカル変数に再代入することで、以降の処理に影響を与えない
  local_variable = calculate_sum
  # その他諸々の処理で、local_variableは使われる...
end

def calculate_sum
  # local_variable = 0
  # @sample.each do |sample|
  #   local_variable += sample.num
  # end
  # local_variable
  
  # 上記はinjectでスマートに書ける
  @sample.inject(0) { |local_variable, sample| local_variable + sample.num }
end

6.2 メソッドのインライン化

メソッドを呼び出し元に組み込み、そのメソッドを削除。

条件:
メソッドの本体が、そのメソッドの名前と同じぐらいわかりやすい場合。
メソッドグループが悪く構成されていて、それらをグループ化するとより明確になる場合。

理由:
過剰な間接化はイライラの原因。
不必要な間接参照(単純な委譲)は面倒になる。

リファクタ前
def sample_method
  # @sampleが5以上の時にtrueを判別するis_more_than_fiveメソッドを作っているが、
  # 間接化が過剰なためクールではない。
  is_more_than_five ? 2 : 1
end

def is_more_than_five
  @sample > 5
end
リファクタ後
def sample_method
  @sample > 5 ? 2 : 1
end

6.3 一時変数のインライン化

一時変数を削除。

条件: 単純 かつ 1度しか代入されていない場合。
一時変数が式を1回割り当てられ、他のリファクタリングを妨げている場合。

理由: リファクタリングを妨げる。

リファクタ前
def price
  base_price = @quantity * @item_price
  if base_price > 1000
    base_price * 0.95
  else
    base_price * 0.98
  end
end
リファクタ後
def price
  if @quantity * @item_price > 1000
    @quantity * @item_price * 0.95
  else
    @quantity * @item_price * 0.98
  end
end

6.4 一時変数から問合せメソッドへ

一時変数(式)をメソッドにする。
メソッドは最初は非公開にしておき、後で他の用途が見つかったら緩めればいい。

条件: なし。
理由: 一時変数の問題点は、一時的でローカルであること。一時変数にアクセスするためにはメソッドを長くする以外になく、メソッドの長大化を助長するため。

この変更で不安なのは下記2点。

同じメソッドを2回以上呼び出しても、パフォーマンスは大丈夫か。
リファクタリングは、わかりやすくすることに集中すべき。(パフォーマンスを上げるのは別の仕事)
メソッド呼び出し回数が増えても、ほとんどの場合問題ない。
複数回呼び出しているメソッドが、冪等かどうか。

一時変数は無いほうがいい。なぜそこに一時変数があるのか分からなくなってしまうため。
コードが何をしようとしているかが、はっきりと分かるようになる。

(感想: とても重要。特に「メソッドを複数回呼び出しまって良いのか」という点。この点は、個人的にはこれまで抵抗があったが、この本を読んで考え方が理解できた。)

リファクタ前
def price
  # base_priceを求めるロジック
  base_price = @quantity * @item_price

  # discount_factorを求めるロジック
  if base_price > 1000
    discount_factor = 0.95
  else
    discount_factor = 0.98
  end
  
  # 本質
  base_price * discount_factor
end
リファクタ後
def price
  base_price * discount_factor
end

def base_price
  @quantity * @item_price
end

def discount_factor
  base_price > 1000 ? 0.95 : 0.98
end

# base_priceを一時変数から問合せメソッドにしなかった場合
def price
  # base_priceが一時変数のままの場合
  base_price = @quantity * @item_price

  discount_factor(base_price)
  
  base_price * discount_factor
end

# base_priceはローカル変数なのでメソッドからアクセスできず、外部から渡さないといけない(引数など)
def discount_factor(base_price)
  base_price > 1000 ? 0.95 : 0.98
end

6.5 一時変数からチェインへ

チェイニング(=メソッドチェーンで繋いで)して、一時変数を削除。

条件: 一時変数に対して複数行に渡ってメソッドを複数回実行している状況。

リファクタ前
mock = Mock.new()
expectation = mock.receive(:method_name)
expectation.with("args")
expectation.and_return("result")
リファクタ後
mock = Mock.new()
mock.receive(:method_name).with("args").and_return("result")

6.6 説明用変数の導入

処理の目的を説明する名前の一時変数に、式orその一部 の結果を保管する。

条件: 式が複雑な場合。式が読みにくい場合
理由: 可読性向上のため。

6.6 ~ 6.8項で一時変数を導入するが、軽々しく一時変数を導入してはいけない。(理由: 6.4項に記載)
<6.6 説明用変数の導入>の前に、<6.1 メソッドの抽出>ができないか考えること。

リファクタ前
if platform.upcase.index("MAC") && browser.upcase.index("IE") && was_initialized && resize > 0
  # 何らかの処理
end
リファクタ後
is_mac_os = platform.upcase.index("MAC")
is_ie_browser = browser.upcase.index("IE")
was_resized = resize > 0

if is_mac_os && is_ie_browser && was_initialized && was_resized
  # 何らかの処理
end

6.7 一時変数の分割

代入ごとに別の一時変数を用意する。

条件: ループ変数でも計算結果蓄積用の変数でもないのに、複数回代入される一時変数がある場合。
一時変数が2回以上割り当てられ、ループ変数でも集計用一時変数でもない場合。

理由: 2つの異なる目的のために1つの一時変数を使い回すと混乱するため。
変数には1つの責任だけがあるべきである。
2つの異なる目的のために1つの一時変数を使用すると、読み手にとって非常に混乱する。

リファクタ前

temp = 2 * (@height + @width)
puts temp
temp = @height * @width
puts temp
リファクタ後
perimeter = 2 * (@height + @width)
puts perimeter
area = @height * @width
puts area

6.8 引数への代入の除去

引数への代入の代わりに一時変数を使う。

条件: 引数に代入を行っている場合。
理由: Rubyは(参照渡しでなく)値渡しなため、呼び出し元ルーチンには影響はないが、紛らわしく、読みにくいため。
また、引数の用途は渡されたものを表すことなので、それに代入して別の役割を持たせないようにするため。

コードがパラメータに代入している場合。

動機

渡されたオブジェクトの内部を変更することはできますが、他のオブジェクトを指すように変更しないように注意してください。
渡されたオブジェクトを代表するために、引数のみを使用してください。

リファクタ前
def discount(input_val, quantity, year_to_date)
  input_val -= 2 if input_val > 50
  # ...
end
リファクタ後
def discount(input_val, quantity, year_to_date)
  result = input_val
  result -= 2 if input_val > 50
  # ...
end

6.9 メソッドからメソッドオブジェクトへ

メソッドを独自のオブジェクト(クラス)に変える。ローカル変数をそのオブジェクトのインスタンス変数にする。

条件: <メソッドの抽出>を適用できないようなローカル変数の使い方をしている場合。
理由: 独自のオブジェクト内で、メソッドの分解(<メソッドの抽出>)ができる。
(感想: 手間のかかるリファクタなので、巨大なメソッドに適用するのが費用対効果的に良いと思う。)

  1. メソッドオブジェクトの導入
    長いメソッドで、ローカル変数を使用しているため、1. メソッドの抽出を適用できない場合。
リファクタ前
class Order
  def price
    primary_base_price = 0
    secondary_base_price = 0
    tertiary_base_price = 0
    # 長い計算処理
    # ...
  end
end
リファクタ後
class Order
  def price
    PriceCalculator.new(self).compute
  end
end

class PriceCalculator
  def initialize(order)
    @order = order
  end

  def compute
    primary_base_price = 0
    secondary_base_price = 0
    tertiary_base_price = 0
    # 長い計算処理
    # ...
  end
end

動機

メソッドに多数のローカル変数がある場合、分解することができない場合。
この例では、実際にはこのリファクタリングが必要ないかもしれませんが、方法を示すために記載しています。

リファクタ前
class Account
  def gamma(input_val, quantity, year_to_date)
    important_value1 = (input_val * quantity) + delta
    important_value2 = (input_val * year_to_date) + 100
    if (year_to_date - important_value1) > 100
      important_value2 -= 20
    end
    important_value3 = important_value2 * 7
    # 以下略
    important_value3 - 2 * important_value1
  end

  def delta
    1
  end
end
リファクタ後
class Gamma
  attr_reader :important_value1, :important_value2, :important_value3

  def initialize(account, input_val, quantity, year_to_date)
    @account = account
    @input_val = input_val
    @quantity = quantity
    @year_to_date = year_to_date
  end

  def compute
    @important_value1 = (@input_val * @quantity) + @account.delta
    @important_value2 = (@input_val * @year_to_date) + 100
    if (@year_to_date - @important_value1) > 100
      @important_value2 -= 20
    end
    @important_value3 = @important_value2 * 7
    # 以下略
    @important_value3 - 2 * @important_value1
  end
end

class Account
  def gamma(input_val, quantity, year_to_date)
    Gamma.new(self, input_val, quantity, year_to_date).compute
  end

  def delta
    1
  end
end

6.10 アルゴリズム変更

より簡単なアルゴリズムに変更。
(感想: 6.11項のコレクションクロージャメソッドを使うことで、アルゴリズムを簡単にすることがかなり実現できると思う。)

アルゴリズムをよりわかりやすいものに置き換えたい場合。

動機

複雑なものを単純なものに分解する。
アルゴリズムの変更を容易にする。
大規模な複雑なアルゴリズムの置換は非常に困難であり、簡単なアルゴリズムにすることで、置換を行いやすくすることができる。

リファクタ前
def found_person(people)
  people.each do |person|
    return 'Don' if person == 'Don'
    return 'John' if person == 'John'
    return 'Kent' if person == 'Kent'
  end
  ''
end
リファクタ後
def found_person(people)
  candidates = ['Don', 'John', 'Kent']
  people.each do |person|
    return person if candidates.include?(person)
  end
  ''
end

6.11 ループからコレクションクロージャメソッドへ

ループでなく、コレクションクロージャメソッドを使う。
each系の処理をmap系の処理にすること。
繰り返し処理した結果を返却したい場合に、ループの外から変数として渡す必要がなくなる。
(感想: とても重要。eachを使いたいと思ったとき、必ずmap系で実現できないか確認する。)

select
条件に合致するものだけを抽出する。

リファクタ前
managers = []
employees.each do |e|
  managers << e if e.manager?
end
# ↓
managers = employees.select { |e| e.manager? } # {}内は真偽値

map
ブロックを実行した戻り値を格納する。

リファクタ前
offices = []
employees.each { |e| offices << e.office }
リファクタ後
offices = employees.map { |e| e.office } # {}内は実行するブロック
リファクタ前
managerOffices = []
employees.each do |e|
  managerOffices << e.office if e.manager?
end
リファクタ後
# チェインで繋げられる
managerOffices = employees.select { |e| e.manager? }
                           .map { |e| e.office }

inject
合計を出すとき など、ループ内で値を生み出すような場合に使う。
返り値の初期値が設定できる。(例: 0からスタートする、10からスタートする)

inject(返り値の初期値) { | 返り値, 配列内のオブジェクト| ブロック }

リファクタ前
total = 0
employees.each { |e| total += e.salary }
リファクタ後
total = employees.inject(0) { |sum, e| sum + e.salary }
# (デフォルト引数は0 なので(0)は省略可)

6.12 サンドイッチメソッドの抽出

条件: ほぼ同じロジックだが、その中間ぐらいのロジックに差異がある複数のメソッドがあるとき。
重複部分を抽出してメソッドにする。
その複数のメソッドは、抽出したメソッドにブロックを渡すようにする。
(感想: よくあるシチュエーションだと感じる。よく使いそう。)

リファクタ前
  def sample_method_equal_one
    if @count.present? && @count == 1
      @count += 1
    end
  end

  def sample_method_equal_arg(arg)
    if @count.present? && @count == arg # 少しだけ違う
      @count += 1
    end
  end
リファクタ後
  def sample_method_equal_one
    sample_method_logic { |count| count == 1 } # {}内がブロック
  end

  def sample_method_equal_arg(arg)
    sample_method_logic { |count| count == arg }
  end

  def sample_method_logic
    if @count.present? && yield(@count) # ブロックを実行。
      @count += 1
    end
  end

6.13 クラスアノテーションの導入

initializeの導入。

6.14 名前付き引数の導入

キーワード引数の導入。

6.15 名前付き引数の除去

キーワード引数の除去。引数が1つであるとき。

理由: 引数が1つだと、わざわざキーワードにする必要がない。メソッドを呼び出すたびにキーワード引数を書かないといけなく、費用対効果が小さいため。

6.16 使われていないデフォルト引数の除去

使われていないデフォルト引数を除去。
引数による条件分岐などが削除できる可能性がある。
(感想: 使われていないことをちゃんと確認しないといけない。)

6.17 動的メソッド定義

メソッドを動的に定義する。

本書にあったのはdef_each等を使う方法であったが、調べたところ古いなので、下記記事を参考にした。
https://tech.ga-tech.co.jp/entry/2019/10/ruby-metaprogramming
define_methodやsendを使うことで実現できる。

define_method

一言で言うなら、defでメソッド定義しなくてもメソッドを定義できる
https://fujiyasu.hatenablog.com/entry/2017/09/23/150205

メリット

define_method を使って、同じような処理を繰り返し書かずに済むようにすることで、コードの重複を減らすことができ、メンテナンス性を高めることができます。

デメリット

しかしながら、define_method を使いすぎると、可読性が低下し、デバッグが困難になることがあります。また、動的に生成されたメソッドは、メソッドテーブルに登録されるため、メモリを消費することにもなります。そのため、必要以上に使用しないように注意する必要があります。

また、define_method は、実行時にメソッドを生成するため、コンパイル時に発生するエラーを検出できないことがあります。そのため、可能な限り静的なコード生成を行う方法を検討することが重要です。

使われるタイミングとしては、以下のような場合によく使用されます。

  • 同じような処理を繰り返し書く必要がある場合
  • 動的にメソッドを生成する必要がある場合
  • 汎用的な機能を提供するためにライブラリを作成する場合

また、上記のような場合でも、メソッドを定義する場合は、できるだけ静的な方法を検討することが重要です。define_method は、その場合に限定的に使用されるべきです。

define_methodとdef定義のメソッドはどちらの方が実行速度は高速ですか?

define_method と def 定義のメソッドは、それぞれ違った特性を持っていますが、一般的には def 定義のメソッドの方が実行速度が高速です。

これは、def 定義のメソッドは、コンパイル時に実行可能なメソッドに変換されるため、実行時に追加のオーバーヘッドが発生せず、高速に実行されるからです。

一方、define_method は、実行時に動的にメソッドを生成するため、実行時のオーバーヘッドが発生します。また、動的に生成されたメソッドは、メソッドテーブルに登録されるため、メモリを消費することにもなります。

ただし、実行速度よりも柔軟性や可読性を重視する場合には、define_method を使うこともあります。define_method を使うことで、動的にメソッドを生成できるため、柔軟性が高くなります。また、同じコードを繰り返し書かなくて済むため、可読性が向上することもあります。

総じて、実行速度を重視する場合には def 定義のメソッドを使い、柔軟性や可読性を重視する場合には define_method を使うことが適切です。

6.18 動的レセプタから動的メソッド定義へ

動的メソッド定義を行うことで、method_missingを使わずに同じような振る舞いを実現。

method_missing
メソッドが未定義の際にNoMethodErrorを発行するメソッド。ここにバグがあると発見しにくいため、自前でmethod_missingを実装するのは避けたほうがいい。

6.19 動的レセプ夕の分離

優先順位低
使用頻度低

6.20 evalを実行時からパース時へ

優先順位低
使用頻度低

7章 オブジェクト間でのメンバの移動

7.1 メソッドの移動

メソッドを最もよく使っている他のクラスに、同じ内容の新メソッドを作る。
古いメソッドは、そのメソッドに処理を委ねる or 削除する。
動機
カプセル化の改善、疎結合化

条件: メソッドが、自身のクラスよりも他クラスをよく利用している or 利用されている場合。(今はそうでなくても、そうなりつつあるとき。)

現在のコンテキストよりも他のコンテキストの要素を参照する機能を移動する

リファクタ前
class Account
  def overdraft_charge
  end
end

class AccountType
end

から

リファクタ後
class Account
end

class AccountType
  def overdraft_charge
  end
end

7.2 フィールドの移動

移すクラスに新しいフィールドのreader(必要ならwriterも)を作り、フィールドを使っているコードを書き換える。

条件: フィールドが、自身のクラスよりも他クラスをよく利用している or 利用されている場合。(今はそうでなくても、そうなりつつあるとき。)

動機

常に一緒に関数に渡されるデータのピースは、単一のレコードに入れる方が良い
あるレコードの変更が別のレコードのフィールドを変更する原因となる場合、それはフィールドが間違った場所にある兆候です

フィールドを一つのクラスから別のクラスに移動する

class Class1
-  attr_accessor :a_field # hogehogeしたいので
end

class Class2
+  attr_accessor :a_field  # hogehogeに変更
end

7.3 クラスの抽出

新しいクラスを作成。関連フィールド・メソッドを移す。

条件: 2つのクラスで行うべき仕事をしているクラスがある場合。

リファクタ前
class Person
  attr_accessor :office_area_code
  attr_accessor :office_number
  
  def telephone_number
    '(' + @office_area_code + ')' + @office_number
  end
end

person = Person.new
person.office_area_code = 123
リファクタ後
class Person
  # 新しいクラスへのリンクを作る
  def initialize
    @office_telephone = TelephoneNumber.new
  end

  # 少し違う手段として、下記のコメント部のようにゲッター・セッターを用意すれば、フィールドへのアクセス方法は変えないで済む。
  # -----------------------------------------------
  # 既存のフィールドは、新しいクラスのフィールドに向ける
  # ゲッター
  # def office_area_code
  #   @office_telephone.area_code
  # end
  # # セッター
  # def office_area_code(arg)
  #   @office_telephone.area_code = arg
  # end
  # -----------------------------------------------

  # 移動したフィールドへのアクセスは、office_telephone経由となる。
  def office_telephone
    @office_telephone
  end

  # メソッドは新しいクラスに移動
  def telephone_number
    @office_telephone.telephone_number
  end
end

class TelephoneNumber
  attr_accessor :area_code, :number

  def telephone_number
    '(' + area_code + ')' + number
  end
end

person = Person.new
# 移動したフィールドへのアクセスは、office_telephone経由となる。
person.office_telephone.area_code = 123

7.4 クラスのインライン化

すべての機能を他のクラスに移して、クラスを削除する。(<7.3 クラスの抽出>の逆)

動機

リファクタリングを行った後、元のクラスから多くの責任が外れ、役に立たなくなった場合。

条件: 大した仕事をしていないクラスがある場合。
<7.3 クラスの抽出>の逆のことをすればいい。
クラスがあまり役に立っていないと感じられた場合、そのクラスに含まれる機能を、別のクラスに移動させ、元のクラスを削除します。

リファクタ前
class Person
  attr_reader :name, :telephone_number

  def initialize(name, telephone_number)
    @name = name
    @telephone_number = telephone_number
  end
end

class TelephoneNumber
  attr_reader :area_code, :number

  def initialize(area_code, number)
    @area_code = area_code
    @number = number
  end

  def telephone_number
    "#{area_code}-#{number}"
  end
end

上記のコードを以下のようにインライン化できます。

リファクタ
class Person
  attr_reader :name, :office_area_code, :office_number

  def initialize(name, office_area_code, office_number)
    @name = name
    @office_area_code = office_area_code
    @office_number = office_number
  end

  def telephone_number
    "#{office_area_code}-#{office_number}"
  end
end

7.5 委譲の隠蔽

サーバに、委譲を隠すためのメソッドを作る。

条件: クライアント(呼び出し元)が、サーバ(呼ばれる側)クラスの委譲クラスを呼び出している。
サーバ(呼ばれる側) : 下記例では、Person。
委譲クラス : 下記例では、Department。
理由: カプセル化できるため。
カプセル化
オブジェクトがシステムの他の部分についてあまり知識を持たなくていい。
→ システムに変更を加えても、その変更を知らせないといけないオブジェクトが減るため、変更しやすい。

リファクタ前
# 社員
class Person
  attr_accessor :department
end

# 部門
class Department
  attr_reader :manager

  def initialize(manager)
    @manager = manager
  end
end

manager = person.department.manager
リファクタ後
class Person
  attr_writer :department # ゲッターは不要になった

  def manager
    @department.manager
  end
end

class Department
  attr_reader :manager

  def initialize(manager)
    @manager = manager
  end

  # ...
end

# managerを取得するためにはdepartmentを経由しないといけない という知識が必要なくなる
manager = person.manager

7.6 横流しブローカーの除去

クライアントに、委譲オブジェクトを直接呼び出させる。(<7.5 委譲の隠蔽>の逆)

条件: クラスが単純な委譲をやりすぎている。
理由: 委譲オブジェクトに新しいメンバが追加されるたびに、サーバクラスに委譲メソッドを作らないといけないため。(<7.5 委譲の隠蔽>の代償)
<7.5 委譲の隠蔽>の逆のことをすればいい。

クラスが単純な委譲を行いすぎている場合。
クライアントが直接デリゲートを呼び出すようにします。

"中間者" (サーバー) が行う作業が多すぎる場合、クライアントが直接デリゲートを呼び出す必要があります。

リファクタ前
class ClientClass
  def do_something
    person = Person.new
    person.do_something
  end
end

class Person
  def initialize
    @department = Department.new
  end

  def do_something
    @department.do_something
  end
end
リファクタ
class ClientClass
  def do_something
    # Dependencies
    person = Person.new
    department = Department.new
    person.do_something
    department.do_something
  end
end

8章 データの構成

8.1 自己カプセル化フィールド

フィールド(インスタンス変数)に直接アクセスするのでなく、ゲッター・セッターを作りそれを経由する。

パブリックフィールドがあります。
・それをプライベートにし、アクセサを提供します。
動機
データを公開するべきではありません。

リファクタ前
class Item
  def initialize(base_price, tax_rate)
    @base_price = base_price
    @tax_rate = tax_rate
  end

  def raise_base_price_by(percent)
    @base_price = @base_price * (1 + percent/100)
  end

  def total
    @base_price * (1 + @tax_rate)
  end
end
リファクタ後
class Item
  attr_accessor :base_price, :tax_rate

  def initialize(base_price, tax_rate)
    @base_price = base_price
    @tax_rate = tax_rate
  end

  def raise_base_price_by(percent)
    base_price = base_price * (1 + percent/100)
  end

  def total
    base_price * (1 + tax_rate)
  end
end

class ImportedItem < Item
  attr_reader :import_duty

  def initialize(base_price, tax_rate, import_duty)
    super(base_price, tax_rate)
    # 関税
    @import_duty = import_duty
  end

  def tax_rate
    # Item側の振る舞いを変えず、簡単にtax_rateをオーバーライドできた
    super + import_duty
  end
end

8.2 データ値からオブジェクトへ

データ項目をオブジェクトに変換する。

理由: 1つの単純なデータ(項目)として表現していたものが、それほど単純でないと分かることがある。そういったときに、振る舞いを追加できるようにするため。

リファクタ前
class Order
  attr_accessor :customer

  def initialize(customer)
    @customer = customer # 顧客名(文字列)
  end
end

# ----------------------
# 別のクラスのどこかのコード

# 顧客が注文したorderの数を取得
def self.number_of_orders_for(orders, customer)
  orders.select { |order| order.customer == customer }.size
end
リファクタ後
class Order
  def initialize(customer)
    @customer = Customer.new(customer)
  end

  def customer
    @customer.name
  end

  def customer=(value)
    @customer = Customer.new(value)
  end
end

class Customer
  attr_reader :name

  def initialize(name)
    @name = name
  end
end

8.3 値から参照へ

条件: 同じインスタンス(値オブジェクト)をいくつも生成しており、それらを1つに圧縮したい場合。
その値オブジェクトを参照オブジェクトに変える。
毎回インスタンスを生成するのでなく、オブジェクト群をハッシュに格納(ロード)しておき、そのハッシュから探して返却する。

class Sample
  instances = {}

  def store
    instances[name] = self
  end

  def self.with_name(name)
    instances[name]
  end
end

8.4 参照から値へ

オブジェクトの参照が小さく、不変であり、扱いが厳しい場合があります。
値オブジェクトに置き換えます。

動機
分散システムや並行システムで使用するため、参照オブジェクトは小さく不変でなければなりません。しかし、参照オブジェクトを扱うのが厳しい場合があります。

リファクタ前
class Currency
  def initialize(code)
    @code = code
  end
end

Currency.new("USD") == Currency.new("USD") # falseを返す
リファクタ
class Currency
  attr_reader :code

  def initialize(code)
    @code = code
  end

  def ==(other)
    return false unless other.is_a?(Currency)
    code == other.code
  end

  alias eql? ==

  def hash
    code.hash
  end
end

Currency.new("USD") == Currency.new("USD") # trueを返す

8.5 配列からオブジェクトへ

ある配列の要素が異なる意味を持つ場合があります。
配列をオブジェクトに置き換えます。各要素にはフィールドを持たせます。
動機
配列は、類似したオブジェクトのコレクションを保持するためだけに使用するべきです。

リファクタ前
row = []
row[0] = "Liverpool"
row[1] = "15"
リファクタ
class Performance
  attr_accessor :name, :wins
end

row = Performance.new
row.name = "Liverpool"
row.wins = "15"

8.6 ハッシュからオブジェクトへ

条件: 異なる種類のオブジェクトを格納しているhashがある。
各キーに対応するフィールドを持つオブジェクトを定義し、hashを削除する。
(感想: フィールドをメソッドに変換できるのがメリット。)

リファクタ前
new_network = { nodes: [], old_networks: [] }

new_network[:old_networks] << node.network
new_network[:nodes] << node

new_network[:name] = new_network[:old_networks].map { |network| network.name }.join(" - ")
リファクタ後
# このように使いたい↓
new_network = NewworkResult.new

new_network.old_networks << node.network
new_network.nodes << node

new_network.name = new_network.old_networks.map { |network| network.name }.join(" - ")

# なので、クラスを定義する↓
class NewworkResult
  attr_reader :old_networks, :nodes
  # attr_accessor :name ← メソッドに移した

  def initialize
    @old_networks, @nodes = [], []
  end

  # nameフィールド→nameメソッドに移し、処理を定義できる
  def name
    @old_networks.old_networks.map { |network| network.name }.join(" - ")
  end
end

8.7 片方向リンクから双方向リンクへ

互いに相手の機能を使用する必要がある2つのクラスがあり、一方向のリンクしかない場合。
・バックポインタを追加し、修飾子を変更して両方のセットを更新する。
動機
参照されるオブジェクトが、自分を参照するオブジェクトにアクセスする必要がある場合。

class Order
  def initialize(customer)
    @customer = customer
  end
  
  def customer
    @customer
  end
  
  def customer=(customer)
    @customer.friend_orders.delete(self) if @customer
    @customer = customer
    @customer.friend_orders << self if @customer
  end
end

class Customer
  def initialize(name)
    @name = name
    @orders = []
  end
  
  def add_order(order)
    order.customer = self
    @orders << order
  end
  
  def remove_order(order)
    order.customer = nil
    @orders.delete(order)
  end
  
  def orders
    @orders.dup.freeze
  end
  
  def friend_orders
    @orders
  end
end

8.8 双方向リンクから片方向リンクへ

2方向の関連がありますが、1つのクラスはもう他のクラスの機能が必要ありません。
・関連の不要な側を削除する。

動機
双方向の関連が必要でない場合は、複雑さを減らし、ゾンビオブジェクトを処理し、相互依存関係を排除します。

8.9 マジックナンバーからシンボル定数へ

マジックナンバーを意味がわかる名前の定数に置き換える。
(感想: とても重要。これは今後必ず行う。)

リファクタ前
def sample_method(number)
  number * 3.14
end
リファクタ後
PI = 3.14

def sample_method(number)
  number * PI
end

8.10 コレクションのカプセル化

あるメソッドがコレクションを返している。
読み取り専用のビューを返し、追加・削除用のメソッドを提供する。

動機

カプセル化することで、所有クラスとそのクライアントの結合度が低減される。
ゲッターはコレクションオブジェクト自体を返すべきではない。
ゲッターは、コレクションの操作を防止し、不必要な詳細を隠すものを返すべきである。
コレクションに対するセッターは存在しないべきで、要素の追加・削除に対応する操作のみを提供するべきである。

リファクタ前
class Person
  def initialize(name)
    @name = name
    @courses = []
  end
  
  def courses
    @courses
  end

リファクタ
class Person
  def initialize(name)
    @name = name
    @courses = []
  end
  
  def courses
    @courses.dup.freeze
  end

  def add_course(course)
    @courses << course
  end
  
  def remove_course(course)
    @courses.delete(course)
  end
end

8.11 レコードからデータクラスへ

伝統的なプログラミング環境でレコード構造とやり取りする必要があります。
レコード用にダムなデータオブジェクトを作成してください。

動機

既存のプログラムをコピーする場合。
伝統的なプログラミングAPIやデータベースで構造化されたレコードを通信する場合。

8.12 タイプコードからポリモーフィズムへ

  1. サブクラスによるタイプコードの置き換え(Replace Type Code with Subclasses)
    クラスに数値のタイプコードがあり、そのコードが動作に影響を与えません。
    その数値を新しいクラスに置き換えてください。
リファクタ前

class Employee
  ENGINEER = 0
  SALESMAN = 1
  MANAGER = 2

  def initialize(type)
    @type = type
  end

  def pay_amount
    case @type
    when ENGINEER
      @monthly_salary
    when SALESMAN
      @monthly_salary + @commission
    when MANAGER
      @monthly_salary + @bonus
    else
      raise 'Incorrect Employee'
    end
  end
end
リファクタ
class Employee
  def initialize(type)
    @type = EmployeeType.new(type)
  end

  def pay_amount
    @type.pay_amount(@monthly_salary, @commission, @bonus)
  end
end

class EmployeeType
  ENGINEER = 0
  SALESMAN = 1
  MANAGER = 2

  def self.new(type)
    case type
    when ENGINEER
      Engineer.new
    when SALESMAN
      Salesman.new
    when MANAGER
      Manager.new
    else
      raise 'Incorrect Employee Code'
    end
  end

  def pay_amount(salary, commission, bonus)
    raise 'Not Implemented'
  end
end

class Engineer < EmployeeType
  def pay_amount(salary, _commission, _bonus)
    salary
  end
end

class Salesman < EmployeeType
  def pay_amount(salary, commission, bonus)
    salary + commission
  end
end

class Manager < EmployeeType
  def pay_amount(salary, _commission, bonus)
    salary + bonus
  end
end

8.13 タイプコードからモジュールのextend

8.14 タイプコードからState/Strategyへ

クラスに数値のタイプコードがあるが、サブクラス化はできない。
状態オブジェクトまたは戦略オブジェクトでタイプコードを置き換える
動機

タイプコードに応じて異なるコードを実行するとき
各タイプコードオブジェクトに固有の機能がある場合
30. クラスによるタイプコードの置き換え(Replace Type Code with Class)を実装するための構造
38. 条件記述の削除(Replace Conditional with Polymorphism)を実現するための構造
31. サブクラスによるタイプコードの置き換え(Replace Type Code with Subclasses)と32. サブクラスによるタイプコードの置き換え(Replace Subclass with Fields)が適用できない場合に使用する。

class Employee
  ENGINEER = 0
  SALESMAN = 1
  MANAGER = 2

  attr_reader :type
  attr_accessor :monthly_salary, :commission, :bonus

  def initialize(type)
    @type = EmployeeType.new_type(type)
  end

  def pay_amount
    @type.pay_amount(self)
  end
end

class EmployeeType
  def initialize(code)
    @code = code
  end

  def self.new_type(code)
    case code
    when Employee::ENGINEER
      Engineer.new
    when Employee::SALESMAN
      Salesman.new
    when Employee::MANAGER
      Manager.new
    else
      raise ArgumentError, 'Incorrect Employee Code'
    end
  end

  def pay_amount(_employee)
    raise NotImplementedError, "abstract method"
  end
end

class Engineer < EmployeeType
  def pay_amount(employee)
    employee.monthly_salary
  end
end

class Salesman < EmployeeType
  def pay_amount(employee)
    employee.monthly_salary + employee.commission
  end
end

class Manager < EmployeeType
  def pay_amount(employee)
    employee.monthly_salary + employee.bonus
  end
end

8.15 サブクラスからフィールドへ

条件: 定数を返すメソッド以外には違いがない複数のサブクラス(子クラス)がある場合。
メソッドをスーパークラス(親クラス)のフィールドに変えて、サブクラスを削除する。

リファクタ前
class Person
  # ..
end

class Female < Person
  def female?
    true
  end

  def code
    'F'
  end
end

class Male < Person
  def female?
    false
  end

  def code
    'M'
  end
end
リファクタ後
class Person
  def initialize(female, code)
    @female = female
    @code = code
  end

  def female?
    @female
  end

  def code
    @code
  end

  def self.create_female
    Person.new(true, 'F')  # フィールドに詰めて初期化する
  end

  def self.create_male
    Person.new(false, 'M')
  end
end

8.16 属性初期化の遅延実行 , 8.17 属性初期化の先行実行

属性を初期化するタイミングをどうするか(どちらがいいか)という話。
(著者の周りでも2つの意見で5分に分かれたと記載ある。)
メリット・デメリットがあるため、どちらの手法を取るかはチームで決めればいい。

属性初期化の先行実行
class Sample
  def initialize
    @samples = []
  end
end

属性初期化の遅延実行
class Sample
  def samples
    # メリット: initializeでいちいち初期化しなくていい,コードが減る
    # デメリット: アクセスの度に値が変わるためデバッグしにくい
    @samples ||= [] 
  end
end

寄り道
上で出てきた ||= はRuby独自の記法。メモ化やローカルキャッシュと呼ぶ。他にも便利なイディオムがあるはずだと思い調べた。

上記サイトを参考に、メモする。

def xxx!
この場合の「!」はメソッド名の一部です。
慣用的に、同名の(! の無い)メソッドに比べてより破壊的な作用をもつメソッドで使われます。
(例: map と map!)

def xx?
この場合の「?」はメソッド名の一部分です。
慣用的に、真偽値を返すメソッドを示すために使われます。

def xxx(&yyy)
&がついた引数はブロック引数。

xxx&.yyy
ぼっち演算子。
xxxがnilでないときにメソッドyyyを呼び出す。

a ||= xxx
「||」演算子の自己代入演算子。
aが偽か未定義 なら aにxxxを代入する。

a ||= :some
p a #=> some
a ||= :sec
p a #=> some # aは定義済みだったので、:secはaに代入されなかった

ls
コマンド出力。
バッククォート(`)で囲まれた文字列は、コマンドとして実行され、その標準出力が文字列として与えられます。

p ls
#=> "README.md\ndata.txt\nrefactoring_1.rb\nrefactoring_2.rb\n"

9章 条件式の単純化

9.1 条件文の分解

条件部からメソッドを抽出する。

条件: 条件が複雑な場合。
(感想: とても重要。よく使用される条件はメソッドに抽出することで再利用しやすくなる。今後複雑な条件を書く必要が出た場合は、必ず検討をする。)

リファクタ前
if number >= 10 || number <= -10
  p "絶対値が10以上"
else
  p "絶対値が10未満"
end
リファクタ後
if is_over_ten(number)
  p "絶対値が10以上"
else
  p "絶対値が10未満"
end

def is_over_ten(number)
  number >= 10 || number <= -10
end

9.2 条件分岐の組み替え

Rubyらしく書くことで読みやすくする。
(感想: とても重要。Rubyらしさ、PHPらしさ...というのは言語特有なので、言語を深く知ることはこういうときに力を発揮する。)

三項演算子  OR代入 
samples = params.present? ? params : []
# ↓
samples = params : []

条件分岐  明示的なreturn 
def sample_method(number)
  if sample > 10
    1
  else
    2
  end
end
# ↓
def sample_method(number)
  return 1 if sample > 10
  2
end

9.3 条件式の統合

条件: 同じ結果になる条件が複数ある場合。
それらを1つにまとめ、メソッドとして抽出する。

リファクタ前
def sample_method
  return 0 if @data.size > 5
  return 0 if @amount > 10 # 結果は上と同じ
  1
end
リファクタ後
def sample_method
  return 0 if sample_condition
  1
end

def sample_condition
  @data.size > 5 || @amount > 10
end

9.4 重複する条件分岐の断片の統合

条件分岐の各ブランチに同じコードがある場合、条件式の外に移動する。
動機

何が変わり、何が変わらないかがより明確になる。

リファクタ前
if special_deal?
  total = price * 0.95
  send
else
  total = price * 0.98
  send
end

リファクタ
if special_deal?
  total = price * 0.95
else
  total = price * 0.98
end

send

9.5 制御フラグの除去

breakやreturnを使って、制御フラグを除去する。

条件: 制御フラグがある場合。
break使用

リファクタ前
def check_security(people)
  found = false # 制御フラグ
  people.each do |person|
    unless found
      if person == "Taro"
        send_alert
        found = true # ある条件に合致したら制御フラグを切り替え
      end
      if person == "Hanako"
        send_alert
        found = true
      end
    end
  end
end
リファクタ後
def check_security(people)
  people.each do |person|
    if person == "Taro"
      send_alert
      break # ループを抜ける
    end
    if person == "Hanako"
      send_alert
      break
    end
  end
end
# まだリファクタの余地があるが、別の話になるので割愛

return使用

リファクタ前
def check_security(people)
  # 制御フラグ かつ 別で使用する値
  found = "" # 初期値
  people.each do |person|
    if found == ""
      if person == "Taro"
        send_alert
        found = "Taro"
      end
      if person == "Hanako"
        send_alert
        found = "Hanako"
      end
    end
  end
  some_later_code(found)
end
リファクタ後
def check_security(people)
  found = found_miscreant(people) # 2つのことをしていたので、メソッドに切り出し
  some_later_code(found)
end

# foundの値を返すメソッド
def found_miscreant(people)
  people.each do |person|
    if person == "Taro"
      send_alert
      return "Taro" # 早期return
    end
    if person == "Hanako"
      send_alert
      return "Hanako"
    end
    end
  end
  "" # 条件に合致しなかった → 初期値のまま
end

9.6 条件分岐のネストからガード節へ

条件:
条件分岐によって、正常な実行経路がわかりづらい場合。
異常な状態を表す条件(特殊条件)がある場合。
特殊条件をガード節で処理(早期リターン)する。
(感想: とても重要。使い分けの指針を下記した。)

条件分岐には2種類ある。

分岐先のいずれも(if/else)、正常な振る舞いで、同じぐらい実行され、同じくらい重要な場合。 → if/else
分岐先の片方が、異常状態を表したり、まれなケースな場合。 → ガード節

リファクタ前
# 支払う給与
def pay_amount
  if @dead
    result = dead_amount # 死亡
  else
    if @separated
      result = separated_amount # 別居中
    else
      if @retired
        result = retired_amount # 退職
      else
        result = normal_pay_amount # 通常
      end
    end
  end
  result
end
リファクタ後
def pay_amount
  # 特殊な条件は早期return
  return dead_amount if @dead # 死亡 
  return separated_amount if @separated # 別居中
  return retired_amount if @retired # 退職

  normal_pay_amount # 通常
end

条件式を逆にしながら<条件分岐のネストからガード節へ>
(感想: 条件を逆にしたほうがすっきりするときがある ということだと思う。)

リファクタ前
def adjussted_capital
  result = 0
  if @capital > 0
    if @interest_rate > 0 && @duration > 0
      result = ( @income / @duration) * ADJ_FACTOR
    end
  end
  result
end
リファクタ後
def adjussted_capital
  return 0 if @capital <= 0 # 条件を逆にした
  return 0 if @interest_rate <= 0 || @duration <= 0 # 条件を逆にした
  (@income / @duration) * ADJ_FACTOR
end

9.7 条件分岐からポリモーフィズムへ

(ポリモーフィック関連の)オブジェクトの種類によって分岐する条件分岐を削除し、その分岐先の処理をオブジェクトに移す。

オブジェクトのタイプに応じて異なる動作を選択する条件があります。
条件の各脚をサブクラスのオーバーライドメソッドに移動し、元のメソッドを抽象化します。

動機

オブジェクトのタイプに応じて動作が異なる場合、明示的な条件文を避けます。
スイッチ文は、オブジェクト指向プログラムではあまり使われないべきです。

リファクタ前
class Employee
  ENGINEER = 0
  SALESMAN = 1
  MANAGER = 2

  def initialize(type)
    @type = type
  end

  def pay_amount
    case @type
    when ENGINEER
      _monthly_salary
    when SALESMAN
      _monthly_salary + _commission
    when MANAGER
      _monthly_salary + _bonus
    else
      raise "Incorrect Employee"
    end
  end
end
リファクタ
class Employee
  ENGINEER = 0
  SALESMAN = 1
  MANAGER = 2

  def set_type(arg)
    @type = EmployeeType.new_type(arg)
  end

  def pay_amount
    @type.pay_amount(self)
  end
end

class Engineer
  def pay_amount(employee)
    employee.monthly_salary
  end
end

ポリモーフィズム(多相性)
多岐にわたるオブジェクトが、同じメッセージに応答できる能力。

9.8 nullオブジェクトの導入

null値の繰り返しチェックがあります。
null値をnullオブジェクトに置き換えます

動機

オブジェクトは、そのタイプに応じて適切なことを行うべきです。Nullオブジェクトもこのルールに従うべきです。
Nullオブジェクトパターンは、特殊ケースパターンの弟分です。

リファクタ前
if customer.nil?
  plan = BillingPlan.basic
else
  plan = customer.plan
end
リファクタ
class Customer
end

class NullCustomer < Customer
end

9.9 アサーションの導入

コードの一部がプログラムの状態について何かを仮定しています。
アサーションを使って仮定を明示的にします

アサーションは、常に真であると仮定される条件文です。
アサーションの失敗は、常にチェックされない例外を引き起こすべきです。
アサーションは通常、本番コードでは削除されます。
コミュニケーションの助けとして: アサーションは、コードがどのような仮定をしているかを読者に理解させるのに役立ちます。
デバッグの助けとして: アサーションは、バグを発生源に近いところでキャッチするのに役立ちます。

リファクタ前

def get_expense_limit
  # should have either expense limit or a primary project
  return (_expense_limit != NULL_EXPENSE) ?
    _expense_limit :
    _primary_project.member_expense_limit
end
リファクタ
def get_expense_limit
  assert(_expense_limit != NULL_EXPENSE || !_primary_project.nil?)
  return (_expense_limit != NULL_EXPENSE) ?
    _expense_limit :
    _primary_project.member_expense_limit
end

10章 メソッドの呼び出しの単純化

10.1 メソッド名の変更

メソッド名を修正。

条件: メソッド名からメソッドの目的がわからない場合。

# def sample_method (悪い)
def full_name
  "#{name_last} #{name_first}"
end

10.2 引数の追加

条件: メソッドの振る舞いの変更により。引数を追加しないといけない。
引数を追加する。
ただし、代わりになる方法を考えてから行うこと。

オブジェクトに問い合わせればいい場合
オブジェクトを渡したほうがいいのでは?
逆に、そのオブジェクトに情報を提供するメソッドを追加するほうがいいのでは?

メソッドは呼び出し元からさらなる情報を必要とする。
この情報を渡すことができるオブジェクトのパラメータを追加する

動機
メソッドを変更した後、さらなる情報が必要となる。

リファクタ前

get_contact()
リファクタ
get_contact(date)

10.3 引数の削除

メソッド本体で使用されなくなったパラメータがある。
それを削除する
動機
パラメータがもはや必要ではない。

リファクタ前
get_contact(date)
リファクタ
get_contact()

10.4 問い合わせと更新の分離

問い合わせ用と更新用の2つのメソッドを作る。

条件: 値を返すとともに、オブジェクトの状態に変更を加えるメソッドがある場合。
理由: 副作用がないメソッドは、考えなければならないことが大幅に減る。

値を返すがオブジェクトの状態も変更するメソッドがある。
クエリ用のメソッドと変更用のメソッドの2つに分ける

リファクタ前
get_total_out_standing_and_set_ready_for_summaries()
リファクタ
get_total_out_standing()
set_ready_for_summaries()

副作用があるメソッドか、副作用がないメソッドかどうかを明確にする。
メソッドの返り値が呼び出し元で使われている かつ 副作用がある場合は分離すべき。
(感想: 重要。使い手・読み手が考えることが少なくなり、結果として可読性も向上する。副作用があるかどうかは命名で明確にすること。GraphQLではQueryとMutationでこの考え方をしていたが、Railsでは考えたことが無かった。)

10.5 メソッドのパラメータ化

条件: 複数のメソッドが異なる値を使って、同じようなことをしている場合。
その異なる値を引数とする1つのメソッドにまとめる。

類似した処理を行うが、メソッド本体に含まれる値が異なるいくつかのメソッドがある。
異なる値をパラメータとして使用する1つのメソッドを作成する

動機
重複コードを削除し、柔軟性を向上させる。

リファクタ前
five_percent_raise()
ten_percent_raise()
リファクタ
raise(percentage)

10.6 引数から別々のメソッドへ

列挙パラメータの値に応じて異なるコードを実行するメソッドがある。
パラメータの各値に対して別々のメソッドを作成する

動機

条件付きの振る舞いを避ける
コンパイル時のチェックを得る
インターフェースが明確になる

リファクタ前
def set_value(name, value)
  if name == "height"
    @_height = value
    return
  end
  if name == "width"
    @_width = value
    return
  end
  raise "should never reach here"
end
リファクタ
def set_height(arg)
  @_height = arg
end

def set_width(arg)
  @_width = arg
end

10.7 オブジェクト自体の受け渡し

オブジェクトから複数の値を取得し、それらの値をメソッド呼び出しのパラメータとして渡す。
代わりにオブジェクト全体を送る

動機

パラメータリストが変更に強くなる
コードがより読みやすくなる
渡されたオブジェクトで既に行われている可能性のある重複コードを削除する
マイナス:パラメータオブジェクトと呼び出されるオブジェクトの間に依存関係が生じる

リファクタ前
low = days_temp_range().low
high = days_temp_range().high
within_plan = plan.within_range(low, high)
リファクタ
within_plan = plan.within_range(days_temp_range())

10.8 引数からメソッドへ

オブジェクトがメソッドを呼び出し、その結果をメソッドのパラメータとして渡す。呼び出されたオブジェクトもこのメソッドを呼び出すことができる。
パラメータを削除し、呼び出されたオブジェクトにメソッドを呼び出させる

動機

メソッドが別の方法で渡されるパラメータとしての値を取得できる場合、それを行うべきである。
受信メソッドが同じ計算を行うことができる場合(呼び出しメソッドのパラメータを参照しない)
呼び出し元オブジェクトへの参照を持つ別のオブジェクト上でメソッドを呼び出す場合。

リファクタ前
base_price = @_quantity * @_item_price
discount_level = get_discount_level()
final_price = discounted_price(base_price, discount_level)
リファクタ
base_price = @_quantity * @_item_price
final_price = discounted_price(base_price)

10.9 引数オブジェクトの導入

条件: 自然にまとめられる引数のグループがある場合。
それらの引数を、オブジェクトにする。

いくつかのパラメータが常に一緒に渡される。
それらのパラメータをまとめて1つのオブジェクトにする

動機

メソッドシグネチャがより明確になる
データをクラスタ化し、コードの意図を明確にする
オブジェクトを操作する新しいメソッドを追加することが容易になる

リファクタ前
def amount_in_range(min_amount, max_amount)
  # ...
end
リファクタ
class AmountRange
  attr_reader :min_amount, :max_amount

  def initialize(min_amount, max_amount)
    @min_amount = min_amount
    @max_amount = max_amount
  end
end

def amount_in_range(amount_range)
  # ...
end

10.10 設定メソッドの削除

条件: インスタンス作成時に設定して、その後は変更すべきでないフィールドがある場合。
設定メソッド(セッター)を削除する。
→ attr_writer(attr_accessor) や def フィールド=(value)を削除する。

フィールドは作成時に設定され、それ以降は変更されないべきです。
そのフィールドに対する設定メソッドを削除してください

動機
意図を明確にする: 一度作成されたフィールドを変更したくない場合は、設定メソッドを提供しない(そしてフィールドをfinalにする)。

リファクタ前
class Employee
  def set_immutable_value
  end
end
リファクタ
class Employee
  # hogehoge
end

10.11 メソッドの隠蔽

あるメソッドが他のクラスで使用されていない場合。
メソッドをprivateにする

動機
メソッドがクラスの外部で必要ない場合は、隠すべきです。

class Employee
  def method
  end
end

から

class Employee
  private

  def method
  end
end

10.12 コンストラクタからファクトリメソッドへ

条件: オブジェクトを生成するときに、単なる構築以上のことをしたい場合。
コンストラクタを取り除いて、ファクトリメソッドを作る。

コンストラクタ : クラスからインスタンスを作る時に実行される処理

リファクタ前
class ProductController
  def create
    # どういう値かによって、作るプロダクトの種類が異なる
    # → この判定をオブジェクトクラスでやっていないことが問題。色々な箇所で毎回このロジックを書く必要があるため。
    @product = if imported
                  ImportedProduct.new(base_price)
                else
                  if base_price > 1000
                    LuxuryProduct.new(base_price)
                  else
                    Product.new(base_price)
                  end
                end
  end
end
リファクタ後
class ProductController
  def create
    @product = Product.create(base_price, imported)
  end
end

class Product
  # ファクトリメソッドへ
  def self.create(base_price, imported=false)
    if imported
      ImportedProduct.new(base_price)
    else
      if base_price > 1000
        LuxuryProduct.new(base_price)
      else
        Product.new(base_price)
      end
    end
  end
end

オブジェクトの作成時に、単純な構築を超えた処理が必要です。
コンストラクタをファクトリーメソッドに置き換える
動機
サブクラス(タイプ)に応じたオブジェクトを作成する。コンストラクタは要求されたオブジェクトのインスタンスのみを返すことができるため、ファクトリーメソッドが必要です。

class Employee
  def initialize(type)
    @type = type
  end
end

から

class Employee
  def self.create(type)
    new(type)
  end

  private

  def initialize(type)
    @type = type
  end
end

10.13 エラーコードから例外へ

条件: エラーを示すために、特別なエラーコード(エラーと知らせるための変な値)を返している。
代わりに例外を生成する。

メソッドがエラーを示す特別なコードを返します。
代わりに例外をスローする

動機
エラーを検出したプログラムがどう対処すべきかわからない場合、それを呼び出し元に知らせる必要があり、呼び出し元はエラーをチェーンに渡すことがあります。

リファクタ前
def withdraw(amount)
  if amount > @balance
    return -1
  else
    @balance -= amount
    return 0
  end
end

から

リファクタ
def withdraw(amount)
  raise BalanceException if amount > @balance
  @balance -= amount
end

10.14 例外からテストへ

呼び出し元が最初にチェックできる条件でチェックされた例外がスローされています。
呼び出し元に最初にテストを行うように変更する

動機

例外を過剰に使用しないでください。それらは例外的な動作(予期しない動作)に使用する必要があります。
条件テストの代替品として使用しないでください。
操作を呼び出す前に、予想される間違った条件をチェックしてください。

リファクタ前
def get_value_for_period(period_number)
  begin
    return @values[period_number]
  rescue IndexError
    return 0
  end
end

から

リファクタ
def get_value_for_period(period_number)
  return 0 if period_number >= @values.length
  return @values[period_number]
end

10.15 ゲートウェイの導入

10.16 式ビルダーの導入

11章 一般化の処理

11.1 メソッドの上位階層への移動

複数の子クラスで同じ結果になるメソッドは、親クラスへ移動する。

2つのサブクラスが同じフィールドを持っています。
フィールドをスーパークラスに移動する

動機

重複したデータ宣言を削除します。
サブクラスからスーパークラスに動作を移動できます。

リファクタ前
class Salesman < Employee
  attr_accessor :name
end

class Engineer < Employee
  attr_accessor :name
end
リファクタ
class Employee
  attr_accessor :name
end

class Salesman < Employee; end
class Engineer < Employee; end

11.2 メソッドの下位階層への移動

サブクラスで同じ結果のメソッドがあります。
それらをスーパークラスに移動する

動機

重複した動作を排除します。

リファクタ
class Salesman < Employee
  def name; end
end

class Engineer < Employee
  def name; end
end
リファクタ
class Employee
  def name; end
end

class Salesman < Employee; end
class Engineer < Employee; end

11.3 モジュールの抽出

条件: 複数のクラスに重複する振る舞いがある場合。
振る舞いを新たなモジュールに移動させ、includeする。

重複を取り除くときは、できるだけクラスの抽出を使う。
ただし、他の(第3の)クラスに再利用できないなら、モジュールの抽出を使う。

11.4 モジュールのインライン化

11.5 サブクラスの抽出

条件: クラスが、一部のインスタンスしか使わないフィールドがある。
そのフィールドのために子クラスを作る。

リファクタ前
class JobItem
  attr_reader :quantity, :employee

  def initialize(unit_price, quantity, is_labor, employee)
    @unit_price = unit_price
    @quantity = quantity
    @is_labor = is_labor
    @employee = employee
  end

  def total_price
    unit_price * @quantity
  end

  def unit_price
    # labor(修理するのが大変)かどうかで振る舞いが変わる
    labor? ? @employee.rate : @unit_price
  end

  def labor?
    @is_labor
  end
end
リファクタ後
class JobItem
  attr_reader :unit_price, :quantity

  # employeeは不要になった
  def initialize(unit_price, quantity, is_labor=false)
    @unit_price = unit_price
    @quantity = quantity
    @is_labor = is_labor
  end

  def total_price
    unit_price * @quantity
  end

  def labor?
    false
  end
end

# 子クラス
class LaborItem < JobItem
  attr_reader :employee

  # LaborItemの生成には、unit_price, is_laborは不要
  def initialize(quantity, employee)
    super(0, quantity, true) # is_laborはtrue
    @employee = employee
  end

  def labor?
    true
  end

  def unit_price
    @employee.rate
  end
end

11.6 継承の導入

条件: 同じような機能を持つクラスが2つある場合。
継承を使い、親子にする。

継承の導入を使うか迷うような場合は、クラスの抽出を使えばいい。
クラスの抽出が使えないなら、モジュールの抽出。
2つのクラスが振る舞いだけでなく、インターフェイスも同じなら、継承の導入を使う。

リファクタ前
class MountaionBike
  TIRE_WIDTH_FACTOR = 6
  attr_accessor :tire_diameter

  def wheel_circumference
    Math::PI * (@wheel_diameter + @tire_diameter)
  end

  def off_road_ability
    @tire_diameter * TIRE_WIDTH_FACTOR
  end
end

class FrontSuspensionMountainBike
  TIRE_WIDTH_FACTOR = 6
  FRONT_SUSPENTION_FACTOR = 8
  attr_accessor :tire_diameter, :front_fork_travel

  def wheel_circumference
    # MountaionBikeと同じ
    Math::PI * (@wheel_diameter + @tire_diameter)
  end

  def off_road_ability
    # MountaionBikeと少し違う
    @tire_diameter * TIRE_WIDTH_FACTOR + @front_fork_travel * FRONT_SUSPENTION_FACTOR
  end
end
リファクタ後
class MountaionBike
  TIRE_WIDTH_FACTOR = 6
  attr_accessor :tire_diameter

  def wheel_circumference
    Math::PI * (@wheel_diameter + @tire_diameter)
  end

  def off_road_ability
    @tire_diameter * TIRE_WIDTH_FACTOR
  end
end

# MountaionBikeの派生なので、MountaionBikeの子クラスにする
class FrontSuspensionMountainBike < MountaionBike
  FRONT_SUSPENTION_FACTOR = 8
  attr_accessor :front_fork_travel

  def off_road_ability
    # MountaionBikeの結果から、FrontSuspensionMountainBike独自の計算をする
    super + @front_fork_travel * FRONT_SUSPENTION_FACTOR
  end
end

11.7 階層構造の統合

スーパークラスとサブクラスの違いがほとんどありません。
それらを一緒にマージする

動機
価値を追加していないサブクラスがある場合。

リファクタ前
class Employee; end
class Salesman < Employee; end
リファクタ

class Employee; end

11.8 テンプレートメソッドの作成

条件: 同じような処理をするメソッドが別々の子クラスにあるが、内容がわずかに違う。
違いのある処理を抽出して、子クラスで同じメソッド名で実装。
共通する処理を親クラスで実装。

継承を使ったテンプレートメソッド

リファクタ前
# レシート表示
def statement
  result = "レンタル #{name}"
  @rentals.each do |rental|
    result << "#{rental.movie.title} #{rental.charge}" # 映画のタイトル、料金
  end
  result << "合計#{total_charge}"
  result << "#{total_frequent_rental_points}"
  result
end

# レシートをHTMLで表示(なのでstatementと少し違う)
def html_statement
  result = "<h1>レンタル #{name}</h1>"
  @rentals.each do |rental|
    result << "#{rental.movie.title} #{rental.charge}<br/>"
  end
  result << "<p>合計#{total_charge}</p>"
  result << "<p>#{total_frequent_rental_points}</p>"
  result
end
リファクタ後
# クライアントコード``````
class Customer
  def statement
    TextStatement.value(self)
  end

  def html_statement
    HtmlStatement.value(self)
  end
end
# ````````

class Statement
  # 2つの子クラスで共通している処理
  def value(customer)
    result = header_string(customer)
    customer.rentals.each do |rental|
      result << each_rental_string(rental)
    end
    result << footer_string(customer)
  end
end

# 子クラス側では、独自の実装をする
class TextStatement < Statement
  def header_string(customer)
    "レンタル #{customer.name}"
  end

  def each_rental_string(rental)
    "#{rental.movie.title} #{rental.charge}"
  end

  def footer_string(customer)
    <<-EOS
      "合計#{customer.total_charge}"
      result << "#{customer.total_frequent_rental_points}"
    EOS
  end
end

class HtmlStatement < Statement
  def header_string(customer)
    "<h1>レンタル #{customer.name}</h1>"
  end

  def each_rental_string(rental)
    "#{rental.movie.title} #{rental.charge}<br/>"
  end

  def footer_string(customer)
    <<-EOS
      "<p>合計#{customer.total_charge}</p>"
      "<p>#{customer.total_frequent_rental_points}</p>"
    EOS
  end
end

モジュールのextendを使ったテンプレートメソッド
Statementクラスのインスタンスを作ることがない場合は、モジュールでextendするのが良い。

継承を使った場合、親クラスは1つだけなのに対し、
extendを使うと、複雑な継承の問題を避けられる。

extend
extendを使うと、moduleのメソッドをそのオブジェクトのインスタンスメソッドとして取り込むことができる。

リファクタ後
# クライアントコード``````
class Customer
  def statement
    Statement.new.extend(TextStatement).value(self)
  end

  def html_statement
    Statement.new.extend(HtmlStatement).value(self)
  end
end
# ````````

class Statement
  # ...
end

module TextStatement
  # ...
end

module HtmlStatement
  # ...
end

参考記事

→ 名前空間を作ってモジュール名やメソッド名の衝突を防ぐ。

→ ActiveSupport::Concernについても記載あり。

サブクラスの2つのメソッドが、同じ順序で同様のステップを実行していますが、ステップが異なります。
同じシグネチャでメソッドにステップを取得し、元のメソッドが同じになるようにします。その後、それらを引き上げることができます
動機

継承とポリモーフィズムを使用して、わずかに異なる重複動作を排除します。
サブクラスに似たメソッドがある場合、スーパークラスでそれらをまとめます。

リファクタ前

class Site; end

class ResidentialSite < Site
  def billable_amount; end
end

class LifelineSite < Site
  def billable_amount; end
end
リファクタ

class Site
  def billable_amount; end
  def base_amount; end
  def tax_amount; end
end

class ResidentialSite < Site
  def base_amount; end
  def tax_amount; end
end

class LifelineSite < Site
  def base_amount; end
  def tax_amount; end
end

11.9 継承から委譲へ

条件: 子クラスが親クラスのインターフェイスの一部しか使っていない or データを継承することが望ましくない 場合。
親クラスに処理を委譲し、継承構造を解消する。

サブクラスはスーパークラスのインターフェイスの一部のみを使用しているか、データを継承したくありません。
スーパークラスのフィールドを作成し、委譲にメソッドを調整し、サブクラスを削除する

動機

委譲を使用すると、デリゲートされたクラスの一部のみを使用していることが明確になります。
インターフェースのどの側面を取るか、どの側面を無視するかを制御できます。

リファクタ前
class Vector
  def empty?; end
end

class Stack < Vector; end
リファクタ
class Vector
  def empty?; end
end

class Stack
  def initialize
    @vector = Vector.new
  end

  def empty?
    @vector.empty?
  end
end

11.10 委譲から継承へ

条件: 多数の委譲メソッドを書いている。
委譲先のクラスをモジュールにして、委譲元のクラスでincludeする。
(<11.9 継承から委譲へ>の逆だが、基本的には継承でなくモジュールを使って階層を作る。)

あなたは委譲を使っていて、しばしばインターフェース全体のために多くのシンプルな委譲を書いています。
デリゲートをサブクラスにすることで、デリゲートクラスを作成します

動機

デリゲートのすべてのメソッドを使用している場合。
デリゲートしているクラスのメソッドをすべて使用していない場合は、それを使用すべきではありません。
注意すべきは、デリゲートが複数のオブジェクトに共有され、変更可能である場合です。データ共有は、継承に戻すことはできません。

リファクタ前
class Person
  def name; end
end

class Employee
  def initialize
    @person = Person.new
  end

  def name
    @person.name
  end
end
リファクタ
class Person
  def name; end
end

class Employee < Person; end

11.11 抽象スーパークラスからモジュールへ

条件: 継承階層を持っているが、親クラスのインスタンスは作るつもり(予定)がない。
継承→モジュールに書き換える。

12章 大規模なリファクタリング

この章では、6~11章のような1つ1つの指し手ではなく、試合全体を説明している。

12.1 リファクタリングという試合の性質

12.2 大規模リファクタリングが重要な理由

12.3 4つの大規模リファクタリング

12.4 複合的な継承階層の分割

条件: 2つの仕事をしている継承階層がある。
理由: 継承関係がもつれると、コードの重複が発生し、メンテしづらいコードになるため。また、単一責務でないため。
2つの階層に分け、片方からもう片方を実行するには委譲を使う。

一度に2つの仕事をしている継承階層があります。
2つの階層を作成し、一方から他方を呼び出すために委譲を使用します

動機

入り組んだ継承はコードの重複を引き起こします。
階層内の特定のレベルのすべてのクラスが、同じ形容詞で始まるサブクラスを持っている場合、おそらく1つの階層で2つの仕事をしています。
手順の注意
階層で行われている異なる仕事を特定します。2次元(またはx次元)グリッドを作成し、異なる仕事で軸にラベルを付けます。

リファクタ前
class Deal; end
class ActiveDeal < Deal; end
class PassiveDeal < Deal; end
class TabularActiveDeal < ActiveDeal; end
class TabularPassiveDeal < PassiveDeal; end

から

リファクタ
class Deal
  attr_accessor :presentation_style
end

class ActiveDeal < Deal; end
class PassiveDeal < Deal; end

class PresentationStyle; end
class TabularPresentationStyle < PresentationStyle; end
class SinglePresentationStyle < PresentationStyle; end

12.5 手続き型設計からオブジェクト指向設計へ

(感想: 本書にははっきりとした理由が書いていなかった。なので、手続き型/オブジェクト指向のメリット/デメリットを調べた。再利用ができない点、値がグローバルである点が手続き型のデメリットであり、大きなシステムになるとそのデメリットが膨れ上がると感じた。)

手続き型スタイルで書かれたコードがあります。
データレコードをオブジェクトに変換し、動作を分割し、動作をオブジェクトに移動します

動機
OOP(少なくともJAVAで)を使用する

リファクタ前
class OrderCalculator
  def determine_price(order); end
  def determine_taxes(order); end
end

class Order; end
class OrderLine; end`

リファクタ
class Order
  def price; end
  def taxes; end
end

class OrderLine
  def price; end
  def taxes; end
end

12.6 ドメインのプレゼンテーションからの分離

ドメインロジックはモデルに移す。

ドメインロジックを含むGUIクラスがあります。
ドメインロジックを別のドメインクラスに分離します

動機

プログラムの2つの複雑な部分を、より簡単に変更できる部分に分割します。
同じビジネスロジックの複数のプレセンテーションを可能にします。

リファクタ前
class OrderWindow; end
リファクタ
class OrderWindow
  attr_accessor :order
end

ビューには、ユーザーインターフェイスを処理するために必要なロジックだけを収める。

12.7 継承階層の抽出

多くの条件文を通じて、あまりにも多くの作業をしているクラスがあります。
特殊なケースを表す各サブクラスを持つクラスの階層を作成します

動機

1つのアイデアを実装するクラスが2つ、3つ、10つになります。
単一責任の原則を維持してください。

リファクタ前
class BillingScheme; end
リファクタ
class BillingScheme; end
class BusinessBillingScheme < BillingScheme; end
class ResidentialBillingScheme < BillingScheme; end
class DisabilityBillingScheme < BillingScheme; end

参考文献
復刊ドットコム: リファクタリング:Rubyエディション
ジェイ・フィールズ シェーン・ハービー マーティン・ファウラー 著 / 長尾高弘 訳

3
3
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
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?