ในโพสต์ของวันนี้ เราจะพิจารณารูปแบบการออกแบบซอฟต์แวร์ที่เรียกว่า 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 ตรรกะทางธุรกิจที่ซับซ้อน ด้วยเหตุนี้จึงทำให้เกิดประโยชน์ดังต่อไปนี้:
- แทนที่จะมีโมดูลเดียวสำหรับ
UsersHelper
หรือArticlesHelper
หรือBooksHelper
การดำเนินการควบคุมแต่ละรายการสามารถมี Facade ของตัวเองได้:Users::IndexFacade
,Articles::ShowFacade
,Books::EditFacade
. - มากกว่าโมดูล 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:
- สามารถแยกการตรวจสอบตามเงื่อนไข การสืบค้นข้อมูลในบรรทัด และตรรกะอื่นๆ ออกจากเทมเพลตได้อย่างเรียบร้อย ทำให้โค้ดอ่านง่ายขึ้นมาก ตัวอย่างเช่น คุณสามารถใช้ในรูปแบบ:
<%= 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? %>
- ตัวแปรที่ถูกเรียกใช้หลายครั้งสามารถแคชได้ สิ่งนี้สามารถปรับปรุงประสิทธิภาพที่สำคัญให้กับแอปของคุณและช่วยลบข้อความค้นหา 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 ของเราและไม่พลาดแม้แต่โพสต์เดียว!