Trong bài viết hôm nay, chúng ta sẽ xem xét một mẫu thiết kế phần mềm có tên là Facade. Khi lần đầu tiên tôi sử dụng nó, tôi cảm thấy hơi lúng túng, nhưng càng sử dụng nó trong các ứng dụng Rails của mình, tôi càng đánh giá cao tính hữu ích của nó. Quan trọng hơn, nó cho phép tôi kiểm tra mã kỹ lưỡng hơn, làm sạch bộ điều khiển của tôi, giảm logic trong các chế độ xem của tôi và giúp tôi suy nghĩ rõ ràng hơn về cấu trúc tổng thể của mã ứng dụng.
Là một mô hình phát triển phần mềm, mặt tiền là một khuôn khổ bất khả tri nhưng các ví dụ tôi sẽ cung cấp ở đây là dành cho Ruby on Rails. Tuy nhiên, tôi khuyến khích bạn đọc hết bài viết này và dùng thử chúng bất kể khuôn khổ bạn đang sử dụng là gì. Tôi chắc chắn rằng khi bạn đã quen với mẫu này, bạn sẽ bắt đầu thấy cơ hội sử dụng nó trong nhiều phần của cơ sở mã của mình.
Không cần thêm lời khuyên nào nữa, chúng ta hãy đi sâu vào ngay!
Sự cố với Mẫu MVC
Mẫu MVC (Model-View-Controller) là một mẫu phát triển phần mềm có từ những năm 1970. Đó là một giải pháp đã được thử nghiệm thực chiến để thiết kế giao diện phần mềm, tách các mối quan tâm về lập trình thành ba nhóm chính giao tiếp với nhau theo một cách riêng.
Nhiều khuôn khổ web lớn đã xuất hiện vào đầu những năm 2000 với mô hình MVC làm nền tảng của chúng. Spring (cho Java), Django (cho Python) và Ruby on Rails (cho Ruby), tất cả đều được rèn với bộ ba phần tử được kết nối với nhau ở cốt lõi của chúng. So với mã spaghetti được tạo ra từ phần mềm không sử dụng nó, mẫu MVC là một thành tựu to lớn và là bước ngoặt trong sự phát triển của cả phát triển phần mềm và internet.
Về bản chất, mẫu Model-View-Controller cho phép những điều sau:người dùng thực hiện một hành động trên View. Chế độ xem kích hoạt một yêu cầu tới Bộ điều khiển có khả năng tạo / đọc / cập nhật hoặc xóa một Mô hình. Giao dịch Mô hình phản hồi lại Bộ điều khiển, từ đó đưa ra một số thay đổi mà người dùng sẽ thấy được phản ánh trong Chế độ xem.
Có rất nhiều ưu điểm cho mô hình lập trình này. Để liệt kê một số:
- Nó cải thiện khả năng bảo trì mã bằng cách tách các mối quan tâm
- Nó cho phép khả năng kiểm tra cao hơn (Mô hình, Chế độ xem và Bộ điều khiển có thể được kiểm tra riêng lẻ)
- Nó khuyến khích các phương pháp mã hóa tốt bằng cách thực thi Nguyên tắc trách nhiệm duy nhất của SOLID:"Một lớp chỉ nên có một lý do để thay đổi."
Một thành tựu phi thường trong thời gian đó, các nhà phát triển sớm nhận ra rằng mô hình MVC cũng có phần hạn chế. Các biến thể bắt đầu xuất hiện, chẳng hạn như HMVC (mô hình phân cấp – view – controller), MVA (model – view – adapter), MVP (model – view – presenter), MVVM (model – view – viewmodel) và các biến thể khác, tất cả đều tìm cách giải quyết những hạn chế của mẫu MVC.
Một trong những vấn đề mà mẫu MVC giới thiệu, và chủ đề của bài viết hôm nay, là:ai chịu trách nhiệm xử lý logic chế độ xem phức tạp? Chế độ xem chỉ nên quan tâm đến việc trình bày dữ liệu, bộ điều khiển chỉ chuyển tiếp thông điệp mà nó nhận được từ mô hình và mô hình không nên quan tâm đến bất kỳ logic nào của chế độ xem.
Để giải quyết vấn đề hóc búa thường gặp này, tất cả các ứng dụng Rails đều được khởi tạo bằng helpers
danh mục. helper
thư mục có thể chứa các mô-đun với các phương thức hỗ trợ logic Chế độ xem phức tạp.
Đây là một ví dụ về trình trợ giúp trong ứng dụng 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
Ví dụ này đơn giản nhưng chứng minh thực tế là bạn muốn trích xuất loại ra quyết định này từ chính mẫu để giảm độ phức tạp của nó.
Người trợ giúp rất hay, nhưng có một mẫu khác để xử lý logic Chế độ xem phức tạp đã được chấp nhận trong nhiều năm và đó là mẫu Mặt tiền.
Giới thiệu về Mẫu mặt tiền
Trong ứng dụng Ruby on Rails, các mặt tiền thường được đặt trong app/facades
thư mục.
Trong khi tương tự với helpers
, facades
không phải là một nhóm các phương thức trong một mô-đun. Mặt tiền là một PORO (Đối tượng Ruby cũ thuần túy) được khởi tạo bên trong bộ điều khiển, nhưng một mặt xử lý logic nghiệp vụ Chế độ xem phức tạp. Như vậy, nó cho phép những lợi ích sau:
- Thay vì có một mô-đun duy nhất cho
UsersHelper
hoặcArticlesHelper
hoặcBooksHelper
, mỗi hành động của bộ điều khiển có thể có Mặt tiền riêng của nó:Users::IndexFacade
,Articles::ShowFacade
,Books::EditFacade
. - Hơn cả các mô-đun, các mặt tiền khuyến khích các phương pháp mã hóa tốt bằng cách cho phép bạn lồng các mặt tiền để đảm bảo Nguyên tắc trách nhiệm duy nhất được thực thi. Mặc dù bạn có thể không muốn các mặt tiền được lồng vào nhau sâu hàng trăm tầng, nhưng có một hoặc hai lớp lồng vào nhau để cải thiện khả năng bảo trì và phạm vi kiểm tra có thể là một điều tốt.
Đây là một ví dụ có sẵn:
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 nào không sử dụng mẫu mặt tiền
Hãy dành một chút thời gian để suy nghĩ về những gì không phải là mặt tiền.
-
Không nên đặt các mặt tiền trong các lớp đang tồn tại, chẳng hạn như trong
lib
thư mục cho mã cần được hiển thị trong Chế độ xem. Vòng đời của mặt tiền phải được tạo trong hành động Bộ điều khiển và được sử dụng trong Chế độ xem liên quan. -
Mặt tiền không được sử dụng cho logic nghiệp vụ để thực hiện các hành động CRUD (có các mẫu khác cho điều đó, chẳng hạn như Dịch vụ hoặc Người tương tác — nhưng đó là một chủ đề cho ngày khác.) Nói cách khác, mặt tiền không nên quan tâm đến việc tạo, cập nhật hoặc xóa. Mục đích của họ là trích xuất logic trình bày phức tạp từ Chế độ xem hoặc Bộ điều khiển và cung cấp một giao diện duy nhất để truy cập tất cả thông tin đó.
-
Cuối cùng nhưng không kém phần quan trọng, Mặt tiền không phải là một viên đạn bạc. Họ không cho phép bạn bỏ qua mô hình MVC, mà thay vào đó, họ chơi cùng với nó. Nếu thay đổi xảy ra trong Mô hình, nó sẽ không được phản ánh ngay lập tức trong Chế độ xem. Như mọi khi xảy ra với MVC, hành động của bộ điều khiển sẽ phải được hiển thị lại để Mặt tiền hiển thị các thay đổi trên Chế độ xem.
Lợi ích của Người kiểm soát
Một trong những lợi ích chính, rõ ràng của Mặt tiền là chúng sẽ cho phép bạn giảm đáng kể logic bộ điều khiển.
Mã bộ điều khiển của bạn sẽ bị giảm từ một cái gì đó như thế này:
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
Về điều này:
class BooksController < ApplicationController
def index
@index_facade = Books::IndexFacade.new(user: current_user, params: params)
end
end
Xem lợi ích
Đối với Chế độ xem, có hai lợi ích chính khi sử dụng Mặt tiền:
- Kiểm tra có điều kiện, truy vấn nội tuyến và logic khác có thể được trích xuất gọn gàng từ chính mẫu làm cho mã dễ đọc hơn nhiều. Ví dụ:bạn có thể sử dụng nó trong một biểu mẫu:
<%= 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?) %>
Chỉ có thể trở thành:
<%= f.label :location %>
<%= f.select :location, options_for_select(@form_facade.user_locations), multiple: @form_facade.multiple_locations? %>
- Các biến được gọi nhiều lần có thể được lưu vào bộ nhớ đệm. Điều này có thể mang lại những cải tiến đáng kể về hiệu suất cho ứng dụng của bạn và giúp loại bỏ các truy vấn N + 1 khó chịu:
// 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 %>
sẽ trở 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 %>
Lợi ích của Thử nghiệm
Lợi ích chính của Facades là chúng cho phép bạn kiểm tra các bit đơn lẻ của logic nghiệp vụ mà không cần phải viết toàn bộ kiểm tra bộ điều khiển hoặc tệ hơn là không cần phải viết kiểm tra tích hợp đi qua một luồng và đến một trang chỉ để đảm bảo rằng trình bày dữ liệu như mong đợi.
Vì bạn sẽ thử nghiệm các PORO đơn lẻ, điều này sẽ giúp duy trì một bộ thử nghiệm nhanh.
Dưới đây là một ví dụ đơn giản về bài kiểm tra được viết bằng Minitest cho mục đích trình diễn:
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
Mặt tiền kiểm thử đơn vị cải thiện đáng kể hiệu suất của bộ thử nghiệm và mọi công ty lớn cuối cùng sẽ gặp phải các bộ thử nghiệm chậm trừ khi các vấn đề như thế này không được giải quyết ở một mức độ nghiêm trọng nào đó.
Một mặt tiền, Hai mặt tiền, Ba mặt tiền, Hơn thế nữa?
Bạn có thể gặp trường hợp trong đó Chế độ xem hiển thị một phần xuất ra một số dữ liệu. Trong trường hợp đó, bạn có tùy chọn sử dụng mặt tiền chính hoặc sử dụng mặt tiền lồng nhau. Điều đó phần lớn phụ thuộc vào mức độ liên quan đến logic, liệu bạn có muốn kiểm tra nó một cách riêng biệt hay không và việc trích xuất chức năng có hợp lý hay không.
Không có quy tắc vàng cho việc sử dụng bao nhiêu mặt tiền hoặc bao nhiêu mặt tiền lồng vào nhau. Đó là quyết định của nhà phát triển. Nói chung, tôi thích có một mặt tiền duy nhất cho hành động của bộ điều khiển và tôi giới hạn việc lồng vào một cấp duy nhất để làm cho mã dễ theo dõi hơn.
Dưới đây là một số câu hỏi chung mà bạn có thể tự hỏi trong quá trình phát triển:
- Mặt tiền có bao hàm logic mà tôi đang cố gắng trình bày trên chế độ xem không?
- Phương pháp bên trong có hợp lý trong bối cảnh này không?
- Mã hiện tại dễ theo dõi hơn hay khó làm theo hơn?
Khi nghi ngờ, hãy luôn cố gắng làm cho mã của bạn dễ theo dõi nhất có thể.
Kết luận
Tóm lại, mặt tiền là một mô hình tuyệt vời để giữ cho bộ điều khiển và chế độ xem của bạn tinh gọn, đồng thời cải thiện khả năng bảo trì mã, hiệu suất và khả năng kiểm tra.
Tuy nhiên, giống như bất kỳ mô hình lập trình nào, không có viên đạn bạc. Ngay cả vô số các mẫu đã xuất hiện trong những năm gần đây (HMVC, MVVM, v.v.) cũng không phải là giải pháp cuối cùng cho những phức tạp của phát triển phần mềm.
Tương tự như định luật thứ hai của nhiệt động lực học, nói rằng trạng thái của entropi trong một hệ thống kín sẽ luôn tăng, vì vậy trong bất kỳ dự án phần mềm nào cũng vậy, độ phức tạp cũng tăng và phát triển theo thời gian. Về lâu dài, mục tiêu là viết mã dễ đọc, dễ kiểm tra, bảo trì và theo dõi nhất có thể; mặt tiền cung cấp chính xác điều này.
Tái bút. Nếu bạn muốn đọc các bài đăng của Ruby Magic ngay khi chúng xuất hiện trên báo chí, hãy đăng ký bản tin Ruby Magic của chúng tôi và không bao giờ bỏ lỡ một bài đăng nào!