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:
- Thay vì single module cho
UsersHelperhoặcArticlesHelperhoặcBooksHelper, mỗi controller action có own Facade:Users::IndexFacade,Articles::ShowFacade,Books::EditFacade. - 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ụ,
libdirectory 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:
- 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? %>
- 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ỡ!