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

パフォーマンスと保守性のためのレールのファサードパターン

今日の投稿では、Facadeと呼ばれるソフトウェアデザインパターンを調べます。最初に採用したときは少し違和感がありましたが、Railsアプリで使用すればするほど、その有用性を高く評価するようになりました。さらに重要なことに、コードをより徹底的にテストし、コントローラーをクリーンアップし、ビュー内のロジックを減らし、アプリケーションのコードの全体的な構造についてより明確に考えることができました。

ソフトウェア開発パターンであるため、ファサードはフレームワークに依存しませんが、ここで提供する例はRubyonRails用です。ただし、使用しているフレームワークに関係なく、この記事を読んで試してみることをお勧めします。このパターンに慣れると、コードベースの多くの部分でこのパターンを使用できるようになると確信しています。

面倒なことはせずに、すぐに飛び込みましょう!

MVCパターンの問題

MVC(Model-View-Controller)パターンは、1970年代にさかのぼるソフトウェア開発パターンです。これは、ソフトウェアインターフェースを設計するための、戦いでテストされたソリューションであり、プログラミングの懸念事項を、独自の方法で相互に通信する3つの主要なグループに分けます。

多くの大規模なWebフレームワークは、MVCパターンを基盤として2000年代初頭に登場しました。 Spring(Javaの場合)、Django(Pythonの場合)、Ruby on Rails(Rubyの場合)はすべて、相互接続された要素のこの三位一体をコアとして鍛造されました。それを利用しなかったソフトウェアから生じたスパゲッティコードと比較して、MVCパターンは、ソフトウェア開発とインターネットの両方の進化における大きな成果であり、ターニングポイントでした。

基本的に、Model-View-Controllerパターンでは、次のことが可能です。ユーザーがビューに対してアクションを実行します。ビューは、モデルを作成/読み取り/更新または削除する可能性のあるコントローラーへの要求をトリガーします。モデルトランザクションはコントローラーに応答します。コントローラーは、ユーザーに表示される変更をビューに反映させます。

このプログラミングパターンには多くの長所があります。いくつかリストするには:

  • 関心の分離により、コードの保守性が向上します
  • より優れたテスト容易性が可能になります(モデル、ビュー、およびコントローラーは個別にテストできます)
  • SOLIDの単一責任原則を適用することにより、優れたコーディング慣行を促進します。「クラスには、変更する理由が1つだけあるはずです。」

当時の驚異的な成果である開発者は、MVCパターンもやや制限的であることにすぐに気づきました。 HMVC(階層モデル-ビュー-コントローラー)、MVA(モデル-ビュー-アダプター)、MVP(モデル-ビュー-プレゼンター)、MVVM(モデル-ビュー-ビューモデル)など、すべてが求めていたバリアントが出現し始めました。 MVCパターンの制限に対処します。

MVCパターンがもたらす問題の1つであり、今日の記事のトピックは次のとおりです。複雑なビューロジックの処理を担当するのは誰ですか。ビューは単にデータの提示に関係している必要があり、コントローラーはモデルから受信したメッセージを中継しているだけであり、モデルはビューロジックに関係してはなりません。

この一般的な難問を解決するために、すべてのRailsアプリケーションはhelpersで初期化されます。 ディレクトリ。 helper ディレクトリには、複雑なビューロジックを支援するメソッドを持つモジュールを含めることができます。

Railsアプリケーション内のヘルパーの例を次に示します。

app/helpers/application_helper.rb

module ApplicationHelper
  def display_ad_type(advertisement)
    type = advertisement.ad_type
    case type
    when 'foo'
      content_tag(:span, class: "foo ad-#{type}") { type }
    when 'bar'
      content_tag(:p, 'bar advertisement')
    else
      content_tag(:span, class: "badge ads-badge badge-pill ad-#{type}") { type }
    end
  end
end

この例は単純ですが、複雑さを軽減するために、テンプレート自体からこの種の意思決定を抽出する必要があるという事実を示しています。

ヘルパーは素晴らしいですが、長年にわたって受け入れられるようになった複雑なビューロジックを処理するためのさらに別のパターンがあります。それはファサードパターンです。

ファサードパターンの概要

Ruby on Railsアプリケーションでは、ファサードは通常app/facades内に配置されます ディレクトリ。

helpersに似ていますが 、facades モジュール内のメソッドのグループではありません。ファサードは、コントローラー内でインスタンス化されるPORO(Plain Old Ruby Object)ですが、複雑なViewビジネスロジックを処理するものです。そのため、次の利点があります。

  1. UsersHelper用の単一のモジュールを持つのではなく またはArticlesHelper またはBooksHelper 、各コントローラーアクションは独自のファサードを持つことができます:Users::IndexFacadeArticles::ShowFacadeBooks::EditFacade
  2. モジュールよりも、ファサードは、単一責任の原則が確実に実施されるようにファサードをネストできるようにすることで、優れたコーディング手法を促進します。数百レベルの深さでネストされたファサードはおそらく望ましくありませんが、保守性とテストカバレッジを向上させるために、1層または2層のネストを行うことは良いことです。

不自然な例を次に示します。

module Books
  class IndexFacade
    attr_reader :books, :params, :user
 
    def initialize(user:, params:)
      @params = params
      @user   = user
      @books  = user.books
    end
 
    def filtered_books
      @filtered_books ||= begin
        scope = if query.present?
                  books.where('name ILIKE ?', "%#{query}%")
                elsif isbn.present?
                  books.where(isbn: isbn)
                else
                  books
                end
 
        scope.order(created_at: :desc).page(params[:page])
      end
    end
 
    def recommended
      # We have a nested facade here.
      # The `Recommended Books` part of the view has a
      # single responsibility so best to extract it
      # to improve its encapsulation and testability.
      @recommended ||= Books::RecommendedFacade.new(
        books: books,
        user: user
      )
    end
 
    private
 
    def query
      @query ||= params[:query]
    end
 
    def isbn
      @isbn ||= params[:isbn]
    end
  end
end

ファサードパターンを使用しない場合

少し時間を取って、ファサードが何でないかについても考えてみましょう。

  • ファサードは、たとえばlibにあるクラスに配置しないでください。 ビューに表示する必要のあるコードのディレクトリ。ファサードのライフサイクルは、コントローラーアクションで生成され、関連するビューで使用される必要があります。

  • ファサードは、CRUDアクションを実行するためのビジネスロジックに使用されることを意図したものではありません(サービスやインタラクターなど、他のパターンがありますが、それは別の日の主題です)。更新または削除。彼らの目的は、ビューまたはコントローラーから複雑なプレゼンテーションロジックを抽出し、そのすべての情報にアクセスするための単一のインターフェイスを提供することです。

  • 最後になりましたが、ファサードは特効薬ではありません。それらはMVCパターンをバイパスすることを許可しませんが、むしろそれと一緒に再生します。モデルに変更が発生した場合、その変更はビューにすぐには反映されません。 MVCの場合は常にそうであるように、ファサードがビューに変更を表示するには、コントローラーアクションを再レンダリングする必要があります。

コントローラーのメリット

ファサードの主な明らかな利点の1つは、コントローラーロジックを大幅に削減できることです。

コントローラコードは次のようなものから削減されます:

class BooksController < ApplicationController
  def index
    @books  = if params[:query].present?
                current_user.books.where('name ILIKE ?', "%#{params[:query]}%")
              elsif params[:isbn].present?
                current_user.books.where(isbn: params[:isbn])
              else
                current_user.books
              end
 
    @books.order(created_at: :desc).page(params[:page])
    @recommended = @books.where(some_complex_query: true)
  end
end

これに:

class BooksController < ApplicationController
  def index
    @index_facade = Books::IndexFacade.new(user: current_user, params: params)
  end
end

メリットを表示

ビューの場合、ファサードを使用する場合の主な利点は2つあります。

  1. 条件付きチェック、インラインクエリ、およびその他のロジックをテンプレート自体から適切に抽出できるため、コードがはるかに読みやすくなります。たとえば、次の形式で使用できます。
<%= f.label :location %>
<%= f.select :location, options_for_select(User::LOCATION_TYPES.map { |type| [type.underscore.humanize, type] }.sort.prepend(['All', 'all'])), multiple: (current_user.active_ips.size > 1 && current_user.settings.use_multiple_locations?) %>

次のようになる可能性があります:

<%= f.label :location %>
<%= f.select :location, options_for_select(@form_facade.user_locations), multiple: @form_facade.multiple_locations? %>
  1. 複数回呼び出される変数はキャッシュできます。これにより、アプリのパフォーマンスが大幅に向上し、厄介なN+1クエリを削除できます。
// Somewhere in the view, a query is performed.
<% current_user.books.where(isbn: params[:isbn]).each do |book| %>
  // Do things
<% end %>
 
// Somewhere else in the view, the same query is performed again.
<% current_user.books.where(isbn: params[:isbn]).each do |book| %>
  // Do things
<% end %>

次のようになります:

// Somewhere in the view, a query is performed.
<% @index_facade.filtered_books.each do |book| %>
  // Do things
<% end %>
 
// Somewhere else in the view.
// Second query is not performed due to instance variable caching.
<% @index_facade.filtered_books.each do |book| %>
  // Do things
<% end %>

テストのメリット

Facadesの主な利点は、コントローラーテスト全体を記述せずに、またはさらに悪いことに、フローを通過してページに到達する統合テストを記述せずに、ビジネスロジックの単一ビットをテストできることです。データの表示は期待どおりです。

単一のPOROをテストするため、これは高速なテストスイートを維持するのに役立ちます。

デモ目的でMinitestで作成されたテストの簡単な例を次に示します。

require 'test_helper'
 
module Books
  class IndexFacadeTest < ActiveSupport::TestCase
    attr_reader :user, :params
 
    setup do
      @user = User.create(first_name: 'Bob', last_name: 'Dylan')
      @params = {}
    end
 
    test "#filtered_books returns all user's books when params are empty"
      index_facade = Books::IndexFacade.new(user: user, params: params)
 
      expectation = user.books.order(created_at: :desc).page(params[:page])
 
      # Without writing an entire controller test or
      # integration test, we can check whether using the facade with
      # empty parameters will return the correct results
      # to the user.
      assert_equal expectation, index_facade.filtered_books
    end
 
    test "#filtered_books returns books matching a query"
      @params = { query: 'Lord of the Rings' }
      index_facade = Books::IndexFacade.new(user: user, params: params)
 
      expectation = user
        .books
        .where('name ILIKE ?', "%#{params[:query]}%")
        .order(created_at: :desc)
        .page(params[:page])
 
      assert_equal expectation, index_facade.filtered_books
    end
  end
end

単体テストのファサードはテストスイートのパフォーマンスを大幅に向上させ、このような問題がある程度深刻に対処されない限り、すべての大企業は最終的に遅いテストスイートに遭遇します。

1つのファサード、2つのファサード、3つのファサード、その他?

ビューが一部のデータを出力するパーシャルをレンダリングするシナリオに遭遇する可能性があります。その場合、親ファサードを使用するか、ネストされたファサードを使用するかを選択できます。これは、関与するロジックの量、個別にテストするかどうか、機能を抽出することが理にかなっているかどうかに大きく依存します。

使用するファサードの数や、相互にネストするファサードの数についての黄金のルールはありません。それは開発者の裁量によるものです。私は通常、コントローラーアクション用に単一のファサードを使用することを好み、コードを理解しやすくするためにネストを単一レベルに制限します。

開発中に自問できる一般的な質問は次のとおりです。

  • ファサードは、ビューに表示しようとしているロジックをカプセル化していますか?
  • ファサード内の方法は、このコンテキストでは意味がありますか?
  • コードは今ではわかりやすくなっていますか、それともわかりにくいですか?

疑問がある場合は、常にコードをできるだけわかりやすくするように努めてください。

結論

結論として、ファサードは、コードの保守性、パフォーマンス、およびテスト性を向上させながら、コントローラーとビューを無駄のない状態に保つための素晴らしいパターンです。

ただし、他のプログラミングパラダイムと同様に、特効薬はありません。近年出現した多数のパターン(HMVC、MVVMなど)でさえ、ソフトウェア開発の複雑さに対する万能の解決策ではありません。

熱力学の第二法則と同様に、閉鎖系のエントロピーの状態は常に増加するため、どのソフトウェアプロジェクトでも、時間の経過とともに複雑さが増し、進化します。長期的には、目標は、読み、テスト、保守、およびフォローが可能な限り簡単なコードを作成することです。ファサードはまさにこれを提供します。

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


  1. 価格とパフォーマンスに最適なChromeboxMiniPC

    デスクトップ向けのGoogleの軽量ChromeOSは、大型のPCを必要としません。モニターの背面に取り付けることができるミニPCに搭載されているため、これらの安価なChromeboxは優れた購入品です。 これらのデスクトップPCが小さくなると、その機能も小さくなることを知っておく必要があります。しかし、それはWindowsオペレーティングシステムにさらに影響を及ぼします。 Chrome OSは軽量で高速で、基本的なハードウェアでもスムーズに動作します。 それで、あなたは何を買うべきですか?これらはあなたの最良の選択肢です。 最高のChromebox: Asus Chromebox 2 G

  2. Parallels Desktop 17 for Mac がリリースされ、パフォーマンスが向上し、Windows 11 がサポートされました

    Parallels Desktop 17 for Mac は、人気のある仮想化ソフトウェアの最新バージョンで、Intel ベースの Mac と、Apple の社内 M1 プロセッサを揺るがす新しいモデルで本日発売されます。ユニバーサル バイナリ アプリケーションとして、Parallels Desktop 17 は x86 と ARM アーキテクチャの両方に最適化されており、組み込みのインストール アシスタントにより、近日公開予定の Windows 11 と macOS Monterey のプレビュー バージョンに簡単にアクセスできるようになりました。 Parallels Desktop 16