はじめに
チーム開発で学んだことや、アプリ開発を通して調べたこと、実装手順、またプログラミングスクールで学習した内容をまとめています。
今回は、複雑な検索機能を実装する際にFormObjectについて調べたことをまとめます。
FormObjectとは
FormObjectとは、form_with
のmodelオプションにActive Record以外のオブジェクトを渡すためのデザインパターンです。このパターンは、データベースと直接関係のないフォームや複数のモデルを扱うフォームを作成する際に、処理をすっきりと記述することができます。
FormObjectにおけるカプセル化とは
フォームに関連するデータとロジックを一箇所にまとめて記述することでコードを効果的に管理し、アプリケーション全体の構造とコードの品質を向上させることができる重要な手法です。これにより、より維持しやすく、拡張しやすいコードを書くことができます。
メリット
説明 | |
---|---|
関心事の分離 | フォームに関する処理をコントローラーから分離できます。その結果、コントローラーの役割が明確になり、コードの可読性が向上します。 |
再利用性 | 同じフォームロジックが異なるコントローラーやアクションで必要になる場合、フォームオブジェクトを使うと、そのロジックを一箇所に書いて何度も再利用できます。これにより、コードの重複を減らし、保守性を向上させることができます。 |
カスタム検証と動作 | 標準バリデーションに加えて特有のビジネスロジックを基にしたカスタムバリデーションが実装可能です。これにより、複雑な条件のフォームに対応しやすくなります。また、データ加工や特定の動作をエンカプセル化することで、変更や拡張が容易になります。 |
テスト容易性 | バリデーションやフォーム処理を集約することで、ユニットテストが効率的に行え、バグの早期発見に役立ちます。変更時も影響範囲が限定され、テストが容易になります。 |
FormObjectの作成
今回は複雑な検索機能を実装する為にFormObjectを作成します。
FormObjectはapp/forms
配下に配置します。
mkdir app/forms # appディレクトリにformsディレクトリを作成
touch app/forms/search_articles_form.rb # formobjectを作成
# 構成
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のパラメーターに対するバリデーション
等でも利用できます。
今後アプリ開発する際は、積極的に取り入れ可読性の高いコードを目指したいです。
参考資料