コントローラの処理内容をクラスで分割してみる

# 概要

こちらは社内技術勉強会用の資料として作成したものです。オブジェクト指向の考え方を取り入れてみよう、という話です。Ruby on Railsを使用します。

---

# 実習内容

---

## 実習用のソースコード

[こちら](https://github.com/ktakase00/inquiry_demo)を参照してください。手直し前がmasterブランチ、手直し後がexample1ブランチにあります。

なお、記事に掲載しているソースコードは抜粋になっており、一部のコードは省略してあります。

---

## サンプルプログラムの概要

以下の3種類の問い合わせを受け付ける入力フォームを開発するとします。

* 営業への質問
* ヘルプデスクへの質問
* 技術フォーラムへの質問

---

## 一覧画面イメージ

<img width="492" alt="inquiry_01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27959/185d9ba2-fc52-6d81-a798-2713b7813460.png">

---

### 新規登録画面イメージ

<img width="486" alt="inquiry_02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27959/c34c5f06-fb15-9f14-eca6-ecfca3fe1793.png">

---

## 実習課題

各問合せの種類ごとに、受付コードを取得して、DBに保存されるようにしてください。受付コードを取得するAPIは、各問合せの種類ごとに3つ提供されているとします。APIは[README](https://github.com/ktakase00/inquiry_demo#%E8%A3%9C%E8%B6%B31%E5%A4%96%E9%83%A8%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9api%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)を参照してください。

---

# 実装例

---

## できました！

問い合わせの新規登録を行うコントローラのアクションに、コードを書きました。

```ruby
  def create
    begin
      accept_cd = case @inquiry.inquiry_cd
      when Const::InquiryCd::SALES
        # 営業への質問
        ex_service = ExService::Sales.new
        res = ex_service.register(@inquiry.inquiry_content)
        res[:code]
  
      when Const::InquiryCd::HELP_DESK
        # ヘルプデスクへの質問
        ex_service = ExService::HelpDesk.new
        res = ex_service.create(@inquiry.inquiry_content)
        raise res[:body][:message] if res[:code] != ExService::HelpDesk::CODE_SUCCESS
        res[:body][:ident]

      when Const::InquiryCd::TECH_FORUM
        # 技術フォーラムへの質問
        res = ExService::TechForum.add_inquiry(@inquiry.inquiry_content)
        res[:code]
      end
      @inquiry.attributes = { accept_cd: accept_cd, error_json: {} }

    rescue => e
      logger.error e.full_message
      @inquiry.errors.add(:base, e.message)
      @inquiry.attributes = { accept_cd: '', error_json: { message: e.message } }
    end

    # DBに保存
    if @inquiry.save
      redirect_to @inquiry, { notice: make_notice(@inquiry.success?) }
    else
      render :new
    end
  end
```

---

## 気になるところ

* 今後問い合わせの種類が増えるなど、なんらかの追加があるとコントローラのソースコードがどんどん増えていく
* いずれかの受付コードを取得するAPIの仕様が変更になっただけでも、コントローラのソースコードに手を加えなければならない

コントローラがたくさんの責務を持ちすぎていて、メンテナンスがやりにくそうです。

---

# 手直しその1

---

## 共通部分を作る

「受付コードを取得する」という同じことを行うAPIなのに、インターフェイスが異なっていて、いちいち呼び出し方に気を付けなければならないのが面倒です。「受付コードを取得する」責務を持つクラス群を定義してみます。

* 3種類のAPIの呼び出し方(メソッド名、引数、戻り値の形)を統一
* 共通処理はBaseモジュール(親クラスに相当)に切り出す

コントローラに直接書いていた時よりもソースコードの量が増えていますが、今後「受付コードを取得する」ことに関する共通の処理が増えた時に、1か所にだけ書けばよい状態になっているのが、注目する点です。

```ruby
module Registrar::Adapter
  class Sales
    include Registrar::Adapter::Base

    def execute
      begin
        ex_service = ExService::Sales.new
        res = ex_service.register(@inquiry_content)
        { accept_cd: res[:code], error: nil }
      rescue => e
        { accept_cd: nil, error: { message: e.message } }
      end
    end
  end
end
```

```ruby
module Registrar::Adapter
  class HelpDesk
    include Registrar::Adapter::Base

    def execute
      ex_service = ExService::HelpDesk.new
      res = ex_service.create(@inquiry_content)

      case res[:code]
      when ExService::HelpDesk::CODE_SUCCESS
        { accept_cd: res[:body][:ident], error: nil }
      else
        { accept_cd: nil, error: res }
      end
    end
  end
end
```

```ruby
module Registrar::Adapter
  class TechForum
    include Registrar::Adapter::Base

    def execute
      begin
        res = ExService::TechForum.add_inquiry(@inquiry_content)
        { accept_cd: res[:code], error: nil }
      rescue => e
        { accept_cd: nil, error: { message: e.message } }
      end
    end
  end
end
```

---

# 手直しその2

---

## クラスの違いを意識しない

先ほど作成した「受付コードを取得する」クラスたちを作成したことで、どのAPIを使う場合であっても、インターフェイスが同じになりました。

そのため、呼び出し側はAPIの種類に関係なくただ「実行する」とだけ書くことができるようになりました。

```ruby
  def create
    adapter = case @inquiry.inquiry_cd
    when Const::InquiryCd::SALES
      Registrar::Adapter::Sales.new(@inquiry.inquiry_content)
    when Const::InquiryCd::HELP_DESK
      Registrar::Adapter::HelpDesk.new(@inquiry.inquiry_content)
    when Const::InquiryCd::TECH_FORUM
      Registrar::Adapter::TechForum.new(@inquiry.inquiry_content)
    end
    result = adapter.execute
    @inquiry.attributes = make_attributes(result)

    if @inquiry.save
      redirect_to @inquiry, { notice: make_notice(@inquiry.success?) }
    else
      render :new
    end
  end
```

---

## ついでに

どの「受付コードを取得する」クラスを使用するかを決定する処理をコントローラに書いているため、APIの種類が増えるとコントローラを修正する必要があります。

どのクラスを使用するかを決定する処理を別の新しいクラスに分離してしまいます。これにより、新しいAPIが増えてもコントローラに手を加える必要がなくなります。

---

### 使用するクラスを決定

caseもやめて、ハッシュで書いてみます。使用するクラスを切り替えるのに、条件分岐が登場しなくなりました。

```ruby
module Registrar
  class AdapterFactory
    ADAPTER_MAP = {
      Const::InquiryCd::SALES => Registrar::Adapter::Sales,
      Const::InquiryCd::HELP_DESK => Registrar::Adapter::HelpDesk,
      Const::InquiryCd::TECH_FORUM => Registrar::Adapter::TechForum,
    }

    def initialize(inquiry_cd, inquiry_content)
      @inquiry_cd = inquiry_cd
      @inquiry_content = inquiry_content
    end

    def create
      ADAPTER_MAP[@inquiry_cd].new(@inquiry_content)
    end
  end
end
```

---

### 手直し途中のコントローラ

使用するAPIを意識する必要がなくなりました。

```ruby
  def create
    adapter_factory = Registrar::AdapterFactory.new(@inquiry.inquiry_cd, @inquiry.inquiry_content)
    adapter = adapter_factory.create
    result = adapter.execute
    @inquiry.attributes = make_attributes(result)

    if @inquiry.save
      redirect_to @inquiry, { notice: make_notice(@inquiry.success?) }
    else
      render :new
    end
  end
```

---

# 手直しその3

---

## ビジネスロジックを分離

コントローラの責務を、ビジネスロジックとビューをつなぐことだけにしてしまいたいです。問い合わせの登録処理を、新しいクラスに独立させます。

問い合わせの登録処理を行う窓口クラスを作成し、受付コードの取得、DBへの保存、の2つの処理の流れ(=>ビジネスロジック)を規定します。

```ruby
module Registrar
  class Facade
    def initialize(inquiry)
      @inquiry = inquiry
    end

    def execute
      adapter_factory = Registrar::AdapterFactory.new(@inquiry.inquiry_cd, @inquiry.inquiry_content)
      adapter = adapter_factory.create
      result = adapter.execute

      @inquiry.attributes = make_attributes(result)
      @inquiry.save!
    end
    # (中略)
  end
end
```

---

## 手直し後のコントローラ

問い合わせの登録を行う窓口クラスに、処理を委譲します。コントローラがビジネスロジックの内容に関与しなくてよくなりました。

```ruby
  def create
    begin
      facade = Registrar::Facade.new(@inquiry)
      facade.execute
      redirect_to @inquiry, { notice: make_notice(@inquiry.success?) }

    rescue => e
      logger.error e.full_message
      @inquiry.errors.add(:base, e.message)
      render :new
    end
  end
```

---

# まとめ

---

## オブジェクト指向を取り入れてみた点

* ビジネスロジックを分離(カプセル化)
* 共通部分を作る(継承)
* クラスの違いを意識しない(多態性)

---

## おわりに

ソースコードの一部を修正することになった時、その影響範囲が小さく抑えられていると安心できます。

また、修正対象のコードを、巨大なクラスの奥の方から探し出すより、その役割を担っているクラスをクラス名で発見する方が簡単です。

開発する際には、メンテナンスのしやすいコードを書けるようになりたいと思います。