Skip to content
← Wszystkie wpisy
4 min czytania Dawid Skłodowski

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.name wciąż 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ć.