プレーンな古いRubyオブジェクトを使用して洗練されたRailsコンポーネントを構築する
公開: 2022-03-11あなたのウェブサイトは勢いを増していて、あなたは急速に成長しています。 Ruby/Railsはあなたが選んだプログラミング言語です。 あなたのチームはより大きく、Railsアプリのデザインスタイルとして「ファットモデル、スキニーコントローラー」をあきらめました。 ただし、それでもRailsの使用をやめたくはありません。
問題ない。 今日は、OOPのベストプラクティスを使用して、コードをよりクリーンで、より分離され、より分離されたものにする方法について説明します。
あなたのアプリはリファクタリングする価値がありますか?
まず、アプリがリファクタリングに適しているかどうかを判断する方法を見てみましょう。
これは、コードのリファクタリングが必要かどうかを判断するために私が通常自問するメトリックと質問のリストです。
- 遅いユニットテスト。 PORO単体テストは通常、十分に分離されたコードで高速に実行されるため、実行速度の遅いテストは、多くの場合、不適切な設計と過度に結合された責任の指標となる可能性があります。
- FATモデルまたはコントローラー。 一般に、200行を超えるコード(LOC)を含むモデルまたはコントローラーは、リファクタリングの候補として適しています。
- 過度に大きなコードベース。 LOCが30,000を超えるERB/HTML / HAMLまたはLOCが50,000を超えるRubyソースコード(GEMなし)がある場合は、リファクタリングする必要があります。
次のようなものを使用して、Rubyソースコードの行数を確認してください。
find app -iname "*.rb" -type f -exec cat {} \;| wc -l
このコマンドは、/ appフォルダー内の.rb拡張子を持つすべてのファイル(rubyファイル)を検索し、行数を出力します。 コメント行はこれらの合計に含まれるため、この数値は概算であることに注意してください。
もう1つのより正確で有益なオプションは、コード行、クラス数、メソッド数、メソッドとクラスの比率、およびメソッドごとのコード行の比率の簡単な要約を出力するRailsrakeタスクstats
を使用することです。
bundle exec rake stats +----------------------+-------+-----+-------+---------+-----+-------+ | Name | Lines | LOC | Class | Methods | M/C | LOC/M | +----------------------+-------+-----+-------+---------+-----+-------+ | Controllers | 195 | 153 | 6 | 18 | 3 | 6 | | Helpers | 14 | 13 | 0 | 2 | 0 | 4 | | Models | 120 | 84 | 5 | 12 | 2 | 5 | | Mailers | 0 | 0 | 0 | 0 | 0 | 0 | | Javascripts | 45 | 12 | 0 | 3 | 0 | 2 | | Libraries | 0 | 0 | 0 | 0 | 0 | 0 | | Controller specs | 106 | 75 | 0 | 0 | 0 | 0 | | Helper specs | 15 | 4 | 0 | 0 | 0 | 0 | | Model specs | 238 | 182 | 0 | 0 | 0 | 0 | | Request specs | 699 | 489 | 0 | 14 | 0 | 32 | | Routing specs | 35 | 26 | 0 | 0 | 0 | 0 | | View specs | 5 | 4 | 0 | 0 | 0 | 0 | +----------------------+-------+-----+-------+---------+-----+-------+ | Total | 1472 |1042 | 11 | 49 | 4 | 19 | +----------------------+-------+-----+-------+---------+-----+-------+ Code LOC: 262 Test LOC: 780 Code to Test Ratio: 1:3.0
- コードベースで繰り返しパターンを抽出できますか?
動作中のデカップリング
実際の例から始めましょう。
ジョガーの時間を追跡するアプリケーションを作成したいとします。 メインページで、ユーザーは入力した時間を確認できます。
毎回のエントリには、日付、距離、期間、および関連する追加の「ステータス」情報(天気、地形のタイプなど)、および必要に応じて計算できる平均速度が含まれます。
1週間あたりの平均速度と距離を表示するレポートページが必要です。
エントリの平均速度が全体の平均速度よりも速い場合は、SMSでユーザーに通知します(この例では、Nexmo RESTful APIを使用してSMSを送信します)。
ホームページでは、ジョギングに費やした距離、日付、時間を選択して、次のようなエントリを作成できます。
また、基本的に週ごとのレポートであるstatistics
ページもあります。これには、1週間あたりの平均速度と走行距離が含まれています。
- ここでオンラインサンプルをチェックできます。
コード
app
ディレクトリの構造は次のようになります。
⇒ tree . ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── helpers │ ├── application_helper.rb │ ├── entries_helper.rb │ └── statistics_helper.rb ├── mailers ├── models │ ├── entry.rb │ └── user.rb └── views ├── devise │ └── ... ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
User
モデルは、認証を実装するためにDeviseで使用しているため、特別なものではないため、ここでは説明しません。
Entry
モデルに関しては、アプリケーションのビジネスロジックが含まれています。
各Entry
はUser
に属しています。
各エントリのdistance
、 time_period
、 date_time
、およびstatus
属性の存在を検証します。
エントリを作成するたびに、ユーザーの平均速度をシステム内の他のすべてのユーザーの平均と比較し、Nexmoを使用してSMSでユーザーに通知します(Nexmoライブラリの使用方法については説明しませんが、外部ライブラリを使用する場合を示すため)。
- 要旨サンプル
Entry
モデルには、ビジネスロジックだけではないことに注意してください。 また、いくつかの検証とコールバックも処理します。
entries_controller.rb
にはメインのCRUDアクションがあります(ただし更新はありません)。 EntriesController#index
は現在のユーザーのエントリを取得し、作成された日付でレコードを並べ替えますが、 EntriesController#create
は新しいエントリを作成します。 EntriesController#destroy
の明白な責任について議論する必要はありません:
- 要旨サンプル
statistics_controller.rb
は週次レポートの計算を担当しますが、 StatisticsController#index
は、RailsのEnumerableクラスに含まれる#group_by
メソッドを使用して、ログインしたユーザーのエントリを取得し、週ごとにグループ化します。 次に、いくつかのプライベートメソッドを使用して結果を装飾しようとします。
- 要旨サンプル
ソースコードは自明であるため、ここではビューについてあまり説明しません。
以下は、ログインしたユーザー( index.html.erb
)のエントリを一覧表示するためのビューです。 これは、エントリコントローラでインデックスアクション(メソッド)の結果を表示するために使用されるテンプレートです。
- 要旨サンプル
パーシャルrender @entries
を使用して、共有コードをパーシャルテンプレート_entry.html.erb
にプルアウトし、コードをDRYで再利用可能に保つことができることに注意してください。
- 要旨サンプル
同じことが_form
パーシャルにも当てはまります。 (新規および編集)アクションで同じコードを使用する代わりに、再利用可能な部分フォームを作成します。
- 要旨サンプル
週次レポートページビューに関しては、 statistics/index.html.erb
はいくつかの統計を表示し、いくつかのエントリをグループ化することによってユーザーの週次パフォーマンスを報告します。
- 要旨サンプル
そして最後に、エントリのヘルパー、 entries_helper.rb
には、属性をより人間的に読みやすくする2つのヘルパーreadable_time_period
とreadable_speed
が含まれています。
- 要旨サンプル
これまでのところ、派手なものはありません。
ほとんどの人は、これをリファクタリングすることはKISSの原則に反し、システムをより複雑にするだろうと主張するでしょう。
では、このアプリケーションは本当にリファクタリングが必要ですか?
絶対にそうではありませんが、デモンストレーションの目的でのみ検討します。
結局のところ、前のセクションを確認し、アプリがリファクタリングを必要としていることを示す特性を確認すると、この例のアプリはリファクタリングの有効な候補ではないことが明らかになります。
ライフサイクル
それでは、RailsMVCパターン構造を説明することから始めましょう。
通常、 https://www.toptal.com/jogging/show/1
などのリクエストを行うブラウザから開始されます。
Webサーバーは要求を受信し、 routes
を使用して使用するcontroller
を見つけます。
コントローラーは、ユーザーリクエスト、データ送信、Cookie、セッションなどを解析し、 model
にデータを取得するように依頼します。
models
は、データベースと通信し、データを保存および検証し、ビジネスロジックを実行し、その他の面倒な作業を行うRubyクラスです。 ビューは、ユーザーに表示されるものです:HTML、CSS、XML、Javascript、JSON。
Railsリクエストのライフサイクルのシーケンスを表示したい場合は、次のようになります。
私が達成したいのは、プレーンな古いルビーオブジェクト(PORO)を使用してさらに抽象化を追加し、 create/update
アクションのパターンを次のようにすることです。
そして、 list/show
アクションの場合は次のようになります。
POROの抽象化を追加することで、Railsがあまり得意ではない責任SRPを完全に分離できるようになります。
ガイドライン
新しい設計を実現するために、以下のガイドラインを使用しますが、これらはTに従わなければならないルールではないことに注意してください。リファクタリングを容易にする柔軟なガイドラインと考えてください。
- ActiveRecordモデルには、関連付けと定数を含めることができますが、それ以外は含めることができません。 つまり、コールバック(サービスオブジェクトを使用してそこにコールバックを追加する)と検証(フォームオブジェクトを使用してモデルの名前付けと検証を含める)がないことを意味します。
- コントローラを薄層として保持し、常にサービスオブジェクトを呼び出します。 ロジックを含めるためにサービスオブジェクトを呼び出し続けたいので、なぜコントローラーを使用するのかと質問する人もいますか? コントローラーは、HTTPルーティング、パラメーターの解析、認証、コンテンツネゴシエーション、適切なサービスまたはエディターオブジェクトの呼び出し、例外のキャッチ、応答の書式設定、および適切なHTTPステータスコードの返送を行うのに適した場所です。
- サービスはQueryオブジェクトを呼び出す必要があり、状態を保存するべきではありません。 クラスメソッドではなく、インスタンスメソッドを使用します。 SRPに準拠するパブリックメソッドはほとんどないはずです。
- クエリはクエリオブジェクトで実行する必要があります。 クエリオブジェクトメソッドは、ActiveRecordの関連付けではなく、オブジェクト、ハッシュ、または配列を返す必要があります。
- ヘルパーの使用を避け、代わりにデコレータを使用してください。 なんで? Railsヘルパーの一般的な落とし穴は、それらがOO以外の関数の大きな山になり、すべてが名前空間を共有し、互いにステップする可能性があることです。 しかし、さらに悪いことに、Railsヘルパーであらゆる種類のポリモーフィズムを使用する優れた方法はありません。さまざまなコンテキストやタイプにさまざまな実装を提供し、ヘルパーをオーバーライドまたはサブクラス化します。 Railsヘルパークラスは通常、ユーティリティメソッドに使用する必要があり、あらゆる種類のプレゼンテーションロジックのモデル属性のフォーマットなどの特定のユースケースには使用しないでください。 それらを軽くてさわやかに保ちます。
- 懸念事項の使用を避け、代わりにデコレータ/デリゲータを使用してください。 なんで? 結局のところ、懸念はRailsのコア部分であるように思われ、複数のモデル間で共有されるとコードを枯渇させる可能性があります。 それにもかかわらず、主な問題は、懸念がモデルオブジェクトをよりまとまりのあるものにしないことです。 コードはより適切に編成されています。 つまり、モデルのAPIに実際の変更はありません。
- コードをよりクリーンに保ち、関連する属性をグループ化するために、モデルから値オブジェクトを抽出してみてください。
- ビューごとに常に1つのインスタンス変数を渡します。
リファクタリング
始める前に、もう1つ話し合いたいと思います。 リファクタリングを開始すると、通常、 「それは本当に良いリファクタリングですか?」と自問することになります。
責任をより分離または分離していると感じた場合(コードと新しいファイルを追加することを意味する場合でも)、これは通常は良いことです。 結局のところ、アプリケーションの分離は非常に優れた方法であり、適切な単体テストを簡単に実行できるようになります。
ロジックをコントローラーからモデルに移動するなどのことについては説明しません。これは、既に実行していて、Rails(通常はSkinnyコントローラーとFATモデル)を快適に使用できると想定しているためです。
この記事をしっかりと保つために、ここではテストについては説明しませんが、テストすべきではないという意味ではありません。
それどころか、先に進む前に、常にテストを開始して、問題がないことを確認する必要があります。 これは、特にリファクタリングの場合は必須です。
次に、変更を実装し、コードの関連部分のすべてのテストに合格することを確認できます。
値オブジェクトの抽出
まず、値オブジェクトとは何ですか?
マーティンファウラーは説明します:
値オブジェクトは、お金や日付範囲のオブジェクトなどの小さなオブジェクトです。 それらの重要な特性は、参照セマンティクスではなく値セマンティクスに従うことです。
概念がそれ自体の抽象化に値し、その平等が価値ではなくアイデンティティに基づいているという状況に遭遇することがあります。 例には、Rubyの日付、URI、パス名が含まれます。 値オブジェクト(またはドメインモデル)への抽出は非常に便利です。
なぜわざわざ?
Valueオブジェクトの最大の利点の1つは、コードでの表現力です。 あなたのコードははるかに明確になる傾向があります、あるいは少なくともあなたが良い命名慣行を持っているならそれはそうすることができます。 値オブジェクトは抽象化されているため、コードがよりクリーンになり、エラーが少なくなります。
もう1つの大きな勝利は、不変性です。 オブジェクトの不変性は非常に重要です。 値オブジェクトで使用できる特定のデータセットを格納する場合、通常、そのデータを操作したくありません。
これはいつ役に立ちますか?
単一の、万能の答えはありません。 あなたにとって最善のことと、与えられた状況で意味のあることをしてください。
それを超えて、しかし、私がその決定をするのを助けるために私が使ういくつかのガイドラインがあります。
メソッドのグループが関連していると考えると、Valueオブジェクトを使用すると、より表現力豊かになります。 この表現力は、Valueオブジェクトが別個のデータセットを表す必要があることを意味します。これは、平均的な開発者がオブジェクトの名前を見るだけで推測できます。
これはどのように行われますか?
値オブジェクトは、いくつかの基本的なルールに従う必要があります。
- 値オブジェクトには複数の属性が必要です。
- 属性は、オブジェクトのライフサイクル全体を通じて不変である必要があります。
- 同等性は、オブジェクトの属性によって決定されます。
この例では、 EntryStatus
値オブジェクトを作成して、Entry#status_weather属性とEntry#status_weather
Entry#status_landform
属性を独自のクラスに抽象化します。これは次のようになります。
- 要旨サンプル
注:これは、 ActiveRecord::Base
から継承しない単なる古いRubyオブジェクト(PORO)です。 属性のリーダーメソッドを定義し、初期化時にそれらを割り当てています。 また、同等のミックスインを使用して、(<=>)メソッドを使用してオブジェクトを比較しました。
作成した値オブジェクトを使用するようにEntry
モデルを変更できます。
- 要旨サンプル
それに応じて新しい値オブジェクトを使用するようにEntryController#create
メソッドを変更することもできます。
- 要旨サンプル
サービスオブジェクトの抽出
では、Serviceオブジェクトとは何ですか?
サービスオブジェクトの仕事は、ビジネスロジックの特定のビットのコードを保持することです。 少数のオブジェクトに必要なすべてのロジックに対して多数のメソッドが含まれる「ファットモデル」スタイルとは異なり、Serviceオブジェクトを使用すると、多くのクラスが生成され、それぞれが単一の目的を果たします。
なんで? メリットは何ですか?
- デカップリング。 サービスオブジェクトは、オブジェクト間の分離を強化するのに役立ちます。
- 可視性。 サービスオブジェクト(適切な名前が付けられている場合)は、アプリケーションの機能を示します。 サービスディレクトリを一瞥するだけで、アプリケーションが提供する機能を確認できます。
- モデルとコントローラーをクリーンアップします。 コントローラーは、要求(params、session、cookies)を引数に変換し、それらをサービスに渡し、サービスの応答に従ってリダイレクトまたはレンダリングします。 モデルは関連付けと永続性のみを扱いますが。 コントローラ/モデルからサービスオブジェクトにコードを抽出すると、SRPがサポートされ、コードがより分離されます。 その場合、モデルの責任は、関連付けとレコードの保存/削除のみを処理することですが、サービスオブジェクトには単一責任(SRP)があります。 これにより、設計と単体テストが向上します。
- DRYとEmbraceが変わります。 サービスオブジェクトはできるだけシンプルで小さくしています。 他のサービスオブジェクトと一緒にサービスオブジェクトを作成し、それらを再利用します。
- テストスイートをクリーンアップして高速化します。 サービスは、1つのエントリポイント(呼び出しメソッド)を持つ小さなRubyオブジェクトであるため、テストが簡単で高速です。 複雑なサービスは他のサービスで構成されているため、テストを簡単に分割できます。 また、サービスオブジェクトを使用すると、Rails環境全体をロードすることなく、関連するオブジェクトをモック/スタブすることが容易になります。
- どこからでも呼び出し可能。 サービスオブジェクトは、コントローラーだけでなく、他のサービスオブジェクト、DelayedJob / Rescue / Sidekiq Jobs、Rakeタスク、コンソールなどから呼び出される可能性があります。
一方、完璧なものはありません。 サービスオブジェクトの欠点は、非常に単純なアクションではやり過ぎになる可能性があることです。 このような場合、コードを単純化するのではなく、複雑にすることになりかねません。

いつサービスオブジェクトを抽出する必要がありますか?
ここにも厳格なルールはありません。
通常、サービスオブジェクトは中規模から大規模のシステムに適しています。 標準のCRUD操作を超える適切な量のロジックを備えたもの。
したがって、コードスニペットが追加しようとしているディレクトリに属していない可能性があると思われる場合は、代わりにサービスオブジェクトに移動するかどうかを再検討して確認することをお勧めします。
サービスオブジェクトをいつ使用するかを示すいくつかの指標は次のとおりです。
- アクションは複雑です。
- アクションは複数のモデルにまたがります。
- アクションは外部サービスと相互作用します。
- アクションは、基礎となるモデルの中心的な関心事ではありません。
- アクションを実行する方法は複数あります。
サービスオブジェクトをどのように設計する必要がありますか?
サービスオブジェクトのクラスの設計は比較的簡単です。特別な宝石は必要なく、新しいDSLを学ぶ必要がなく、すでに持っているソフトウェア設計スキルに多かれ少なかれ依存することができるからです。
私は通常、次のガイドラインと規則を使用してサービスオブジェクトを設計します。
- オブジェクトの状態を保存しないでください。
- クラスメソッドではなく、インスタンスメソッドを使用します。
- パブリックメソッドは非常に少ないはずです(SRPをサポートするメソッドが望ましい)。
- メソッドは、ブール値ではなく、豊富な結果オブジェクトを返す必要があります。
- サービスは
app/services
ディレクトリにあります。 ビジネスロジックが多いドメインにはサブディレクトリを使用することをお勧めします。 たとえば、ファイルapp/services/report/generate_weekly.rb
はReport::GenerateWeekly
を定義し、app/services/report/publish_monthly.rb
はReport::PublishMonthly
を定義します。 - サービスは動詞で始まります(サービスで終わらない):
ApproveTransaction
、SendTestNewsletter
、ImportUsersFromCsv
。 - サービスは呼び出しメソッドに応答します。 別の動詞を使用すると、少し冗長になることがわかりました。ApproveTransaction.approve()はうまく読みません。 また、callメソッドは、lambda、procs、およびメソッドオブジェクトの事実上のメソッドです。
StatisticsController#index
を見ると、コントローラーに結合されたメソッドのグループ( weeks_to_date_from
、 weeks_to_date_to
、 avg_distance
など)に気付くでしょう。 それは本当に良くありません。 statistics_controller
の外部で週次レポートを生成する場合は、影響を考慮してください。
この例では、 Report::GenerateWeekly
を作成し、 StatisticsController
からレポートロジックを抽出します。
- 要旨サンプル
したがって、 StatisticsController#index
はよりクリーンに見えます。
- 要旨サンプル
Serviceオブジェクトパターンを適用することで、特定の複雑なアクションの周りにコードをバンドルし、より小さく、より明確なメソッドの作成を促進します。
宿題: Struct
の代わりにWeeklyReport
にValueオブジェクトを使用することを検討してください。
コントローラからクエリオブジェクトを抽出する
Queryオブジェクトとは何ですか?
Queryオブジェクトは、データベースクエリを表すPOROです。 クエリロジックを非表示にしながら、アプリケーションのさまざまな場所で再利用できます。 また、テストに適した分離ユニットも提供します。
複雑なSQL/NoSQLクエリを独自のクラスに抽出する必要があります。
各Queryオブジェクトは、基準/ビジネスルールに基づいて結果セットを返す責任があります。
この例では、複雑なクエリがないため、Queryオブジェクトを使用するのは効率的ではありません。 ただし、デモンストレーションの目的で、 Report::GenerateWeekly#call
でクエリを抽出し、 generate_entries_query.rb
を作成してみましょう。
- 要旨サンプル
そして、 Report::GenerateWeekly#call
で、次を置き換えましょう:
def call @user.entries.group_by(&:week).map do |week, entries| WeeklyReport.new( ... ) end end
と:
def call weekly_grouped_entries = GroupEntriesQuery.new(@user).call weekly_grouped_entries.map do |week, entries| WeeklyReport.new( ... ) end end
クエリオブジェクトパターンは、モデルロジックをクラスの動作に厳密に関連付け、コントローラーをスキニーに保つのに役立ちます。 それらは単なる古いRubyクラスにすぎないため、クエリオブジェクトはActiveRecord::Base
から継承する必要はなく、クエリの実行のみを担当する必要があります。
サービスオブジェクトへのエントリの作成を抽出します
次に、新しいサービスオブジェクトへの新しいエントリを作成するロジックを抽出しましょう。 規則を使用してCreateEntry
を作成しましょう:
- 要旨サンプル
そして今、 EntriesController#create
は次のようになっています。
def create begin CreateEntry.new(current_user, entry_params).call flash[:notice] = 'Entry was successfully created.' rescue Exception => e flash[:error] = e.message end redirect_to root_path end
検証をフォームオブジェクトに移動する
さて、ここで物事はより面白くなり始めます。
ガイドラインでは、モデルに関連付けと定数を含めることに同意しましたが、それ以外は何も含まないことに同意しました(検証とコールバックはありません)。 それでは、コールバックを削除することから始めて、代わりにFormオブジェクトを使用しましょう。
Formオブジェクトは、Plain Old Ruby Object(PORO)です。 データベースと通信する必要がある場合は常に、コントローラー/サービスオブジェクトから引き継ぎます。
なぜFormオブジェクトを使用するのですか?
アプリのリファクタリングを検討する場合は、単一責任の原則(SRP)を念頭に置くことをお勧めします。
SRPは、クラスが何を担当すべきかについて、より適切な設計上の決定を下すのに役立ちます。
たとえば、データベーステーブルモデル(RailsのコンテキストではActiveRecordモデル)は、コード内の単一のデータベースレコードを表すため、ユーザーが行っていることに関係する理由はありません。
これがFormオブジェクトの出番です。
Formオブジェクトは、アプリケーションでフォームを表す役割を果たします。 したがって、各入力フィールドはクラスの属性として扱うことができます。 これらの属性がいくつかの検証ルールを満たしていることを検証でき、「クリーンな」データを必要な場所(データベースモデルや検索クエリビルダーなど)に渡すことができます。
いつFormオブジェクトを使用する必要がありますか?
- Railsモデルから検証を抽出する場合。
- 1回のフォーム送信で複数のモデルを更新できる場合は、Formオブジェクトを作成することをお勧めします。
これにより、すべてのフォームロジック(命名規則、検証など)を1か所にまとめることができます。
Formオブジェクトをどのように作成しますか?
- プレーンなRubyクラスを作成します。
-
ActiveModel::Model
を含める(Rails 3では、代わりに名前付け、変換、検証を含める必要があります) - 新しいフォームクラスを通常のActiveRecordモデルであるかのように使い始めます。最大の違いは、このオブジェクトに格納されているデータを永続化できないことです。
リフォームジェムを使用できますが、POROに固執して、次のようなentry_form.rb
を作成することに注意してください。
- 要旨サンプル
そして、 CreateEntry
を変更して、FormオブジェクトEntryForm
の使用を開始します。
class CreateEntry ...... ...... def call @entry_form = ::EntryForm.new(@params) if @entry_form.valid? .... else .... end end end
注: ServiceオブジェクトからFormオブジェクトにアクセスする必要はなく、コントローラーから直接Formオブジェクトを呼び出すことができると言う人もいます。これは有効な引数です。 ただし、明確なフローが必要なため、常にServiceオブジェクトからFormオブジェクトを呼び出します。
コールバックをサービスオブジェクトに移動する
以前に同意したように、モデルに検証とコールバックが含まれることは望ましくありません。 Formオブジェクトを使用して検証を抽出しました。 ただし、まだいくつかのコールバックを使用しています( Entry
モデルcompare_speed_and_notify_user
のafter_create
)。
モデルからコールバックを削除したいのはなぜですか?
Rails開発者は通常、テスト中にコールバックの痛みに気づき始めます。 ActiveRecordモデルをテストしていない場合は、アプリケーションが大きくなり、コールバックを呼び出すか回避するためにより多くのロジックが必要になるため、後で痛みに気付くようになります。
after_*
コールバックは、主にオブジェクトの保存または永続化に関連して使用されます。
オブジェクトが保存されると、オブジェクトの目的(つまり責任)が果たされます。 したがって、オブジェクトが保存された後もコールバックが呼び出されている場合は、コールバックがオブジェクトの責任範囲外に到達している可能性があります。そのときに問題が発生します。
この例では、エントリを保存した後にSMSをユーザーに送信していますが、これは実際にはエントリのドメインとは関係ありません。
この問題を解決する簡単な方法は、コールバックを関連するサービスオブジェクトに移動することです。 結局のところ、エンドユーザーへのSMSの送信は、エントリモデル自体ではなく、 CreateEntry
サービスオブジェクトに関連しています。
そうすることで、テストでcompare_speed_and_notify_user
メソッドをスタブ化する必要がなくなります。 SMSを送信せずにエントリを作成するのは簡単なことであり、クラスに単一責任(SRP)があることを確認することで、優れたオブジェクト指向設計に従っています。
これで、 CreateEntry
は次のようになります。
- 要旨サンプル
ヘルパーの代わりにデコレータを使用する
ビューモデルとデコレータのDraperコレクションは簡単に使用できますが、これまで行ってきたように、この記事ではPOROに固執します。
必要なのは、装飾されたオブジェクトのメソッドを呼び出すクラスです。
method_missing
を使用してそれを実装できますが、Rubyの標準ライブラリSimpleDelegator
を使用します。
次のコードは、 SimpleDelegator
を使用してベースデコレータを実装する方法を示しています。
% app/decorators/base_decorator.rb require 'delegate' class BaseDecorator < SimpleDelegator def initialize(base, view_context) super(base) @object = base @view_context = view_context end private def self.decorates(name) define_method(name) do @object end end def _h @view_context end end
では、なぜ_h
メソッドなのですか?
このメソッドは、ビューコンテキストのプロキシとして機能します。 デフォルトでは、ビューコンテキストはビュークラスのインスタンスであり、デフォルトのビュークラスはActionView::Base
です。 次のようにビューヘルパーにアクセスできます。
_h.content_tag :div, 'my-div', class: 'my-class'
より便利にするために、 ApplicationHelper
にdecorate
メソッドを追加します。
module ApplicationHelper # ..... def decorate(object, klass = nil) klass ||= "#{object.class}Decorator".constantize decorator = klass.new(object, self) yield decorator if block_given? decorator end # ..... end
これで、 EntriesHelper
ヘルパーをデコレータに移動できます。
# app/decorators/entry_decorator.rb class EntryDecorator < BaseDecorator decorates :entry def readable_time_period mins = entry.time_period return Time.at(60 * mins).utc.strftime('%M <small>Mins</small>').html_safe if mins < 60 Time.at(60 * mins).utc.strftime('%H <small>Hour</small> %M <small>Mins</small>').html_safe end def readable_speed "#{sprintf('%0.2f', entry.speed)} <small>Km/H</small>".html_safe end end
そして、次のようにreadable_time_period
とreadable_speed
速度を使用できます。
# app/views/entries/_entry.html.erb - <td><%= readable_speed(entry) %> </td> + <td><%= decorate(entry).readable_speed %> </td>
- <td><%= readable_time_period(entry) %></td> + <td><%= decorate(entry).readable_time_period %></td>
リファクタリング後の構造
最終的にファイルが増えましたが、それは必ずしも悪いことではありません(最初から、この例は説明のみを目的としており、リファクタリングの良いユースケースではないことを認識していました)。
app ├── assets │ └── ... ├── controllers │ ├── application_controller.rb │ ├── entries_controller.rb │ └── statistics_controller.rb ├── decorators │ ├── base_decorator.rb │ └── entry_decorator.rb ├── forms │ └── entry_form.rb ├── helpers │ └── application_helper.rb ├── mailers ├── models │ ├── entry.rb │ ├── entry_status.rb │ └── user.rb ├── queries │ └── group_entries_query.rb ├── services │ ├── create_entry.rb │ └── report │ └── generate_weekly.rb └── views ├── devise │ └── .. ├── entries │ ├── _entry.html.erb │ ├── _form.html.erb │ └── index.html.erb ├── layouts │ └── application.html.erb └── statistics └── index.html.erb
結論
このブログ投稿ではRailsに焦点を当てていますが、RoRは説明されているサービスオブジェクトや他のPOROの依存関係ではありません。 このアプローチは、任意のWebフレームワーク、モバイル、またはコンソールアプリで使用できます。
WebアプリのアーキテクチャとしてMVCを使用することにより、ほとんどの変更がアプリの他の部分に影響を与えるため、すべてが結合されたままになり、速度が低下します。 また、ビジネスロジックをどこに配置するかを考える必要があります。モデル、コントローラー、またはビューのどこに配置する必要がありますか?
シンプルなPOROを使用することで、ビジネスロジックをActiveRecord
から継承しないモデルまたはサービスに移行しました。これは、SRPとより高速な単体テストをサポートするよりクリーンなコードがあることは言うまでもなく、すでに大きな成果です。
クリーンなアーキテクチャは、ユースケースを構造の中央/上部に配置することを目的としているため、アプリの機能を簡単に確認できます。 また、モジュール化されて分離されているため、変更の採用が容易になります。
プレーンオールドRubyオブジェクトとより多くの抽象化を使用することで、懸念を切り離し、テストを簡素化し、クリーンで保守可能なコードを生成する方法を示したことを願っています。