Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

Use the Facade Pattern in Rails to Optimize Performance and Maintainability

ในโพสต์วันนี้จะ looking into software design pattern ชื่อ Facade ตอน adopt ครั้งแรกรู้สึก awkward นิดหน่อย แต่ยิ่งใช้ใน Rails apps ยิ่ง appreciate usefulness สำคัญกว่านั้น allowed me to test code thoroughly, clean out controllers, reduce logic ใน views และทำให้คิดชัดเจนขึ้นเกี่ยวกับ overall structure ของ code

เป็น software development pattern facade framework agnostic แต่ examples ที่นี่สำหรับ Ruby on Rails แนะนำอ่านผ่านและลอง不管 framework ที่ใช้ มั่นใจว่า once familiar กับ pattern นี้ จะเริ่มเห็น opportunities ใช้ในหลายส่วนของ codebase

Without further ado, let's dive right in!

The Problem with the MVC Pattern

MVC (Model-View-Controller) pattern เป็น software development pattern ตั้งแต่ 1970s Battle-tested solution สำหรับ design software interfaces แยก programming concerns เป็นสามกลุ่มหลักที่ communicate ระหว่างกัน uniquely

Web frameworks ใหญ่หลายตัว emerge early 2000s ด้วย MVC pattern เป็น foundation Spring (Java), Django (Python) และ Ruby on Rails (Ruby) forged กับ trinity ของ interconnected elements นี้ที่ core เทียบกับ spaghetti-code จาก software ที่ไม่ใช้ MVC pattern เป็น huge achievement และ turning point ใน evolution ของ software development และ internet

In essence MVC pattern allows: user performs action on View View triggers request to Controller ซึ่ง potentially create/read/update/delete Model Model transaction responds back to Controller ซึ่ง render change ที่ user เห็น reflected ใน View

Pros เยอะของ pattern นี้ รายการบ้าง:

  • Improve code maintainability โดย separate concerns
  • Allow greater testability (Models, Views, Controllers test isolated ได้)
  • Encourage good coding practices โดย enforce Single Responsibility Principle ของ SOLID: "A class should have only one reason to change."

Phenomenal achievement for its time developers ตระหนักเร็วว่า MVC pattern limiting ด้วย Variants emerged เช่น HMVC, MVA, MVP, MVVM และอื่นๆ ซึ่ง sought to address limitations ของ MVC

หนึ่งในปัญหาที่ MVC introduce และ topic ของ article วันนี้คือ: ใคร responsible สำหรับ handle complex view logic? View ควร concerned แค่ presenting data Controller just relaying message จาก Model และ Model ไม่ควร concerned กับ view logic ใดๆ

ช่วย common conundrum นี้ ทุก Rails apps initialized ด้วย helpers directory helper directory มี modules with methods ช่วย complex View logic

Example helper ใน Rails app:

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 แต่ demonstrate ว่าอยาก extract decision making นี้จาก template เองเพื่อ reduce complexity

Helpers ไม่錯 แต่ยังมี pattern อีกอันสำหรับ handle complicated View logic ที่ accepted มาปีหลายๆ คือ Facade pattern

Introduction to the Facade Pattern

ใน Rails application facades ปกติวางใน app/facades directory

คล้าย helpers แต facades ไม่ใช่ group of methods ใน module Facade คือ PORO (Plain Old Ruby Object) ที่ instantiated ภายใน controller แต่ handle elaborate View business logic ทำให้ได้ benefits:

  1. แทน single module สำหรับ UsersHelper หรือ ArticlesHelper หรือ BooksHelper แต่ละ controller action มี Facade ของตัวเอง: Users::IndexFacade, Articles::ShowFacade, Books::EditFacade
  2. มากกว่า modules facades encourage good coding practices โดย allow nest facades เพื่อ enforce Single Responsibility Principle แม้ไม่อยาก facades nested hundreds of levels deep แต่ one or two layers nesting สำหรับ improved maintainability และ test coverage เป็น 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

When Not to Use the Facade Pattern

Reflect บนสิ่งที่ facades ไม่ใช่

  • Facades ไม่ควรวางใน classes ที่อยู่ เช่น lib directory สำหรับ code ที่ต้อง displayed ใน View Facade lifecycle ควร generated ใน Controller action และใช้ใน associated View

  • Facades ไม่ meant สำหรับ business logic perform CRUD actions (มี patterns อื่น เช่น Services หรือ Interactors — แต่วิชานั้นเป็น subject for another day) กล่าวคือ facades ไม่ควร concerned กับ creating, updating หรือ deleting Aim คือ extract intricate presentation logic จาก View หรือ Controller และ offer single interface เข้าถึงข้อมูลทั้งหมด

  • Last but not least Facades ไม่ใช่ silver bullet ไม่ให้ bypass MVC pattern แต่ play along กับมัน ถ้า change เกิดใน Model จะไม่ reflected ใน View ทันที ตามปกติ MVC controller action ต้อง re-render ให้ Facade แสดง changes บน View

Controller Benefits

หนึ่งใน main benefits ของ Facades คือ dramatically reduce controller logic

Controller code ลดจากแบบนี้:

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

View Benefits

สำหรับ Views มี two main benefits ใช้ Facades:

  1. Conditional checks, inline queries และ logic อื่นๆ extract จาก template เนียะ code readable มากขึ้น เช่นใช้ใน 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?) %>

กลายเป็น:

<%= f.label :location %>
<%= f.select :location, options_for_select(@form_facade.user_locations), multiple: @form_facade.multiple_locations? %>
  1. Variables ที่เรียกหลายครั้ง cache ได้ ให้ significant performance improvements และช่วย remove pesky N+1 queries:
// ที่ view บางส่วน queryPerformed
<% current_user.books.where(isbn: params[:isbn]).each do |book| %>
// Do things
<% end %>
// ที่อื่นใน view query เดียวกัน performed อีกครั้ง
<% current_user.books.where(isbn: params[:isbn]).each do |book| %>
// Do things
<% end %>

กลายเป็น:

// ที่ view บางส่วน queryPerformed
<% @index_facade.filtered_books.each do |book| %>
// Do things
<% end %>
// ที่อื่นใน view
// Second query ไม่ performed เนื่องจาก instance variable caching
<% @index_facade.filtered_books.each do |book| %>
// Do things
<% end %>

Testing Benefits

Major benefit ของ Facades คือ allow test singular bits of business logic โดยไม่ต้องเขียน entire controller test หรือ worse integration test ที่ go through flow และ reach page เพื่อ ensure data presentation as expected

Test single POROs ช่วย maintain fast test suite

Simple example test ใน Minitest สำหรับ 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 และทุก large company จะเจอ slow test suites เวลานี้ ถ้า problems เหล่านี้ไม่ addressed ด้วย seriousness ระดับหนึ่ง

One Facade, Two Facades, Three Facades, More?

อาจเจอ scenario view render partial output data ในกรณีนี้มี option ใช้ parent facade หรือ nested facade ขึ้นอยู่กับ logic involved มากแค่ไหน อยาก test แยกหรือไม่ และ extract functionality makes sense

No golden rule สำหรับ how many facades to use หรือ how many facades to nest within each other ขึ้นอยู่กับ developer discretion ผม generally prefer single facade สำหรับ controller action และ limit nesting ไป single level ทำ code easier to follow

General questions ถามตัวเองระหว่าง development:

  • Facade encapsulate logic ที่พยายาม present บน view หรือไม่?
  • Method ภายใน facade make sense ใน context นี้ไหม?
  • Code easier to follow ตอนนี้ หรือ harder?

When in doubt always strive to make code as easy to follow as possible

สรุป

In conclusion facades เป็น fantastic pattern เก็บ controllers และ views lean พร้อม improve code maintainability, performance และ testability

แตเหมือน programming paradigm ใดๆ ไม่มี silver bullet แม้ patterns หลายตัว emerged ใน recent years (HMVC, MVVM, ฯลฯ) ไม่ใช่ be-all-end-all solutions สำหรับ complications ของ software development

คล้าย second law of thermodynamics ระบุว่า state of entropy ใน closed system จะ always increase เหมือนกันใน software project ใดๆ complexity เพิ่มและ evolve ตลอดเวลา In the long run goal คือเขียน code ที่ easy to read, test, maintain และ follow มากที่สุด facades offer exactly this

P.S. หากอยากอ่าน Ruby Magic posts ทันทีที่เผยแพร่ สมัครสมาชิก Ruby Magic newsletter และไม่พลาดโพสต์เด็ดๆ!