Layered Design for Ruby on Rails Applications - Chapter 7: Handling User Input outside of Models
注:以下の文章は、機械翻訳や Packt に備わっている Assistant AI による要約から成り立っている
Part 2: Extracting Layers from Models - Chapter 7: Handling User Input outside of Models
Chapter 7: Handling User Input outside of Models
Webアプリケーションにおけるユーザの操作は、フォームの送信とリンクのクリックを交互に行うシーケンスとしてとらえることができます。フォームオブジェクトは、このような観点から、HTMLフォームから受け取ったパラメータの処理と、フォームに表示するためのデータの提供という、2つの役割を持つ重要な抽象化です。
フォームオブジェクトは、Active Recordモデルを介したドメインオブジェクトの処理とは異なり、フォーム入力のフィルタリング、型変換、検証などのサポートを提供します。また、フォームオブジェクトは、コールバックやアフターコミットアクションなどの、フォームの送信時の副作用の管理にも使用できます。
フォームオブジェクトは、コントローラーがビジネスロジックを実行するためのリクエストルーティングという、本来の目的に集中できるように設計されています。さらに、フォームオブジェクトを使用することで、複雑なフォームの処理や、複数のモデルにまたがる操作など、標準的なコントローラーとモデルの対応関係を超えたシナリオに対処できます。
つまり、フォームオブジェクトは、単なるモデルのラッパーではなく、アプリケーションにおける複雑なフォームインターフェースを表す汎用的な抽象化として機能します。
Form objects to the rescue
Rails開発で学ぶべき重要なことの1つです。新しいロジックをモデルクラスに入れるかコントローラクラスに入れるかで悩んだときは、常に第3の方法があることを覚えておいてください。
第5章「Railsの抽象化だけでは不十分なとき」では、サービスオブジェクトの現象(と、それが普遍的な中間オブジェクトとしてよく使われること)について説明しました。 私たちは、サービスオブジェクトはプレゼンテーション層のすぐ下、ドメイン層の上に独立したアーキテクチャ層(アプリケーションサービス)を構成できると結論づけました。 抽象化レイヤーは、アーキテクチャレイヤーの境界を尊重する場合にのみ有効である。 したがって、フォーム処理ロジックを実装するのに、銀の弾丸であるサービス・オブジェクトを使うことはできない。 何か新しいものが必要なのだ。 そこで、フォームオブジェクトの登場だ。
フォームオブジェクトは、ユーザーの入力の検証と、インタラクション固有のビジネスロジック規則と副作用のトリガーを担当します。この章では、フォームオブジェクトの抽出を通じてコントローラーの肥大化を軽減し、モデルをユーザーインターフェイスの特定のユースケースに密結合させることを回避する方法について説明しました。
一方、フィルターオブジェクトは、フォームオブジェクトのサブセットであり、ユーザー入力の検証とパラメーターのフィルタリングに焦点を当てています。フォームオブジェクトと同様に、フィルターオブジェクトはフォームヘルパーと互換性があり、ビューテンプレートで使用できます。
フィルターオブジェクトを使用する主な利点は次のとおりです。
- コントローラーの肥大化の軽減: フィルターオブジェクトを使用すると、コントローラーからパラメーターのフィルタリングロジックを抽出できます。これにより、コントローラーはより簡潔で保守しやすくなります。
- 再利用可能な検証ロジック: フィルターオブジェクトは検証ロジックをカプセル化するため、複数のフォームやコントローラーで再利用できます。
- 一貫したデータ検証: フィルターオブジェクトを使用すると、アプリケーション全体でデータ検証を標準化できます。
form_helper
にフォームオブジェクトを認識させるために、以下のようにクラス名から Form
のサフィックスを除外して model_name
を返す、ということをしている
class ApplicationForm
# ...
def model_name
ActiveModel::Name.new(
nil, nil, self.class.name.sub(/Form$/, "")
)
end
end
Q. We override the #model_name
method used for inference to cut the Form suffix from the resource name represented by a form object. Thus, InvitationForm represents the (virtual) Invitation resource. This is another convention we use to define the form object abstraction. At this point, our abstraction seems rather complete. There are multiple directions in which we can evolve it depending on the application’s needs. One common power-up is integrating form objects with strong parameters.
この部分とコード例について、別の方法があるなら教えてください。この方法では例えば InvitationForm
は Invitation
class のインスタンスしか扱えないような気がします
A. InvitationFormがInvitationクラスのインスタンスしか扱えないように感じる場合、他の方法やアプローチについて考慮することができます。例えば、別の方法として、フォームオブジェクトに対して継承を活用するのではなく、モジュールを組み合わせて構成することで、多様な振る舞いを注入できるようにすることが考えられます。これは特に、異なるコンテキストによって異なるフォーム動作が必要な場合に有益です。
ここでは、以下のようなアプローチを試すことができます。
-
モジュールの利用: 一つの統一されたフォームクラスの代わりに、異なる「役割」や機能を持つモジュールを作成し、それらをフォームオブジェクトに組み合わせる。これにより、オブジェクトの振る舞いを柔軟に変更でき、異なるリソースタイプにも対応できます。
-
コンポジションを活用: フォームオブジェクトを複数の小さなクラスに分け、それぞれが特定の役割や機能に責任を持つようにする。この方法は単一責任原則に従い、個々のクラスが特定の責任を担う形にすることで、保守性や再利用性が向上します。
これらの方法を採用することで、フォームオブジェクトが特定のモデルクラスに厳密に結びつくのを避け、多様性を持たせた設計が可能となります。また、例えば、型の違いによって様々なデータ入力を適切にハンドルすることができる点でも利点があります。
この際、ActiveModelやActiveSupportをベースにしたモジュールとの組み合わせを考慮することで、Railsのフォームオブジェクトの現在の強みを失わずに、より柔軟なアプローチをとることができます【22:0†isbn-9781801813785-ebook.pdf】。
フィルターオブジェクトを作成するには、次の手順に従います。
- フォームオブジェクトを作成する: フィルターオブジェクトに関連するフォームオブジェクトを作成します。このフォームオブジェクトには、検証ロジックとアクションの実装が含まれます。
- フィルターオブジェクトを作成する: フィルターオブジェクトを作成し、フォームオブジェクトを継承します。フィルターオブジェクトで、属性を定義し、検証をオーバーライドします。属性は、フォームヘルパーで使用するために公開されます。
- コントローラーで使用する: コントローラーでフィルターオブジェクトを使用し、パラメーターのフィルタリングと検証を行います。
フィルターオブジェクトは、フォームオブジェクトよりも軽量で、検証とフィルタリングに特化していることに注意することが重要です。フォームオブジェクトと同様に、フィルターオブジェクトは Active Model を活用してフォームヘルパーと互換性を持たせることができます。
全体として、フィルターオブジェクトは、パラメーターのフィルタリングと検証をカプセル化するための便利な抽象化です。これにより、コントローラーの肥大化が軽減され、再利用可能な検証ロジックが提供され、一貫したデータ検証が保証されます。
Filter objects versus form objects versus query objects
セクション「フィルターオブジェクトとフォームオブジェクトとクエリオブジェクト」では、これらのオブジェクトが持つ役割の違いが説明されています。
-
フィルターオブジェクトは、ユーザー入力に基づいてデータセットを変換する責任を持ち、ユーザーからの入力をサニタイズし、既知の値を抽出し、デフォルトを提供し、必要な変換を行います。これにより、ユーザーからの多数のリクエストパラメータを扱うことができます。
-
フォームオブジェクトは、特定のユーザーインタラクション(通常はHTMLフォームなどでのデータ送信)の処理を担当します。これは、ユーザー入力のバリデーションを行い、特定のビジネスロジックや副作用を引き起こします。フォームオブジェクトは主にプレゼンテーション層に位置し、ビューとも連動して動的にフォームフィールドをレンダリングすることができます。
-
クエリオブジェクトは、通常はSQLクエリを生成するオブジェクトで、ドメインレベルのオブジェクトを利用して永続化層とドメインを分離します。これにより、モデルから複雑なクエリを分離し、ビジネスロジックを明確にする効果があります。
フィルターオブジェクトとクエリオブジェクトはどちらもクエリ構築に関係しますが、フィルターオブジェクトはユーザー入力により駆動され、クエリオブジェクトは通常、ドメインサービスレイヤーに位置し、プレゼンテーション層から独立しています【26:0†isbn-9781801813785-ebook.pdf】。
日本語訳
フィルター・オブジェクトはクエリー・オブジェクトと混同されるかもしれません(第6章「データ層の抽象化」を参照)。 確かに、これらはクエリーを構築するという同じような目的を果たします。 しかし、クエリオブジェクトはドメイン(またはドメインサービス)レイヤーに属していることを思い出してほしい。 フィルターオブジェクトがユーザー入力を消費するのに対して、クエリオブジェクトはドメインオブジェクトのみを操作します。 フィルターオブジェクトはクエリオブジェクトを使うかもしれないが、クエリオブジェクトは決してプレゼンテーションレイヤーのエンティティ(リクエストパラメータなど)を扱うべきではない。
フィルターオブジェクトはフォームオブジェクトに近い。 しかし、2つの点で異なっている。 第一に、フォーム・オブジェクトはアクションを実行し、実行のステータスを返すが、フィルター・オブジェクトは情報を表示するためにフィルターされたデータを返さなければならない。 第二に、フォームオブジェクトは失敗した場合(例えば無効な入力)にフィードバックを提供しなければなりません。 しかし、フォームオブジェクトとフィルターオブジェクトのハイブリッドオブジェクトが適している場合もあります。
オブジェクトタイプ | 役割 | 主な特徴 | やらないこと・役割ではないこと |
---|---|---|---|
フィルターオブジェクト | ユーザー入力に基づいたデータセットの変換を担当 | ユーザー入力をサニタイズし、デフォルト値を提供 | データのバリデーションやビジネスロジックの実行は行わない |
フォームオブジェクト | 特定のユーザーインタラクション(例えばHTMLフォーム送信)の処理を担当 | 入力のバリデーション、ビジネスロジックや副作用の実行 | データのクエリ作成や大量のデータ変換は行わない |
クエリオブジェクト | SQLクエリを生成し、永続化層とドメインを分離 | 複雑なクエリをモデルから分離し、ビジネスロジックを明確化 | ユーザー入力の取得やバリデーションは行わない |