Dekoratory i presentery dla czystych widoków
Logika widoku nie ma w Rails dobrego miejsca — przecieka do modeli i zaśmieca szablony. Deep-dive o dekoratorach i presenterach: różnica, kiedy używać którego, czyste PORO vs Draper i jak trzymać prezentację z dala od domeny.
Każda aplikacja Rails kumuluje logikę prezentacji — kod decydujący, jak model jest
pokazywany: sformatowana cena, klasa CSS dla statusu, „dodano 3 dni temu”, pełne imię
złożone z części. Rails nie daje temu kodowi oczywistego domu, więc przecieka w dwa złe
miejsca. Idzie do modelu (def display_name; "#{first} #{last}".strip; end), puchnąc
obiekt domeny troską o to, jak wygląda na stronie. Albo idzie do widoku jako plątanina
if/else i inline’owego formatowania, niemożliwa do przetestowania i zduplikowana między
szablonami. Dekoratory i presentery dają tej logice porządny dom. Oto jak ich używać.
Problem, konkretnie
Rozważ status produktu pokazany z etykietą i kolorem. Bez domu dla prezentacji widok wypełnia się logiką:
<% if product.stock > 10 %>
<span class="badge badge--green">W magazynie</span>
<% elsif product.stock > 0 %>
<span class="badge badge--amber">Mało (zostało <%= product.stock %>)</span>
<% else %>
<span class="badge badge--red">Brak w magazynie</span>
<% end %>
To logika prezentacji — reguły, jak produkt wygląda — siedząca w szablonie, gdzie nie da się
jej unit-testować i jest kopiowana na każdą stronę pokazującą stan. Instynkt naprawienia tego
przez przeniesienie stock_label na model Product jest też błędny: model wie teraz o
plakietkach i CSS, mieszając logikę domeny z prezentacją. Ani model, ani widok nie jest
właściwym miejscem. Obiekt prezentacji jest.
Dekorator vs presenter: rozróżnienie
Oba terminy są używane luźno, ale użytecznym rozróżnieniem jest to:
- Dekorator opakowuje pojedynczą instancję modelu i dodaje do niej metody prezentacji.
Kwacze jak model —
product.namewciąż działa — ale oferuje teżproduct.stock_label. To model „ubrany” dla widoku. - Presenter to obiekt zbudowany dla konkretnego widoku lub ekranu, często łączący kilka modeli i dane, których potrzebuje dana strona. To mniej „udekorowany rekord”, a bardziej „view-model dla tej strony”.
W praktyce dekorator obsługuje „jak wyświetlić ten rekord”, a presenter obsługuje „czego potrzebuje ta strona”. Oba trzymają prezentację z dala od modelu i szablonu; po który sięgasz, zależy od tego, czy twoja logika dotyczy jednego rekordu, czy jednego ekranu.
Dekorator w czystym Ruby
Nie potrzebujesz gemu, by zacząć. Dekorator może być zwykłym obiektem opakowującym model:
class ProductDecorator < SimpleDelegator
def stock_label
case __getobj__.stock
when 11..Float::INFINITY then badge("W magazynie", :green)
when 1..10 then badge("Mało (zostało #{stock})", :amber)
else badge("Brak w magazynie", :red)
end
end
private
def badge(text, colour)
%(<span class="badge badge--#{colour}">#{text}</span>).html_safe
end
end
SimpleDelegator przekazuje wszystko do opakowanego obiektu, więc dekorator jest produktem
dla wszystkich innych celów — decorated.name, decorated.price działają — dodając
stock_label. Kontroler opakowuje rekord, a widok pozostaje czysty:
@product = ProductDecorator.new(Product.find(params[:id]))
<%= @product.stock_label %>
Teraz logika plakietki to metoda, którą możesz unit-testować wprost — zainstancjuj dekorator z wartością stanu i sprawdź wyjście — bez widoku, bez żądania. Ta testowalność to cały sens: logika prezentacji zasługuje na testy jak każdy inny kod, a w szablonie nie dostaje żadnych.
Draper: konwencjonalny gem
Dla spójnego, gotowego podejścia Draper to ustalony gem Rails. Daje dekoratorom konwencję
(ProductDecorator < Draper::Decorator), helper decorate, automatyczne dekorowanie kolekcji
i — co użyteczne — dostęp do helperów widoku i tras wewnątrz dekoratora (h.number_to_currency,
h.product_path), którego czyste PORO nie dostaje za darmo:
class ProductDecorator < Draper::Decorator
delegate_all
def price
h.number_to_currency(object.price) # helpery widoku dostępne przez `h`
end
def stock_label
h.content_tag(:span, status_text, class: "badge badge--#{status_colour}")
end
end
@product = Product.find(params[:id]).decorate
Dostęp Drapera do helperów i tras to jego główna przewaga nad ręcznie klepanym
SimpleDelegator — budowanie HTML i linków to to, co metody prezentacji robią nieustannie, a
posiadanie dostępnego h trzyma je czystymi. Wymianą jest zależność i odrobina magii; dla
małej aplikacji czyste PORO często wystarcza, podczas gdy większa korzysta ze spójności
Drapera.
Dekoratory vs helpery
Rails ma już helpery do logiki widoku, więc kiedy używać dekoratora zamiast nich?
Rozróżnienie, które nam służyło: helpery są dobre do generycznych, bezstanowych funkcji
niezwiązanych z konkretnym obiektem — format_date, icon_for, generyczny builder przycisków.
Dekoratory są dobre do prezentacji należącej do konkretnego modelu — product.stock_label,
user.display_name. Zapachem, który pcha cię od helperów do dekoratorów, jest helper biorący
model i grzebiący w jego atrybutach (def stock_label(product); product.stock > 10 ? ... ; end)
— to naprawdę metoda na prezentacji produktu i czyta się znacznie lepiej jako
product.stock_label. Helpery będące naprawdę metodami obiektu w przebraniu to dokładnie to, co
dekoratory sprzątają.
Nie przebudowuj
Zwykłe zastrzeżenie. Nie każdy model potrzebuje dekoratora, a dekorator z jedną trywialną metodą to ceremonia. Sięgaj po niego, gdy model kumuluje kilka trosk prezentacji, albo gdy ta sama logika wyświetlania pojawia się w więcej niż jednym miejscu, albo gdy logika widoku urosła na tyle, by zasługiwać na testy. Dla jednorazowego kawałka formatowania na pojedynczej stronie helper — albo nawet odrobina logiki widoku — jest w porządku. Celem jest dać prezentacji testowalny dom, gdy na niego zasłuży, a nie opakowywać wszystkiego odruchowo.
Werdykt
Logika prezentacji to prawdziwy kod, który zasługuje na prawdziwy dom, a w Rails tym domem jest
dekorator lub presenter — nie model (który nie powinien wiedzieć o CSS) i nie szablon (którego
nie da się przetestować). Użyj dekoratora, by ubrać pojedynczy rekord metodami wyświetlania,
presentera, by złożyć to, czego potrzebuje cały ekran; zacznij od czystego SimpleDelegator i
awansuj do Drapera, gdy chcesz dostępu do helperów i konwencji; i wolej dekoratory od helperów,
gdy logika wyraźnie należy do konkretnego obiektu. Stosuj to tam, gdzie prezentacja urosła na
tyle, by zasłużyć na testy, a twoje modele zostają skupione na domenie, twoje widoki zostają
czytelne, a reguły, jak rzeczy wyglądają, stają się kodem, który faktycznie możesz zweryfikować.