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

รูปแบบ Facade ใน Rails เพื่อประสิทธิภาพและการบำรุงรักษา

ในโพสต์ของวันนี้ เราจะพิจารณารูปแบบการออกแบบซอฟต์แวร์ที่เรียกว่า Facade เมื่อฉันนำมาใช้ครั้งแรก ฉันรู้สึกอึดอัดเล็กน้อย แต่ยิ่งฉันใช้มันในแอพ Rails ของฉันมากเท่าไหร่ ฉันก็ยิ่งรู้สึกซาบซึ้งกับประโยชน์ของมันมากขึ้นเท่านั้น ที่สำคัญกว่านั้น ทำให้ฉันได้ทดสอบโค้ดอย่างละเอียดยิ่งขึ้น เพื่อล้างตัวควบคุม ลดตรรกะภายในมุมมองของฉัน และทำให้ฉันคิดได้ชัดเจนยิ่งขึ้นเกี่ยวกับโครงสร้างโดยรวมของโค้ดของแอปพลิเคชัน

เป็นรูปแบบการพัฒนาซอฟต์แวร์ Facade เป็นเฟรมเวิร์กที่ไม่เชื่อเรื่องพระเจ้า แต่ตัวอย่างที่ฉันจะให้ที่นี่มีไว้สำหรับ Ruby on Rails อย่างไรก็ตาม เราขอแนะนำให้คุณอ่านบทความนี้และลองใช้ดู ไม่ว่าคุณจะใช้เฟรมเวิร์กใดก็ตาม ฉันแน่ใจว่าเมื่อคุณคุ้นเคยกับรูปแบบนี้แล้ว คุณจะเริ่มเห็นโอกาสในการใช้รูปแบบนี้ในหลายส่วนของโค้ดเบสของคุณ

เพื่อไม่ให้เป็นการเสียเวลา มาดำน้ำกัน!

ปัญหาเกี่ยวกับรูปแบบ MVC

รูปแบบ MVC (Model-View-Controller) เป็นรูปแบบการพัฒนาซอฟต์แวร์ที่มีอายุย้อนไปถึงปี 1970 เป็นโซลูชันที่ผ่านการทดสอบแล้วสำหรับการออกแบบอินเทอร์เฟซของซอฟต์แวร์ โดยแยกข้อกังวลด้านการเขียนโปรแกรมออกเป็น 3 กลุ่มหลักที่สื่อสารระหว่างกันด้วยวิธีที่ไม่ซ้ำใคร

เว็บเฟรมเวิร์กขนาดใหญ่จำนวนมากเกิดขึ้นในช่วงต้นทศวรรษ 2000 โดยมีรูปแบบ MVC เป็นรากฐาน Spring (สำหรับ Java), Django (สำหรับ Python) และ Ruby on Rails (สำหรับ Ruby) ล้วนถูกหลอมรวมด้วยองค์ประกอบที่เชื่อมต่อถึงกันทั้งสามส่วนนี้เป็นแกนหลัก เมื่อเทียบกับรหัสปาเก็ตตี้ที่เกิดจากซอฟต์แวร์ที่ไม่ได้ใช้งาน รูปแบบ MVC ถือเป็นความสำเร็จครั้งยิ่งใหญ่และเป็นจุดเปลี่ยนในการพัฒนาทั้งการพัฒนาซอฟต์แวร์และอินเทอร์เน็ต

โดยพื้นฐานแล้ว รูปแบบ Model-View-Controller ช่วยให้สามารถทำสิ่งต่อไปนี้:ผู้ใช้ดำเนินการกับ View มุมมองทริกเกอร์คำขอไปยังคอนโทรลเลอร์ซึ่งสามารถสร้าง/อ่าน/อัปเดตหรือลบโมเดลได้ ธุรกรรมของโมเดลจะตอบกลับไปยังคอนโทรลเลอร์ ซึ่งจะทำให้การเปลี่ยนแปลงบางอย่างที่ผู้ใช้เห็นสะท้อนให้เห็นในมุมมอง

มีข้อดีมากมายสำหรับรูปแบบการเขียนโปรแกรมนี้ ในการลงรายการ:

  • ปรับปรุงความสามารถในการบำรุงรักษาโค้ดโดยแยกข้อกังวลออก
  • ช่วยให้ทดสอบได้มากขึ้น (สามารถทดสอบโมเดล มุมมอง และตัวควบคุมแยกกันได้)
  • สนับสนุนแนวทางการเขียนโค้ดที่ดีโดยบังคับใช้ Single Responsibility Principle ของ SOLID:"คลาสควรมีเหตุผลเพียงข้อเดียวในการเปลี่ยนแปลง"

ความสำเร็จที่ยิ่งใหญ่ในช่วงเวลานั้น นักพัฒนาซอฟต์แวร์ตระหนักในไม่ช้าว่ารูปแบบ MVC ก็ค่อนข้างจำกัดเช่นกัน รูปแบบต่างๆ เริ่มปรากฏขึ้น เช่น HMVC (แบบจำลองลำดับชั้น–มุมมอง–ตัวควบคุม), MVA (แบบจำลอง–มุมมอง–อะแดปเตอร์), MVP (แบบจำลอง–มุมมอง–ผู้นำเสนอ), MVVM (แบบจำลอง–มุมมอง–มุมมองแบบจำลอง) และอื่นๆ ซึ่งล้วนแต่พยายาม แก้ไขข้อจำกัดของรูปแบบ MVC

ปัญหาอย่างหนึ่งที่รูปแบบ MVC นำเสนอและหัวข้อของบทความในปัจจุบันมีดังต่อไปนี้:ใครเป็นผู้รับผิดชอบในการจัดการตรรกะของมุมมองที่ซับซ้อน มุมมองควรเกี่ยวข้องกับการนำเสนอข้อมูลเพียงอย่างเดียว ผู้ควบคุมเพียงถ่ายทอดข้อความที่ได้รับจากโมเดล และโมเดลไม่ควรเกี่ยวข้องกับตรรกะของมุมมองใดๆ

เพื่อช่วยแก้ปัญหานี้ แอปพลิเคชัน 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

ตัวอย่างนี้เรียบง่าย แต่แสดงให้เห็นข้อเท็จจริงว่าคุณต้องการแยกการตัดสินใจประเภทนี้ออกจากเทมเพลตเอง เพื่อลดความซับซ้อน

ตัวช่วยนั้นดี แต่มีรูปแบบอื่นสำหรับการจัดการตรรกะการดูที่ซับซ้อนซึ่งเป็นที่ยอมรับตลอดหลายปีที่ผ่านมา และนั่นคือรูปแบบ Facade

รู้เบื้องต้นเกี่ยวกับรูปแบบซุ้ม

ในแอปพลิเคชัน Ruby on Rails หน้าอาคารมักจะอยู่ใน app/facades ไดเรกทอรี

ในขณะที่คล้ายกับ helper , facades ไม่ใช่กลุ่มของเมธอดภายในโมดูล Facade คือ PORO (Plain Old Ruby Object) ที่สร้างอินสแตนซ์ภายในตัวควบคุม แต่เป็นส่วนที่จัดการ View ตรรกะทางธุรกิจที่ซับซ้อน ด้วยเหตุนี้จึงทำให้เกิดประโยชน์ดังต่อไปนี้:

  1. แทนที่จะมีโมดูลเดียวสำหรับ UsersHelper หรือ ArticlesHelper หรือ BooksHelper การดำเนินการควบคุมแต่ละรายการสามารถมี Facade ของตัวเองได้:Users::IndexFacade , Articles::ShowFacade , Books::EditFacade .
  2. มากกว่าโมดูล Facade สนับสนุนแนวทางการเขียนโค้ดที่ดีโดยอนุญาตให้คุณซ้อน Facade เพื่อให้แน่ใจว่ามีการบังคับใช้หลักการความรับผิดชอบเดียว แม้ว่าคุณอาจไม่ต้องการอาคารที่ซ้อนกันลึกหลายร้อยระดับ แต่การซ้อนชั้นหนึ่งหรือสองชั้นเพื่อการบำรุงรักษาที่ดีขึ้นและความครอบคลุมในการทดสอบอาจเป็นสิ่งที่ดี

นี่คือตัวอย่างที่ประดิษฐ์ขึ้น:

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

เมื่อไม่ควรใช้รูปแบบ Facade

ลองใช้เวลาสักครู่เพื่อไตร่ตรองดูว่าส่วนหน้าไม่ใช่ส่วนใด

  • ไม่ควรวาง Facades ในคลาสที่ใช้งานได้ เช่น ใน lib ไดเร็กทอรีสำหรับรหัสที่ต้องแสดงในมุมมอง วงจรชีวิตของซุ้มควรสร้างขึ้นในการดำเนินการควบคุมและใช้ในมุมมองที่เกี่ยวข้อง

  • Facades ไม่ได้มีไว้สำหรับใช้สำหรับตรรกะทางธุรกิจเพื่อดำเนินการ CRUD (มีรูปแบบอื่นๆ สำหรับสิ่งนั้น เช่น บริการหรือผู้โต้ตอบ—แต่นั่นเป็นหัวข้อสำหรับวันอื่น) กล่าวอีกนัยหนึ่ง Facades ไม่ควรเกี่ยวข้องกับการสร้าง การอัปเดตหรือการลบ เป้าหมายของพวกเขาคือการดึงตรรกะการนำเสนอที่ซับซ้อนออกจากมุมมองหรือตัวควบคุม และนำเสนออินเทอร์เฟซเดียวเพื่อเข้าถึงข้อมูลทั้งหมดนั้น

  • สุดท้ายแต่ไม่ท้ายสุด Facades ไม่ใช่กระสุนเงิน พวกเขาไม่อนุญาตให้คุณข้ามรูปแบบ MVC แต่เล่นไปพร้อมกับมัน หากการเปลี่ยนแปลงเกิดขึ้นในแบบจำลอง การเปลี่ยนแปลงนั้นจะไม่ปรากฏในมุมมองทันที เช่นเดียวกับในกรณีของ MVC การดำเนินการควบคุมจะต้องแสดงผลใหม่เพื่อให้ Facade แสดงการเปลี่ยนแปลงในมุมมอง

ประโยชน์ของคอนโทรลเลอร์

ประโยชน์หลักประการหนึ่งที่ชัดเจนของ Facades คือจะช่วยให้คุณลดตรรกะของตัวควบคุมลงได้อย่างมาก

รหัสคอนโทรลเลอร์ของคุณจะลดลงจากสิ่งนี้:

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

ดูประโยชน์ที่ได้รับ

สำหรับ Views มีประโยชน์หลักสองประการเมื่อใช้ Facades:

  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

ส่วนหน้าของการทดสอบหน่วยช่วยปรับปรุงประสิทธิภาพของชุดทดสอบได้อย่างมาก และในที่สุดบริษัทขนาดใหญ่ทุกแห่งก็จะพบกับชุดทดสอบที่ช้า เว้นแต่ปัญหาเช่นนี้จะไม่ได้รับการแก้ไขด้วยความจริงจังระดับหนึ่ง

หนึ่งซุ้ม สองซุ้ม สามซุ้ม เพิ่มเติมหรือไม่

คุณอาจพบสถานการณ์สมมติที่มุมมองแสดงผลบางส่วนที่ส่งออกข้อมูลบางส่วน ในกรณีนั้น คุณมีตัวเลือกในการใช้ส่วนหน้าหลักหรือใช้ส่วนหน้าแบบซ้อน ส่วนใหญ่ขึ้นอยู่กับตรรกะที่เกี่ยวข้องว่าคุณต้องการทดสอบแยกกันหรือไม่และเหมาะสมหรือไม่ที่จะแยกการทำงาน

ไม่มีกฎทองสำหรับจำนวนอาคารที่จะใช้หรือจำนวนอาคารที่จะซ้อนกันภายในกันและกัน นั่นคือดุลยพินิจของนักพัฒนา โดยทั่วไป ฉันชอบที่จะมีหน้าเดียวสำหรับการทำงานของตัวควบคุม และจำกัดการซ้อนไว้ที่ระดับเดียวเพื่อให้โค้ดติดตามได้ง่ายขึ้น

ต่อไปนี้เป็นคำถามทั่วไปที่คุณสามารถถามตัวเองได้ในระหว่างการพัฒนา:

  • ด้านหน้าอาคารครอบคลุมตรรกะที่ฉันพยายามนำเสนอในมุมมองหรือไม่
  • วิธีการภายในส่วนหน้ามีความหมายในบริบทนี้หรือไม่
  • ตอนนี้ติดตามโค้ดได้ง่ายขึ้นหรือติดตามยากขึ้นไหม

หากมีข้อสงสัย พยายามทำให้โค้ดของคุณง่ายต่อการติดตามมากที่สุด

บทสรุป

โดยสรุป Facades เป็นรูปแบบที่ยอดเยี่ยมในการทำให้คอนโทรลเลอร์และมุมมองของคุณมีความคล่องตัว ขณะที่ปรับปรุงความสามารถในการบำรุงรักษาโค้ด ประสิทธิภาพ และความสามารถในการทดสอบ

อย่างไรก็ตาม เช่นเดียวกับกระบวนทัศน์การเขียนโปรแกรมใดๆ ไม่มีสัญลักษณ์แสดงหัวข้อย่อยสีเงิน แม้แต่รูปแบบมากมายที่เกิดขึ้นในช่วงไม่กี่ปีที่ผ่านมา (HMVC, MVVM เป็นต้น) ก็ไม่ใช่วิธีแก้ปัญหาที่ยุ่งยากซับซ้อนของการพัฒนาซอฟต์แวร์

คล้ายกับกฎข้อที่สองของเทอร์โมไดนามิกส์ ซึ่งระบุว่าสถานะของเอนโทรปีในระบบปิดจะเพิ่มขึ้นเสมอ ดังนั้นในโครงการซอฟต์แวร์ใดๆ ก็ตาม ความซับซ้อนก็เพิ่มขึ้นและวิวัฒนาการไปตามกาลเวลาเช่นกัน ในระยะยาว เป้าหมายคือการเขียนโค้ดที่อ่าน ทดสอบ บำรุงรักษา และปฏิบัติตามได้ง่ายที่สุด เบื้องหน้านำเสนอสิ่งนี้

ป.ล. หากคุณต้องการอ่านโพสต์ Ruby Magic ทันทีที่ออกจากสื่อ สมัครรับจดหมายข่าว Ruby Magic ของเราและไม่พลาดแม้แต่โพสต์เดียว!