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

## はじめに

<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エディション` の `第9章 条件式の単純化` で紹介されているいくつかのテクニックについて、まとめました。

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

## テクニック一覧

- [Decompose Conditional](https://qiita.com/tamago3keran/items/2818b20cdc42bb9c5388#decompose-conditional)
- [Introduce Null Object](https://qiita.com/tamago3keran/items/2818b20cdc42bb9c5388#introduce-null-object)
- [Introduce Assertion](https://qiita.com/tamago3keran/items/2818b20cdc42bb9c5388#introduce-assertion)


## テクニック詳細

### Decompose Conditional

複雑な条件文があるときに、条件部と then 部、 else 部からメソッドを抽出するテクニック

```ruby:サンプル

# リファクタリング前
if WINTER_START < date < WINTER_END
  price = base_price * @winter_rate + @winter_service_price
else
  price = base_price * @summer_rate
end

# リファクタリング後
if is_winter?(date)
  price = winter_price(base_price)
else
  price = summer_price(base_price)
end
```

注意しておかないとプログラムに条件分岐を加えることで、あっという間にコードが長くなってしまいますよね。長いコードはそれだけで読みにくいですが、条件分岐が入ることでさらに可読性が低くなってしまいます。

問題は、どういう条件でどういった処理が行われるかはコードを見ればわかるかもしれませんが、なぜそういった処理を実行するのか、意図がぼやけてしまいやすいということです。

だからこそ、条件部や分岐先の処理を意図が伝わりやすいメソッドに置き換えることが大切だと感じました。


### Introduce Null Object

nil 値チェックを繰り返し実行しているとき、 nil 値の代わりに null オブジェクトを導入するテクニック

```ruby:サンプル

# リファクタリング前
class Resident
  attr_reader :name, :age

  def initialize(params)
    @name = params[:name]
    @age  = params[:age]
  end
end

class Room
  attr_reader :resident

  def initialize(params)
    @resident = params[:resident]
  end

  def resident_name
    @resident ? @resident.name : "empty"
  end

  def resident_age
    @resident ? @resident.age : "empty"
  end
end

# リファクタリング後
class Resident
  attr_reader :name, :age

  def initialize(params)
    @name = params[:name]
    @age  = params[:age]
  end

  def self.new_missing
    MissingResident.new
  end
end

class MissingResident
  def name
    "empty"
  end

  def age
    "empty"
  end
end

class Room
  def initialize(params)
    @resident = params[:resident]
  end

  def resident
    @resident || Resident.new_missing
  end

  def resident_name
    resident.name
  end

  def resident_age
    resident.age
  end
end
```

nil 値のチェックが繰り返し行われるような処理が増えてきた場合、nullオブジェクトの導入によるリファクタリングが有効とのことでした。

上記のサンプルでは、部屋（`Room`）に住人（`Resident`）がいない場合に nil を返すのではなく、存在しない住人（`MissingResident`）というクラスのインスタンス（= **nullオブジェクト**）を返すようにしています。

新しく作成した null オブジェクトのクラス（サンプルでいうところの `MissingResident` クラス）に null オブジェクトにも処理できるようにしたいメソッドだけを定義しています。このように実装することで、クライアントコードから nil 値チェックを無くし、スッキリさせることができます。

たしかにクライアントコードを綺麗にすることはできますが、メンテナンスコストが高くなるという印象を受けました。上記のサンプルでいえば、 `Resident` クラスにメソッドが増えてきた場合、 `MissingResident` の方も気にしながら実装しなければならなくなるような気がします。


### Introduce Assertion

ある条件を前提としているコードがあるとき、アサーションを導入して前提条件を明確にするテクニック

```ruby:サンプル

# リファクタリング前
class Employee
  def initialize(params)
    @project = params[:project]
    @salary  = params[:salary]
  end

  def salary
    # salary か project が必要
    @salary ? @salary : @project.price
  end
end

class Project
  attr_reader :price

  def initialize(price)
    @price = price
  end
end

# リファクタリング後

module Assertions
  class AssertionsFailedError < StandardError; end

  def assert(&condition)
    raise AssertionsFailedError.new("Assertion Failed") unless condition.call
  end
end

class Employee
  include Assertions

  def initialize(params)
    @project = params[:project]
    @salary  = params[:salary]
  end

  def salary
    assert { (!@salary.nil?) || (!@project.nil?) }
    @salary ? @salary : @project.price
  end
end

class Project
  attr_reader :price

  def initialize(price)
    @price = price
  end
end
```

特定の条件が前提となっているコードというのは私もよく見かけます。コメントで前提条件が書く場合もあるかと思います。しかし、コメントで書いたりするよりもアサーションを用いて前提条件を明確にした方がよいということが書かれています。

アサーションとは、常に真でなければならない条件のことを指し、アサーションが偽を返すということはプログラミングエラーを意味するので、必ず例外を生成するようにします。

これにより、前提条件がコードとして明確になるだけでなく、デバックも楽になります。コードの可読性が向上し、デバックがしやすくなることで、効率の良い開発に繋がるという長所があります。

## 考察

今回紹介したテクニックにもそれぞれ特徴があり、積極的に活用していけるものと、よく注意して活用すべきものに分けられると思います。

**Decompose Conditional**　は前者であると考えていて、条件文や分岐先の処理を意図が伝わりやすいメソッドとすることは、開発者にとってメリットが多く、デメリットが少ないように感じます。

**Introduce Null Object** は後者であると考えていて、 null オブジェクトを導入することで多くのメリットを享受できる一方で、よく考えて導入しないとリファクタリング箇所が多すぎてバグを生み出してしまう可能性が高かったり、導入後のメンテナンスコストが高くなってしまうようにも感じます。

テクニックのメリット、デメリットを理解した上で活用していくことが大切だと思います。

## 関連

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