Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Sử Dụng Facade Pattern Trong Rails Để Tối Ưu Hiệu Suất Và Maintainability

Hôm nay, chúng ta sẽ look into software design pattern gọi là Facade. Khi first adopted it, hơi awkward, nhưng càng dùng trong Rails apps càng appreciate usefulness. Quan trọng hơn, nó allowed me test code thoroughly hơn, clean out controllers, reduce logic trong views, và make me think clearly hơn về overall structure của application code.

Là software development pattern, facade framework agnostic nhưng examples tôi provide là cho Ruby on Rails. Tuy nhiên, khuyến khích read through article và try them out regardless framework bạn dùng. Tôi sure once familiar với pattern này, bạn sẽ start seeing opportunities dùng nó trong nhiều parts của codebase.

Không further ado, let's dive right in!

Vấn Đề Với MVC Pattern

MVC (Model-View-Controller) pattern là software development pattern từ những năm 1970. Battle-tested solution design software interfaces, separating programming concerns thành ba main groups communicate amongst each other unique way.

Nhiều large web frameworks emerged early 2000s với MVC pattern as foundation. Spring (Java), Django (Python) và Ruby on Rails (Ruby), đều forged với trinity này at core. Compared to spaghetti-code từ software không dùng nó, MVC pattern là huge achievement và turning point evolution của software development và internet.

Bản chất, MVC pattern allows: user performs action on View. View triggers request to Controller which potentially create/read/update/delete Model. Model transaction responds back to Controller, which renders change user sees reflected in View.

Plenty of pros pattern này. List some:

  • Improves code maintainability bằng separating concerns
  • Allows greater testability (Models, Views, Controllers tested in isolation)
  • Encourages good coding practices bằng enforcing Single Responsibility Principle của SOLID: "A class should have only one reason to change."

Phenomenal achievement cho its time, developers soon realized MVC pattern also somewhat limiting. Variants emerged, như HMVC (hierarchical model–view–controller), MVA (model–view–adapter), MVP (model–view–presenter), MVVM (model–view–viewmodel) và others, all sought address limitations của MVC pattern.

Một problem MVC pattern introduces, và topic bài này: who responsible handle complex view logic? View should simply concerned với presenting data, controller just relaying message received từ model, và model should not concerned với any view logic.

Để help với common conundrum này, all Rails apps get initialized với helpers directory. helper directory chứa modules với methods assist complex View logic.

Example helper trong Rails application:

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

Example simple nhưng demonstrate fact bạn muốn extract decision making từ template itself để reduce complexity.

Helpers nice, nhưng还有另一个 pattern handle complicated View logic become accepted through years, đó là Facade pattern.

Giới Thiệu Facade Pattern

Trong Ruby on Rails application, facades thường placed trong app/facades directory.

Mặc dù similar to helpers, facades không phải group of methods trong module. Facade là PORO (Plain Old Ruby Object) instantiated trong controller, nhưng handle elaborate View business logic. Vì vậy, cho phép benefits sau:

  1. Thay vì single module cho UsersHelper hoặc ArticlesHelper hoặc BooksHelper, mỗi controller action có own Facade: Users::IndexFacade, Articles::ShowFacade, Books::EditFacade.
  2. Hơn modules, facades encourage good coding practices bằng cách allow nest facades để ensure Single Responsibility Principle enforced. Mặc dù probably không muốn facades nested hundreds levels deep, having one or two layers nesting cho improved maintainability và test coverage có thể good thing.

Contrived example:

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

Khi Không Nên Dùng Facade Pattern

Hãy reflect trên những gì facades không phải.

  • Facades không nên placed in classes living, ví dụ, lib directory cho code cần displayed in View. Facade's lifecycle nên generated in Controller action và used in associated View.

  • Facades không meant dùng cho business logic perform CRUD actions (có other patterns cho đó, như Services hoặc Interactors — nhưng đó subject cho another day). Nói cách khác, facades không nên concerned với creating, updating, deleting. Mục tiêu extract intricate presentation logic từ View hoặc Controller và offer single interface access all that information.

  • Last but not least, Facades không phải silver bullet. Không allow bypass MVC pattern, mà rather play along with it. Nếu change occurs in Model, không immediately reflected in View. Như luôn với MVC, controller action phải re-render để Facade display changes on View.

Controller Benefits

Một main, obvious benefit của Facades: dramatically reduce controller logic.

Controller code reduced từ something like this:

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

Thành này:

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

View Benefits

Cho Views, hai main benefits khi dùng Facades:

  1. Conditional checks, inline queries và other logic neatly extracted từ template làm code far more readable. Ví dụ, dùng trong form:
<%= 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?) %>

Có thể thành:

<%= f.label :location %>
<%= f.select :location, options_for_select(@form_facade.user_locations), multiple: @form_facade.multiple_locations? %>
  1. Variables called multiple times có thể cached. Có thể offer significant performance improvements và help remove pesky N+1 queries:
// 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 %>

Thành:

// 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 %>

Testing Benefits

Major benefit Facades: allow test singular bits business logic without phải viết entire controller test, hoặc worse, không phải viết integration test go through flow và reach page just để ensure data presentation as expected.

Vì test single POROs, giúp maintain fast test suite.

Simple example test viết bằng Minitest cho demonstration:

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" do
      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" do
      @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

Unit testing facades considerably improves test suite performance, và every large company eventually encounter slow test suites unless problems like these aren't addressed with some level of seriousness.

One Facade, Two Facades, Three Facades, More?

Bạn might encounter scenario View render partial output some data. Trong case đó, option either dùng parent facade hoặc nested facade. Largely depends on how much logic involved, whether want test separately và whether makes sense extract functionality.

No golden rule cho how many facades dùng hoặc nest within each other. Đó là developer's discretion. Tôi generally prefer single facade cho controller action và limit nesting đến single level để code easier to follow.

General questions tự hỏi khi development:

  • Facade có encapsulate logic tôi trying present on view không?
  • Method trong facade make sense trong context này không?
  • Code easier to follow now, hay harder?

Khi in doubt, always strive make code as easy to follow as possible.

Kết Luận

In conclusion, facades là fantastic pattern keep controllers và views lean, while improving code maintainability, performance và testability.

Tuy nhiên, như any programming paradigm, no silver bullet. Even multitude patterns emerged recent years (HMVC, MVVM, etc.) không be-all-end-all solutions cho complications của software development.

Similar to second law of thermodynamics, states entropy trong closed system always increase, so too in any software project complexity increase và evolve over time. In long run, goal là write code as easy to read, test, maintain và follow as possible; facades offer exactly this.

P.S. Nếu muốn đọc bài Ruby Magic ngay khi xuất bản, đăng ký newsletter Ruby Magic để không bỏ lỡ!