FormObject 複雑な検索機能のカプセル化

## はじめに
チーム開発で学んだことや、アプリ開発を通して調べたこと、実装手順、またプログラミングスクールで学習した内容をまとめています。

今回は、複雑な検索機能を実装する際にFormObjectについて調べたことをまとめます。

# FormObjectとは
FormObjectとは、`form_with`のmodelオプションにActive Record以外のオブジェクトを渡すためのデザインパターンです。このパターンは、データベースと直接関係のないフォームや複数のモデルを扱うフォームを作成する際に、処理をすっきりと記述することができます。

# FormObjectにおけるカプセル化とは
フォームに関連するデータとロジックを一箇所にまとめて記述することでコードを効果的に管理し、アプリケーション全体の構造とコードの品質を向上させることができる重要な手法です。これにより、より維持しやすく、拡張しやすいコードを書くことができます。

## メリット
<table>

<thead>
<tr>
<th></th> <th>説明</th>
</tr>
</thead>
<tr>
<td> 関心事の分離 </td> <td>フォームに関する処理をコントローラーから分離できます。その結果、コントローラーの役割が明確になり、コードの可読性が向上します。</td>
</tr>
<tr>
<td> 再利用性 </td> <td>同じフォームロジックが異なるコントローラーやアクションで必要になる場合、フォームオブジェクトを使うと、そのロジックを一箇所に書いて何度も再利用できます。これにより、コードの重複を減らし、保守性を向上させることができます。</td>
</tr>
<tr>
<td> カスタム検証と動作 </td> <td>標準バリデーションに加えて特有のビジネスロジックを基にしたカスタムバリデーションが実装可能です。これにより、複雑な条件のフォームに対応しやすくなります。また、データ加工や特定の動作をエンカプセル化することで、変更や拡張が容易になります。</td>
</tr>
<tr>
<td> テスト容易性 </td> <td>バリデーションやフォーム処理を集約することで、ユニットテストが効率的に行え、バグの早期発見に役立ちます。変更時も影響範囲が限定され、テストが容易になります。</td>
</tr>
</table>


## FormObjectの作成
今回は複雑な検索機能を実装する為にFormObjectを作成します。

FormObjectは`app/forms`配下に配置します。
```ruby
mkdir app/forms　# appディレクトリにformsディレクトリを作成
touch app/forms/search_articles_form.rb　# formobjectを作成
```

```ruby:app/forms/search_articles_form.rb
 # 構成

class SearchArticlesForm
### モジュールのインクルード
  include ActiveModel::Model
  include ActiveModel::Attributes

### 属性定義
  attribute :author_id, :integer
  attribute :category_id, :integer
  attribute :title, :string
  attribute :body, :string
  attribute :tag_id, :string

### バリデーション
# validate :title... 
# このformでしか使用しないバリデーションがあればこちらに記述する。

### searchメソッド
  def search
    relation = Article.distinct

    relation = relation.by_category(category_id) if category_id.present?
    title_words.each do |word|
      relation = relation.title_contain(word)
    end
    relation = relation.by_author(author_id) if author_id.present?
    relation = relation.by_tag(tag_id) if tag_id.present?
    body_words.each do |word|
      relation = relation.body_contain(word)
    end
    relation
  end

  private

### プライベートメソッド
  def title_words
    title.present? ? title.split(nil) : []
  end

  def body_words
    body.present? ? body.split(nil) : []
  end
end
```
このクラスは、RailsのMVCパターンにおける「モデル」と「ビュー」の間に位置し、フォームの入力値の処理とデータベースの検索ロジックを担います。

　　
### モジュールのインクルード
- **`ActiveModel::Model`**
    Ruby on Railsフレームワークにおいて、Active Recordの一部の機能を、データベースに直接関連しないモデルに提供するためのモジュールです。一時的なデータの保持や、フォームの入力処理など、データベースに保存せずに済むケースの場合に`ActiveModel::Model`モジュールをインクルードすることで、データベースに保存されないオブジェクトにバリデーション、属性の変換、エラーメッセージの管理等の機能を持たせることができます。
　　
- **`ActiveModel::Attributes`**
    Ruby on RailsフレームワークにおけるActive Modelの一部で、モデルに属性（attribute）を追加するためのモジュールです。`ActiveModel::Attributes`モジュールをインクルードすることで、Active Recordのようにデータベースと直接やり取りしないモデルでも、型付けされた属性を定義し扱うことができます。

### 属性定義
- **`attribute :author_id,…`**
    `ActiveModel::Attributes`を使うことで、属性に特定のデータ型（例：string, integer, dateなど）やデフォルト値を指定でき、属性の値がその型に従って自動的に変換されます。
   
### バリデーション
　　Active Modelのバリデーションと組み合わせて使用することができます。

### メソッド
- **`search`**
    指定された検索条件（著者、カテゴリ、タイトル、本文、タグ）に基づいて、Articleモデルのレコードを検索します。
    Articleモデルにscopeで`by_tag`のようにクエリ条件が定義されています。`SearchArticlesForm`はActive Modelなので、直接SQLを呼び出すことはできません。Articleモデルにscopeを書くことで`Article.by_author(some_author_id)`のように呼び出すことができ、その著者による記事をすべて取得できます。

### プライベートメソッド
- **`title_words`**と**`body_words`**: 
    これらのメソッドは、タイトルと本文の文字列を空白文字で分割して単語の配列を生成します。これにより、各単語ごとに記事を検索することが可能になります。

このように検索機能に関連するコードをカプセル化することにより、コントローラーの処理がシンプルになり、再利用性と保守性が向上します。


# その他のFormObject
今回は検索機能をカプセル化しましたが、FormObjectは他にも
- ユーザーのログイン機能
- 問い合わせフォーム
- 1つの記事に複数の画像を保存する処理（has_manyな関係）
- APIのパラメーターに対するバリデーション

等でも利用できます。

今後アプリ開発する際は、積極的に取り入れ可読性の高いコードを目指したいです。


# 参考資料
https://railsguides.jp/active_model_basics.html

https://tech.medpeer.co.jp/entry/2017/05/09/070758#fn-1653d010

https://www.mintbit.com/blog/understanding-the-differences-traditional-forms-vs-form-objects-in-rails

https://qiita.com/ren0826jam/items/0effb716067a861e71f2

https://qiita.com/s_yanada/items/d856c11d074bcf98f73e

https://qiita.com/alpaca_taichou/items/bebace92f06af3f32898