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

## はじめに

<img src='https://qiita-image-store.s3.amazonaws.com/0/186009/5d219e32-cd26-4cbd-1390-ad61e884ade0.jpeg' height=300px >

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

第6章から第11章まではリファクタリングの具体的なテクニックがまとめられています。
今回は `リファクタリング: Rubyエディション` の `第6章 メソッドの構成方法` の中のいくつかのテクニックについて、まとめました。

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

## 具体例一覧

- [Extract Method](https://qiita.com/tamago3keran/items/898936b716984cd55b1f#extract-method)
- [Inline Method](https://qiita.com/tamago3keran/items/898936b716984cd55b1f#inline-method)
- [Replace Temp with Query](https://qiita.com/tamago3keran/items/898936b716984cd55b1f#replace-temp-with-query)
- [Replace Temp with Chain](https://qiita.com/tamago3keran/items/898936b716984cd55b1f#replace-temp-with-chain)
- [Replace Method with Method Object](https://qiita.com/tamago3keran/items/898936b716984cd55b1f#replace-method-with-method-object)
- [Substitute Algorithm](https://qiita.com/tamago3keran/items/898936b716984cd55b1f#substitute-algorithm)
- [Replace Loop with Collection Closure Method](https://qiita.com/tamago3keran/items/898936b716984cd55b1f#replace-loop-with-collection-closure-method)
- [Extract Surrounding Method](https://qiita.com/tamago3keran/items/898936b716984cd55b1f#extract-surrounding-method)
- [Introduce Class Annotation](https://qiita.com/tamago3keran/items/898936b716984cd55b1f#introduce-class-annotation)
- [Introduce Named Parameter](https://qiita.com/tamago3keran/items/898936b716984cd55b1f#introduce-named-parameter)
- [Replace Dynamic Receptor with Dynamic Method Definition](https://qiita.com/tamago3keran/items/898936b716984cd55b1f#replace-dynamic-receptor-with-dynamic-method-definition)


## 具体例詳細

### Extract Method

#### 要約

複数の場所で同じ構造のコードがあるとき、対象のコードを抽出して、目的を的確に表現する名前をつける

#### 例

```ruby

# リファクタリング前
def print_boading_ticket(flight_number)
  print_personal_info
  puts "seats_number: #{@seats_number}"
  puts "flight_number: #{flight_number}"
end

# リファクタリング後
def print_boading_ticket(flight_number)
  print_personal_info
  print_flight_info flight_number
end

def print_fligth_info
  puts "seats_number: #{@seats_number}"
  puts "flight number: #{flight_number}"
end
```

#### 説明

- ソースメソッドから重複するコードを抽出することで、メソッドの粒度を細かく分けることができる
- メソッドの粒度が細かいと、再利用できる可能性が高まる
- 細かい粒度のメソッドを多数抽出し、短く適切な名前がつけることで、高水準なメソッドの可読性も上がる
- メソッドの粒度が細かければ、オーバライドもしやすい

#### 備考

- メソッド名の長さより、メソッド名とメソッドの中身のギャップを重視する。
- メソッド名が長くても、メソッドの意味を適切に表現しているのであれば、問題ない。

#### 参照場所

`リファクタリング: Rubyエディション` P128 ~ P134


### Inline Method

#### 要約

メソッドの本体がメソッド名と同じくらいわかりやすいときは、メソッドの本体を呼び出し元に組み込んで、メソッドを削除する

#### 例

```ruby

# リファクタリング前
def get_point
  more_than_three_orders ? 2 : 1
end

def more_then_three_orders
  @number_of_orders > 3
end

# リファクタリング後
def get_point
  @number_of_orders > 3 ? 2 : 1
end
```

#### 説明

- 「メソッド抽出」など1つのものを2つに分解する度に、管理する場所が増えてしまう
- メソッド本体がメソッド名と同じくらいわかりやすいのであれば、分解しておく意味がない
- メンテナンスコストだけが高くなってしまう

#### 参照場所

`リファクタリング: Rubyエディション` P134 ~ P135


### Replace Temp with Query

#### 要約

式の結果を一時変数に保存しているときは、式をメソッドにし、一時変数の参照部分をメソッドに置き換える

#### 例

```ruby

# リファクタリング前
base_price =  @entrance_fee * @visitors.count

if (base_price > 10000)
  base_price * 0.80
else
  base_price * 0.90
end

# リファクタリング後
if (base_price > 10000)
  base_price * 0.80
else
  base_price * 0.90
end

def base_price
  @entrance_fee * @visitors.count
end
```

#### 説明

- 一時変数は使用されているメソッド内でしか参照できないため、一時変数にアクセスするとメソッドが長くなる
- 一時変数を問い合わせメソッドに置き換えれば、クラス内の全てのメソッドがアクセスできるようになる
- ローカル変数は「メソッドの抽出」を妨げるので、一時変数は問い合わせメソッドに変更しておく

#### 備考

- 一時変数は、ループ処理の結果を格納するために使われることが多い
- ループごとメソッドに抽出することで、まとまった行のコードを取り除ける

#### 参照場所

`リファクタリング: Rubyエディション` P137 ~ 140


### Replace Temp with Chain

#### 要約

一時変数を使って式の結果を保存しているときは、メソッドチェーンで書き換えて、一時変数を不要にする

#### 例

```ruby

# リファクタリング前
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")
```

#### 説明

- メソッド呼び出しを連鎖的に行うことで、流れるように読みやすいコードを書くことができる
- メソッドチェーンで書き換えることで、不要な一時変数を削除することができる場合がある

#### 参照場所

`リファクタリング: Rubyエディション` P140 ~ 143


### Replace Method with Method Object

#### 要約

ローカル変数によって「メソッドの抽出」ができないとき、メソッドを独自のオブジェクトに変更し、すべてのローカル変数をそのオブジェクトのインスタンス変数にする

#### 例

```ruby

# リファクタリング前
class Rental
  def fee(input_val)
    primary_base_fee = input_val * 0.7
    secondary_base_fee = input_val * 0.9
    tertiary_base_fee = input_val
    if primary_base_fee - secondary_base_fee > 200
      tertiary_base_fee -= 200
    end
  end
end

# リファクタリング後
class Rental
  def fee(input_val)
    Fee.new(input_val).calculate
  end
end

class Fee
  attr_reader :input_val,
              :primary_base_fee,
              :secondary_base_fee,
              :tertiary_base_fee

  def calculate
    primary_base_fee = input_val * 0.7
    secondary_base_fee = input_val * 0.9
    tertiary_base_fee = input_val
    if primary_base_fee - secondary_base_fee > 200
      tertiary_base_fee -= 200
    end
  end
end
```

#### 説明

- ローカル変数によって「メソッドの抽出」ができないときがある
- 「一時変数から問い合わせメソッドへ」を使っても、メソッドの分解が難しくなることもある
- 上記のときは、メソッドをオブジェクトとして切り出すとよい
- ローカル変数はメソッドオブジェクトの属性となり、そこで「メソッドの抽出」が可能になる

#### 参照場所

`リファクタリング: Rubyエディション` P153 ~ 156


### Substitute Algorithm

#### 要約

複雑なメソッドを分解して単純な部品にした後に、より単純なアルゴリズムで書き換える

#### 例

```ruby

# リファクタリング前
def find_relatives(people)
  relatives = []
  people.each do |person|
    if(person == "Ichiro")
      relatives << person
    end
    if(person == "Jiro")
      relatives << person
    end
    if(person == "Saburo")
      relatives << person
    end
  end
  return relatives
end

# リファクタリング後
def find_relatives(people)
  people.select do |person|
    %w(Ichiro Jiro Saburo).include? person
  end
end
```

#### 説明

- 問題の解決方法は1つではなく、他より簡単な方法が必ずある
- リファクタリングで複雑なメソッドを単純な部品に分解していくには限界がある
- アルゴリズム全体を書き換えることでより単純な部品にできるときもある

#### 参照場所

`リファクタリング: Rubyエディション` P156 ~ 157


### Replace Loop with Collection Closure Method

#### 要約

コレクションの要素をループで処理しているときは、コレクションクロージャメソッドを使用する

#### 例

```ruby

# リファクタリング前
women = []
people.each do |person|
  women << person if person.female?
end

# リファクタリング後
women = people.select { |person| person.female? }
```

#### 説明

- コレクションクロージャメソッドを使えば、ループ処理を簡単にできる
- 例: select, reject, map, collect
- コードも少なくなるし、可読性も上がる

#### 参照場所

`リファクタリング: Rubyエディション` P158 ~ 160


### Extract Surrounding Method

#### 要約

ほぼ同じコードのメソッドが複数あり、違いがメソッドの中頃にあるときは、重複部分を抽出して、ブロック付きメソッドにする

#### 例

```ruby

# リファクタリング前
def number_of_living_pets
  pets.inject(0) do |count, pet|
    count += 1 if pet.alive?
  end
end

def number_of_pets_named(name)
  pets.inject(0) do |count, pet|
    count += 1 if pets.name == name
  end
end

# リファクタリング後
def number_of_living_pets
  count_pets_matching { |pet| pet.alive? }
end

def number_of_pets_named(name)
  count_pets_metching { |pet| pet.name == name }
end

protected
def count_pets_matching(&block)
  pets.inject(0) do |count, pet|
    count += 1 if yield pet
  end
end
```

#### 説明

- ほぼ同じメソッド内の中央にユニークなコードがあるときに適用する
- 重複する処理はメソッドとして抽出し、異なる処理は Ruby のブロックを使って引数として渡すようにする

#### 参照場所

`リファクタリング: Rubyエディション` P160 ~ 164


### Introduce Class Annotation

#### 要約

実装手順がごく一般的で、安全に隠蔽できるメソッドがあるときは、クラス定義からクラスメソッドを呼び出してふるまいを宣言する

#### 例

```ruby

# リファクタリング前
class Person
  def initialize(params)
    @age = params[:age]
    @weight = params[:weight]
    @height = params[:height]
  end
end

# リファクタリング後
module CustomInitializers
  def hash_initializer(*attribute_names)
    define_method(:initialize) do |*args|
      data = args.first || {}
      attribute_names.each do |attribute_name|
        instance_variable_set "@#{attribute_name}", data[attribute_name]
      end
    end
  end
end

Class.send :include, CustomInitializers # 全クラスで使用できるように Class クラスに include

class Person
  hash_initializer :age, :weight, :height
end
```

#### 説明

- 属性アクセッサの実装は非常に単純なので、クラスアノテーションに置き換えることができる
- 宣言的な構文でコードの目的が明確に掴めるときは、クラスアノテーションを導入することで、コードの意図を明確にできる

#### 参照場所

`リファクタリング: Rubyエディション` P164 ~ 166


### Introduce Named Parameter

#### 要約

メソッドの名前から引数の意味が推測できないときは、引数をハッシュで渡すようにして、ハッシュキーを引数の名前として使うようにする

#### 例

```ruby

# リファクタリング前
class Person
  attr_reader :age, :weight, :height
  def initialize(age, weight, height)
    @age = age
    @weight = weight
    @height = height
  end
end

Person.new(20, 60, 160)

# リファクタリング後
class Person
  def initialize(params)
    @age = params[:age]
    @weight = params[:weight]
    @height = params[:height]
  end
end

Person.new(age: 20, weight: 60, height: 160)
```

#### 説明

- 処理を委譲されているオブジェクトのメソッド名と引数の役割がうまく表現できていないときに適用する
- メソッド名と引数の役割がわからなければ、オブジェクトの実装を確認するために行ったり来たりしなければならない
- 引数に `Hash` を使えば、引数として何を渡しているか明確になり、メソッドの中身を推測しやすくなる

#### 参照場所

`リファクタリング: Rubyエディション` P166 ~ 171


### Replace Dynamic Receptor with Dynamic Method Definition

#### 要約

method_missing を使わずに、動的メソッド定義を使って必要なメソッドを定義する

#### 例

```ruby

# リファクタリング前
class Staff
  def initialize(user)
    @user = user
  end

  def method_missing(method_name, *args)
    @user.send(method_name, *args)
  end
end

# リファクタリング後
class Staff
  def initialize(user)
    user.public_methods(false).each do |method_name|
      (class << self; self; end).class_eval do
        define_method(method_name) do |*args|
          user.send(method_name, *args)
        end
      end
    end
  end
end
```

#### 説明

- method_missing を使った実装は、デバックが困難になりがちである（以下参考）

```ruby

# 例のリファクタリング前の実装の場合
user.full_name = "test test"
staff = Staff.new(user)

# full_name を fullname とタイポすると...
staff.fullname
NoMethodError: undefined method 'fullname' for #<User:0x00007f904c973888>
# ↑メソッド呼び出しは Staff に対して行なっているのに、エラーを起こしているのは User になる
```

- 動的メソッド定義を使えば、method_missing を使用せずとも、同じようなふるまいを実現でき、デバックも容易になる

```ruby

# 例のリファクタリング後の実装の場合
user.full_name = "test test"
staff = Staff.new(user)

# full_name を fullname とタイポすると...
staff.fullname
NoMethodError: undefined method 'fullname' for #<Staff:0x00007fbde9a1c9e0>
# ↑メソッド呼び出しが Staff に対して行なわれると、エラーを起こしすのも 　Staff になる
```

#### 参照場所

`リファクタリング: Rubyエディション` P182 ~ 185

## 関連

- [リファクタリング: Rubyエディション 第2章](https://qiita.com/tamago3keran/items/dbe43cfcad01b4bac3ce)
- [リファクタリング: Rubyエディション 第7章](https://qiita.com/tamago3keran/items/d435f6c1280f8de29ad4)
- [リファクタリング: Rubyエディション 第8章](https://qiita.com/tamago3keran/items/2de394cfa1d36fdba924)
- [リファクタリング: Rubyエディション 第9章](https://qiita.com/tamago3keran/items/2818b20cdc42bb9c5388)
- [リファクタリング: Rubyエディション 第10章](https://qiita.com/tamago3keran/items/81a7fbd68d33c705b452)
- [リファクタリング: Rubyエディション 第11章](https://qiita.com/tamago3keran/items/aeee895399655b9c701b)