Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

RubyonRailsコントローラーのパターンとアンチパターン

RubyonRailsのパターンとアンチパターンシリーズの第4回へようこそ。

以前は、一般的なパターンとアンチパターン、およびRailsモデルとビューとの関係について説明しました。この投稿では、MVC(Model-View-Controller)デザインパターンの最後の部分であるtheControllerを分析します。 Railsコントローラーに関連するパターンとアンチパターンを詳しく見ていきましょう。

最前線で

Ruby on RailsはWebフレームワークであるため、HTTPリクエストはその重要な部分です。あらゆる種類のクライアントがリクエストを介してRailsバックエンドにアクセスします。これがコントローラーの優れた点です。コントローラーは、リクエストの受信と処理の最前線にいます。これにより、RubyonRailsフレームワークの基本的な部分になります。もちろん、コントローラーの前に来るコードもありますが、コントローラーコードは私たちのほとんどが制御できるものです。

config/routes.rbでルートを定義したら 、設定されたルートでサーバーにアクセスすると、対応するコントローラーが残りの処理を行います。前の文を読むと、すべてがそれと同じくらい単純であるという印象を与えるかもしれません。しかし、多くの場合、多くの重みがコントローラーの肩にかかります。認証と承認の懸念があり、必要なデータを取得する方法や、ビジネスロジックを実行する場所と方法に問題があります。

コントローラ内で発生する可能性のあるこれらの懸念と責任はすべて、いくつかのアンチパターンにつながる可能性があります。最も「有名な」ものの1つは、「太い」コントローラーのアンチパターンです。

脂肪(肥満)コントローラー

コントローラにロジックを入れすぎることの問題は、単一責任原則(SRP)に違反し始めていることです。これは、コントローラー内で多くの作業を行っていることを意味します。多くの場合、これは多くのコードと責任がそこに積み重なることにつながります。ここで、「fat」は、コントローラーファイルに含まれる広範なコードと、コントローラーがサポートするロジックを指します。多くの場合、アンチパターンと見なされます。

コントローラーが何をすべきかについては多くの意見があります。コントローラーが持つべき責任の共通の根拠には、次のものが含まれます。

  • 認証と承認 —リクエストの背後にあるエンティティ(多くの場合、ユーザー)が本人であるかどうか、およびリソースへのアクセスまたはアクションの実行が許可されているかどうかを確認します。多くの場合、認証はセッションまたはCookieに保存されますが、コントローラーは認証データがまだ有効かどうかを確認する必要があります。
  • データの取得 —リクエストに付属するパラメータに基づいて適切なデータを見つけるためのロジックを呼び出す必要があります。完璧な世界では、すべての作業を実行する1つのメソッドを呼び出す必要があります。コントローラーは重い作業を行うべきではなく、さらに委任する必要があります。
  • テンプレートレンダリング —最後に、結果を適切な形式(HTML、JSONなど)でレンダリングすることにより、正しい応答を返す必要があります。または、他のパスまたはURLにリダイレクトする必要があります。

これらのアイデアに従うことで、コントローラーのアクションやコントローラー全般の内部で多くのことを行う必要がなくなります。コントローラレベルでシンプルに保つことで、アプリケーションの他の領域に作業を委任できます。責任を委任して1つずつテストすることで、アプリを堅牢に開発していることが保証されます。

もちろん、上記の原則に従うことはできますが、いくつかの例に熱心でなければなりません。飛び込んで、コントローラーの重量を軽減するために使用できるパターンを見てみましょう。

クエリオブジェクト

コントローラアクション内で発生する問題の1つは、データのクエリが多すぎることです。 Railsモデルのアンチパターンとパターンに関するブログ投稿をフォローすると、モデルにクエリロジックが多すぎるという同様の問題が発生しましたが、今回はクエリオブジェクトと呼ばれるパターンを使用します。クエリオブジェクトは、複雑なクエリを1つのオブジェクトに分離する手法です。

ほとんどの場合、クエリオブジェクトはActiveRecordで初期化されたプレーンな古いRubyオブジェクトです。 関係。一般的なクエリオブジェクトは次のようになります。

# app/queries/all_songs_query.rb
 
class AllSongsQuery
  def initialize(songs = Song.all)
    @songs = songs
  end
 
  def call(params, songs = Song.all)
    songs.where(published: true)
         .where(artist_id: params[:artist_id])
         .order(:title)
  end
end

次のようにコントローラー内で使用するように作られています:

class SongsController < ApplicationController
  def index
    @songs = AllSongsQuery.new.call(all_songs_params)
  end
 
  private
 
  def all_songs_params
    params.slice(:artist_id)
  end
end

クエリオブジェクトの別のアプローチを試すこともできます:

# app/queries/all_songs_query.rb
 
class AllSongsQuery
  attr_reader :songs
 
  def initialize(songs = Song.all)
    @songs = songs
  end
 
  def call(params = {})
    scope = published(songs)
    scope = by_artist_id(scope, params[:artist_id])
    scope = order_by_title(scope)
  end
 
  private
 
  def published(scope)
    scope.where(published: true)
  end
 
  def by_artist_id(scope, artist_id)
    artist_id ? scope.where(artist_id: artist_id) : scope
  end
 
  def order_by_title(scope)
    scope.order(:title)
  end
end

後者のアプローチでは、paramsを作成することで、クエリオブジェクトをより堅牢にします。 オプション。また、AllSongsQuery.new.callを呼び出すことができるようになりました。 。これがあまり好きでない場合は、クラスメソッドを使用できます。クラスメソッドを使用してクエリクラスを作成すると、それは「オブジェクト」ではなくなりますが、これは個人的な好みの問題です。説明のために、AllSongsQueryを作成する方法を見てみましょう。 野生で呼び出す方が簡単です。

# app/queries/all_songs_query.rb
 
class AllSongsQuery
  class << self
    def call(params = {}, songs = Song.all)
      scope = published(songs)
      scope = by_artist_id(scope, params[:artist_id])
      scope = order_by_title(scope)
    end
 
    private
 
    def published(scope)
      scope.where(published: true)
    end
 
    def by_artist_id(scope, artist_id)
      artist_id ? scope.where(artist_id: artist_id) : scope
    end
 
    def order_by_title(scope)
      scope.order(:title)
    end
  end
end

これで、AllSongsQuery.callを呼び出すことができます これで完了です。 paramsを渡すことができます artist_idを使用 。また、何らかの理由で初期スコープを変更する必要がある場合は、初期スコープを渡すことができます。 newの呼び出しを本当に避けたい場合 クエリクラスで、この「トリック」を試してみてください:

# app/queries/application_query.rb
 
class ApplicationQuery
  def self.call(*params)
    new(*params).call
  end
end

ApplicationQueryを作成できます 次に、他のクエリクラスで継承します:

# app/queries/all_songs_query.rb
class AllSongsQuery < ApplicationQuery
  ...
end

あなたはまだAllSongsQuery.callを保持しました 、しかしあなたはそれをよりエレガントにしました。

クエリオブジェクトの優れている点は、それらを個別にテストし、それらが実行すべきことを実行していることを確認できることです。さらに、これらのクエリクラスを拡張して、コントローラのロジックについてあまり心配することなくテストできます。注意すべき点の1つは、リクエストパラメータを他の場所で処理する必要があり、クエリオブジェクトに依存しないようにすることです。どう思いますか、クエリオブジェクトを試してみますか?

提供の準備ができました

OK、データの収集とフェッチをQueryObjectsに委任する方法を処理しました。データ収集とそれをレンダリングするステップの間に積み上げられたロジックをどのように処理しますか?解決策の1つは、サービスと呼ばれるものを使用することであるため、あなたが尋ねたのは良いことです。サービスは、多くの場合、単一の(ビジネス)アクションを実行するPORO(プレーンオールドRubyオブジェクト)と見なされます。先に進んで、このアイデアを少し下で調べます。

2つのサービスがあると想像してください。 1つは領収書を作成し、もう1つは次のようにユーザーに領収書を送信します。

# app/services/create_receipt_service.rb
class CreateReceiptService
  def self.call(total, user_id)
    Receipt.create!(total: total, user_id: user_id)
  end
end
 
# app/services/send_receipt_service.rb
class SendReceiptService
  def self.call(receipt)
    UserMailer.send_receipt(receipt).deliver_later
  end
end

次に、コントローラーでSendReceiptServiceを呼び出します。 このように:

# app/controllers/receipts_controller.rb
 
class ReceiptsController < ApplicationController
  def create
    receipt = CreateReceiptService.call(total: receipt_params[:total],
                                        user_id: receipt_params[:user_id])
 
    SendReceiptService.call(receipt)
  end
end

これで、2つのサービスがすべての作業を実行し、コントローラーがそれらを呼び出すだけです。これらを個別にテストできますが、問題は、サービス間に明確な接続がないことです。はい、理論的には、それらすべてが単一のビジネスアクションを実行します。しかし、利害関係者の観点から抽象化レベルを考えると、レシートを作成するアクションの彼らの見方には、それを電子メールで送信することが含まれます。抽象化のレベルは「正しい」™️ですか?

この思考実験をもう少し複雑にするために、領収書の合計を計算するか、領収書の作成中にどこかから取得する必要があるという要件を追加しましょう。それではどうしますか?合計の合計を処理する別のサービスを作成しますか?答えは、単一責任原則(SRP)に従い、物事を互いに引き離すことかもしれません。

# app/services/create_receipt_service.rb
class CreateReceiptService
  ...
end
 
# app/services/send_receipt_service.rb
class SendReceiptService
  ...
end
 
# app/services/calculate_receipt_total_service.rb
class CalculateReceiptTotalService
  ...
end
 
# app/controllers/receipts_controller.rb
class ReceiptsController < ApplicationController
  def create
    total = CalculateReceiptTotalService.call(user_id: receipts_controller[:user_id])
 
    receipt = CreateReceiptService.call(total: total,
                                        user_id: receipt_params[:user_id])
 
    SendReceiptService.call(receipt)
  end
end

SRPに従うことで、サービスをReceiptCreationのようなより大きな抽象化にまとめることができるようになります。 処理する。この「プロセス」クラスを作成することにより、プロセスを完了するために必要なすべてのアクションをグループ化できます。このアイデアについてどう思いますか?最初は抽象化しすぎているように聞こえるかもしれませんが、これらのアクションをあちこちで呼び出すと有益な場合があります。これが適切に聞こえる場合は、Trailblazerの操作を確認してください。

要約すると、新しいCalculateReceiptTotalService サービスはすべての数の処理に対処できます。 CreateReceiptService データベースに領収書を書き込む責任があります。 SendReceiptService 領収書に関するメールをユーザーに送信するためにあります。これらの小さくて焦点を絞ったクラスがあると、他のユースケースでそれらを簡単に組み合わせることができるため、コードベースの保守とテストが容易になります。

サービスのバックストーリー

Rubyの世界では、サービスクラスを使用するアプローチは、アクション、操作などとしても知られています。これらすべての要約は、コマンドパターンです。コマンドパターンの背後にある考え方は、オブジェクト(またはこの例ではクラス)が、ビジネスアクションの実行またはイベントのトリガーに必要なすべての情報をカプセル化することです。コマンドの呼び出し元が知っておくべき情報は次のとおりです。

  • コマンドの名前
  • コマンドオブジェクト/クラスで呼び出すメソッド名
  • メソッドパラメータに渡される値

したがって、この場合、コマンドの呼び出し元はコントローラーです。アプローチは非常に似ていますが、Rubyでの命名は「サービス」です。

作業を分割する

コントローラがサードパーティのサービスを呼び出していて、レンダリングをブロックしている場合は、これらの呼び出しを抽出して、別のコントローラアクションで個別にレンダリングする必要があります。この例としては、本の情報をレンダリングして、実際には影響を与えられない他のサービス(Goodreadsなど)からその評価を取得しようとする場合があります。

# app/controllers/books_controller.rb
 
class BooksController < ApplicationController
  def show
    @book = Book.find(params[:id])
 
    @rating = GoodreadsRatingService.new(book).call
  end
end

Goodreadsがダウンしているなどの場合、ユーザーはGoodreadsサーバーへのリクエストがタイムアウトするのを待つ必要があります。または、サーバーで何かが遅い場合、ページの読み込みが遅くなります。サードパーティサービスの呼び出しを次のような別のアクションに抽出できます:

# app/controllers/books_controller.rb
 
class BooksController < ApplicationController
  ...
 
  def show
    @book = Book.find(params[:id])
  end
 
  def rating
    @rating = GoodreadsRatingService.new(@book).call
 
    render partial: 'book_rating'
  end
 
  ...
end

次に、ratingを呼び出す必要があります あなたの見解からの道、しかしねえ、あなたのshowactionはもうブロッカーを持っていません。また、'book_rating'partialが必要です。これをより簡単に行うには、render_async gemを使用できます。本の評価をレンダリングする場所に次のステートメントを入力するだけです:

<%= render_async book_rating_path %>

レーティングをbook_ratingにレンダリングするためのHTMLを抽出します 部分的、そして置く:

<%= content_for :render_async %>

レイアウトファイル内で、gemはbook_rating_pathを呼び出します ページが読み込まれるとAJAXrequestを使用し、評価が取得されると、ページに表示されます。これによる大きな利点の1つは、評価を個別に読み込むことで、ユーザーが本のページをより速く表示できるようになることです。

または、必要に応じて、Basecampのターボフレームを使用できます。考え方は同じですが、<turbo-frame>を使用するだけです。 マークアップの要素は次のようになります:

<turbo-frame id="rating_1" src="/books/1/rating"> </turbo-frame>

どのオプションを選択する場合でも、メインのコントローラーアクションから重い作業や不安定な作業を分割し、できるだけ早くページをユーザーに表示することをお勧めします。

最終的な考え

コントローラーを薄くして、他のメソッドの単なる「呼び出し元」としてそれらを描くというアイデアが好きなら、この投稿は、コントローラーをそのように保つ方法についての洞察をもたらしたと思います。ここで説明したいくつかのパターンとアンチパターンは、もちろん、完全なリストではありません。何が良いか、何が好きかについてアイデアがあれば、Twitterで連絡してください。話し合うことができます。

このシリーズに注目してください。少なくとももう1つブログ投稿を行い、Railsの一般的な問題とシリーズのポイントをまとめます。

次回まで、乾杯!

P.S。 Ruby Magicの投稿をマスコミから離れたらすぐに読みたい場合は、Ruby Magicニュースレターを購読して、投稿を1つも見逃さないでください。


  1. SMバスコントローラーとそのドライバー

    システム管理バス(多くの場合、SMバス、SMBus、またはSMBと短縮されます)コントローラーは、シングルエンドの2線式バスであり、非常にシンプルで、軽量通信を目的として設計されています。 SMバスコントローラは、コンピュータと、電源ユニット(PSU)や冷却ファンなどの最も重要なハードウェアコンポーネントとの間の効率的でシームレスな通信を可能にするコンポーネントです。 SMバスコントローラを使用すると、基本的に、コンピュータは電源ユニットのオン/オフや冷却ファンの制御などのコマンドを正常に処理および実行できます。 ただし、SMバスコントローラの機能は、オペレーティングシステムがPSUや冷却フ

  2. Ruby on Railsとは何ですか?なぜそれが役立つのですか?

    Ruby on Rails(RoRの場合もある)は、最も人気のあるオープンソースのWebアプリケーションフレームワークです。 Rubyプログラミング言語で構築されています。 Railsを使用すると、単純なものから複雑なものまで、アプリケーションの構築に役立ちます。Railsで実行できることには制限がありません。 フレームワークとは何ですか? フレームワークは、ソフトウェアを作成するときに使用する特定の構造を提供するコード、ツール、およびユーティリティのコレクションです。 この構造により、コードがより整理されます。 正しく使うことを学ぶと、作業が簡単になります。 レールは正確に何を