Layered Design For Ruby On Rails Application
注:以下の文章は、機械翻訳や Packt に備わっている Assistant AI による要約から成り立っている
Part 1: Exploring Rails and Its Abstractions
Chapter 1: Rails as a Web Application Framework
The journey of a click through Rails abstraction layers
Railsアプリケーションにおけるクリックの処理を、以下の層を通じて説明します。
- Webリクエストとアーキテクチャ: レイヤードアーキテクチャの適用理由とWebアプリケーションのライフサイクル。
- Rack: HTTPリクエストとRubyアプリケーション間のインターフェース。
- Railsルーティング: リクエストを適切なコントローラアクションにマッピング。
- コントローラ: リクエストをビジネスロジックに変換し、UIを更新。
- リクエスト外の処理: バックグラウンドジョブなどによる非同期処理。
trace_locationとは
trace_location
gemは、ライブラリやフレームワークの内部処理を追跡するためのツールで、RubyのTracePoint APIを使用。これは、メソッドの呼び出しや戻りをトレースし、Railsアプリケーションのリクエスト-レスポンスサイクル中のRubyメソッドコールを追跡するのに役立つ。
Beyond requests – background and scheduled tasks
バックグラウンドジョブとスケジュールジョブにより、HTTPリクエスト以外の作業を効率的に処理する方法を説明。
- 背景ジョブの必要性: スループットを向上させるための非同期処理。
- ジョブとキューの抽象: ジョブの定義とキューの利用。
- スケジュールされたジョブ: クロックトリガーによるジョブとそのスケジューリング方法。
- バックグラウンドジョブのエコシステム: SidekiqやActive Jobの使用。
ActiveJobの導入の背景
ActiveJobは、さまざまなバックグラウンドジョブライブラリの統一されたインターフェースを提供し、ジョブのスケジューリングとエンキューイングをシンプルにする。
The heart of a web application – the database
データベースがウェブアプリケーションの中心的な役割を果たし、パフォーマンスと抽象化のトレードオフについて説明。
- データベースの重要性: データベースの健全性がアプリケーションパフォーマンスに与える影響。
- 抽象化とパフォーマンスのトレードオフ: 過度な抽象化がパフォーマンスに及ぼす影響。
- 抽象化の目的: 抽象化は実装の詳細を隠すために使用されるが、必ずしもパフォーマンスの向上に繋がるわけではない。例えば、データベースに対する簡単なインサート操作も、抽象化が過度に使用されるとパフォーマンスに悪影響を及ぼすことがある。
- データベースレイヤーでの処理: 一部の機能はデータベースレベルで処理する方がパフォーマンスと生産性の両方でメリットがある。例えば、監査記録の保持や論理削除(soft deletion, レコードを削除する代わりに「削除済み」とマークする)などの機能は、アプリケーションコードで実装するのではなく、データベースのトリガやカスタムタイプを使用する方が効率的である。
- データベースアブストラクションの戦略: 一貫性を保つためのビジネスロジックのデータベースレイヤーへの移行。
以上が概要で、各コンポーネントがどのように連携してWebアプリケーションを構築し、効率的に動作させるかについて学びます。
Chapter 2: Active Models and Records
以下 AI による要約
この内容は、Ruby on Railsアプリケーションのモデル層に焦点を当てています。特にActive RecordおよびActive Modelの使用と潜在的な落とし穴に関する理解を深めることを目指しています。Active Recordは、データベーステーブルの名前で名付けられたクラスを使用することでプロセスを簡略化し、さらなる設定が不要です。一方、あまり知られていないActive Modelは、Active Recordのようなモデルを作成したり、Active Recordからドメインモデルを抽出するために使用できます。旧バージョンのソフトウェアに適用可能な様々なコードサンプルが提供されています。これには、状況遷移や整合性ルールのデモンストレーション、Active Recordの規約による設定(CoC)の利点などが含まれます。また、純粋なRubyオブジェクトがActive Modelにより強化されたクラスよりも優れているシナリオや、この知識をチャーン計算器と組み合わせて、チャーンと複雑さでトップNファイルを表示することが可能かどうかなど、重要な問いも提起します。
Data-mapper model についての説明
Hanami での実装例が紹介されている。
class PostRepository < Hanami::Repository
associations do
has_many :comments
end
def latest_for_user(user_id)
posts.where(user_id:).order { id.desc }.limit(1).one
end
end
class CommentRepository < Hanami::Repository
def latest_for_post(post_id, count:)
comments.where(post_id:)
.order { id.desc }
.limit(count).to_a
end
end
latest_post = PostRepository.new.latest_for_user(user_id)
latest_comments = CommentRepository.new.latest_for_post
(latest_post.id, count: 3)
基本的に、私たちはActive Recordが裏でやってくれていたすべてのことを、最初から書き直しました。一方では、より多くの(退屈な)コードを書く必要がありました。他方では、クエリの完全な制御を手に入れ、データベース通信APIについて考える必要があります。そして、考えることは決して損ではありません。
Data-mapper patternの主な利点は、データベースとビジネスロジックの分離です。このパターンを使用すると、データベースアクセスコードはリポジトリクラスにカプセル化され、ビジネスロジックから独立した形で管理・再利用できます。結果として、ビジネスロジックはデータベースの詳細を知らずに、データへのアクセスや操作を行うことができます。これにより、コードの読みやすさ、保守性、テストの容易さが向上します。また、リポジトリがデータアクセスの詳細を抽象化しているため、後々データベースの変更が必要になった場合でも、ビジネスロジックのコード変更は最小限で済む可能性があります。
このコードでは、リポジトリパターンを使用してデータベース操作を抽象化しています。これにより、データへのアクセスとその操作を疎結合に保つことができ、テストや保守が容易になります。また、PostRepository
と CommentRepository
に動作を集中させることで、コードの再利用が容易になります。具体的なメソッドでは、ユーザーの最新の投稿を取得するための latest_for_user(user_id)
と、指定した投稿に対する最新のコメントを取得するための latest_for_post(post_id, count:)
を提供しています。これにより、最新の投稿やコメントを簡単に取得する事ができます。
From mapping to the model
セクション「From mapping to the model」では、アプリケーションの状態またはモデルが永続化のスキーマだけでなく、データベーススキーマがアプリケーションモデルの反映であると述べています。さらに、モデルは遷移規則と一貫性の規則も記述します。遷移規則は状態をどのように変更できるかを定義し、一貫性の規則は可能な状態の制約を定義します。例えば、遷移規則として「投稿は公開後に下書きに戻すことができません」、一貫性規則として「タイトルが空でない場合のみ投稿を作成できる」といった例があります
class Post < ApplicationRecord
validates_with PublishingValidator, on: :publish
def publish
self.status = :published
save!(context: :publish)
end
end
このテクニックは、モデルの保守性をきちんと保ちながら、Rails Wayにできるだけ近づけるのに役立ちます。 しかし、コンテキストの数が増えるにつれて、単一のモデルクラス内でそれらすべてを追跡することは難しくなります。 バリデーションとルールに関するモデルレイヤーの複雑さに対処するために使用できる抽象化については、第7章「モデルの外側でユーザー入力を処理する」と第8章「表現レイヤーを引き出す」で説明します。
Seeking God Objects
この章で述べられている “churn” とは、特定のファイルがどのくらい頻繁に変更されたかを示す指標です。高い変更率は、コードデザインの欠陥を示している可能性があります。それは、新たな責任を追加したり、初期の実装の欠点を修正しようとしている場合があります。Churnを測定するために、Gitのようなバージョン管理システムを使用して、それぞれのファイルが影響を受けたコミットの総数を計算することができます
$ git log --format=oneline -- app/models/user.rb | wc -l
408
app/models/user.rb
に変更を加えたコミット数をカウントするコマンド。408回の変更があることがわかる。
(flog)[https://github.com/seattlerb/flog] や (attractor)[https://github.com/julianrubisch/attractor] といった gem でコードの複雑度を測定したりする
-
Active RecordパターンとData Mapperパターンの主な違いは何ですか? Active RecordパターンとData Mapperパターンの主な違いは、Data Mapperパターンがモデルと永続化を分離する点です。Active Recordはデータベースのレコードを表現し、読み書き操作を内包しますが、Data Mapperはクエリやデータの保存には別のオブジェクトを要求し、ドメインレイヤーとドメインサービス間の明確な分離を提供します【16:1†isbn-9781801813785-ebook.pdf】。
-
モデルバリデーションはいつ使用すべきで、データベース制約はいつ使用すべきですか? モデルバリデーションは、主にアプリケーションレベルでのデータ整合性を確保するために使用され、より説明的なエラーメッセージや、条件やコンテキストに応じた柔軟なバリデーションが可能です。一方、データベース制約は、プログラミングエラーや予期せぬ状況でもデータベースレベルでの強固な保証を提供します【16:6†isbn-9781801813785-ebook.pdf】。
-
ダックタイピングとは何ですか、そしてRailsではどのように利用されていますか? ダックタイピングは、オブジェクトの型やクラスをそのメソッドやプロパティによって判断する技術です。Railsでは主にActive Modelインターフェースを通じて、具体的なタイプを要求せずに特定の動作セットを持つオブジェクトとやり取りするためにダックタイピングが利用されています【16:17†isbn-9781801813785-ebook.pdf】。
-
どんな場合に、純粋なRubyオブジェクト(Structインスタンスのようなもの)がActive Model強化クラスよりも優先されますか? 純粋なRubyオブジェクトは、データベースとのやり取りが不要な場合のシンプルなデータ構造が必要なときに優先されます。これは、軽量で管理やテストが容易なソリューションを提供します【16:5†isbn-9781801813785-ebook.pdf】。
-
チャーンとは何ですか、そしてそれはコードの複雑さとどのように関係していますか? チャーンは、コードの一部(例えばファイル)が時間の経過とともにどれくらい頻繁に変更されるかを示す指標です。これは、コードデザインの潜在的な問題を示す指標として使われます。高い変更率は、設計上の弱点を示しており、複雑さが増し維持が難しくなる可能性があります【16:0†isbn-9781801813785-ebook.pdf】
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クエリを生成し、永続化層とドメインを分離 | 複雑なクエリをモデルから分離し、ビジネスロジックを明確化 | ユーザー入力の取得やバリデーションは行わない |
:memo: 次は第6章を読んでみる