Rails Helper oder Presenter? Wann View-Models besser sind
Öffne in einer gewachsenen Rails-Anwendung mal app/helpers/application_helper.rb. Dort stehen oft Datumsformate, Status-Badges, Menüeinträge und Preisanzeigen im selben Modul. Ein Rails Helper soll kleine Ausgaben in der Ansicht vereinfachen. Sucht er aber Daten aus drei Modellen zusammen, wird die Datei zur Ablage für alles, was sonst keinen Platz hat. Wann reicht die Hilfsfunktion, wann ist ein Presenter die bessere Wahl, und wo kommen View-Komponenten ins Spiel?
Was ein Rails Helper ist und was er gut kann
Es sind schlichte Ruby-Module unter app/helpers. Ruby on Rails bindet sie in den View-Kontext ein, deshalb rufst du sie in jedem Template direkt auf. Dazu kommen die eingebauten Funktionen aus Action View: link_to, number_to_currency, time_ago_in_words, tag.span und einige Dutzend mehr.
Für eine Funktion mit einem Argument und einem String als Ergebnis passt das gut. Ein Beispiel:
module ApplicationHelper
def short_date(time)
l(time.to_date, format: :short)
end
end
Kein Zustand, keine Abhängigkeit, ein klarer Zweck. Solche Formatierer schreibe ich auch 2026 noch genau so, für diesen Einsatz ist das Bordmittel einfach und gut genug.
Wo Helper an Grenzen stoßen
Der Haken liegt im Geltungsbereich. In der Konfiguration steht config.action_controller.include_all_helpers standardmäßig auf true. Damit landet jede Funktion aus jedem Modul unter app/helpers in jeder View der Anwendung. Daraus folgen drei Probleme:
- Globaler Namensraum: Definieren
OrdersHelperundInvoicesHelperbeidestatus_badge, gewinnt das zuletzt eingebundene Modul. Der Fehler zeigt sich dann nur auf einzelnen Seiten. - Versteckte Abhängigkeiten: Wer auf
@orderodercurrent_userzugreift, braucht Daten, die in der Signatur gar nicht auftauchen. Verwendet ein anderes Template die Funktion, merken Entwickler das erst zur Laufzeit auf dem Server. - Umständliche Prüfung: Du brauchst einen View-Kontext, etwa über
ActionView::TestCase. Hängt alles zusätzlich an Instanzvariablen, baust du für jeden Testfall den halben Controller nach.
Mit include_all_helpers = false bekommt jeder Controller nur sein Modul plus die geerbten. Das verbessert die Lage bei Namenskollisionen. Die unsichtbaren Annahmen über Instanzvariablen bleiben bestehen.
Im klassischen MVC ist die View für die Darstellung zuständig, das Modell für Daten und Regeln. Ein globales Modul voller Formatierer passt in keine der beiden Rollen. Deshalb wächst es in größeren Projekten oft ohne Plan.
Presenter, Decorator, View-Model: ein Entwurfsmuster, drei Namen
View-Model ist englisch und heißt so viel wie Ansichtsmodell. Alle drei Begriffe meinen eine Schicht zwischen Model und Ansicht. Sie nimmt ein Objekt entgegen und bereitet seine Inhalte für die Darstellung auf, die Geschäftslogik bleibt im Modell. Im Umfeld von Model View Controller (MVC) heißt dieses Entwurfsmuster Presentation Model, so hat es Martin Fowler beschrieben. Verwandte Entwurfsmuster sind Model View Presenter (MVP) und Model View ViewModel (MVVM) aus der Desktop-Welt, beide aus dem MVC-Umfeld hervorgegangen. Das Decorator-Entwurfsmuster stammt aus dem Buch der „Gang of Four“ um Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides von 1994, erschienen bei Addison-Wesley.
In Ruby on Rails begegnen dir folgende drei Varianten:
- PORO-Presenter: ein einfaches Ruby-Objekt („Plain Old Ruby Object“), das den Datensatz und den View-Kontext bekommt. Kein zusätzliches Gem, volle Kontrolle.
- SimpleDelegator: stammt aus der Standardbibliothek von Ruby. Er reicht alle unbekannten Aufrufe an das umhüllte Objekt weiter, du schreibst nur die Ergänzungen.
- Draper: ein Gem, das sich auf dieses Muster beschränkt.
delegate_allleitet weiter, überherreichst du die View-Funktionen, überobjectden Datensatz. Mitdecoratelassen sich einzelne Objekte und ganze Relationen dekorieren.
Für Entwickler besteht der Gewinn in einer definierten Schnittstelle: Der Presenter bekommt, was er braucht, und gibt fertige Inhalte für die View zurück. Jede Methode lässt sich einzeln prüfen, und Änderungen am Layout bleiben im Template.
Decorator und Presenter unterscheiden sich in der Praxis wenig. Ein Decorator erweitert ein einzelnes Objekt und verhält sich nach außen wie das Original. Ein Presenter darf auch mehrere Modelle für eine Ansicht kombinieren, zum Beispiel Bestellung, Kunde und Versandstatus. Welches Entwurfsmuster im Team verwendet wird, ist zweitrangig, solange der Name im Projekt einheitlich ist.
Wann Draper? Sobald du Assoziationen automatisch mitdekorieren willst (decorates_association) oder ein Team bereits damit arbeitet. Für zwei, drei Presenter reicht SimpleDelegator, und du sparst dir eine Zeile im Gemfile. Eine Möglichkeit ist auch ein Mischbetrieb: Die meisten Modelle bekommen einen schlichten Presenter, nur die mit vielen Assoziationen einen Draper-Decorator.
Vergleich: Helper, Presenter und View-Komponente
| Kriterium | Helper | Presenter (PORO, SimpleDelegator, Draper) | View-Komponente (ViewComponent, Phlex) |
|---|---|---|---|
| Ort im Projekt | app/helpers |
app/presenters oder app/decorators |
app/components |
| Sichtbarkeit | alle Views (Standard) | nur wo du ihn erzeugst | nur wo du sie renderst |
| Bezug | lose Funktionen | ein Objekt oder eine Ansicht | ein Stück Markup plus Verhalten |
| Schnittstellen | oft implizit | im Konstruktor sichtbar | im Konstruktor sichtbar |
| Absicherung | mit View-Kontext | als normales Ruby-Objekt | Rendern und HTML prüfen |
| Extra-Gem nötig | nein | nein (Draper optional) | ja |
| Passt für | Formatierung, Kleinkram | Anzeige eines Modells | wiederverwendbare UI-Bausteine |
Meine Einschätzung als Best Practice: Für neue Projekte würde ich mit PORO-Presentern auf Basis von SimpleDelegator anfangen. Das kostet kein Gem und lässt sich später auf Draper oder Komponenten umstellen.
Wann sich View-Komponenten lohnen
Ein Presenter liefert Werte, das HTML steht weiter im Partial. Bei einer View-Komponente wandern Markup und Verhalten zusammen in einen Baustein. Bei ViewComponent, ursprünglich bei GitHub entstanden, erbt er von ViewComponent::Base und bekommt ein Template daneben. Phlex geht einen Schritt weiter und definiert auch das HTML in Ruby, ganz ohne ERB. Wie sich die beiden gegen das ältere Cells schlagen, steht im Beitrag über Rails-Komponenten mit Cells, ViewComponent und Phlex.
Der Vorteil zeigt sich bei der Prüfung: render_inline rendert die Komponente isoliert, und du kontrollierst das HTML, ohne einen Request durch den Controller zu schicken. Der Preis ist ein weiteres Gem und eine Konvention, die das Team lernen muss. Lohnend wird der Einsatz von Komponenten, wenn ein Baustein wie eine Statusanzeige oder eine Produktkarte in mehreren Views mit Verhalten auftaucht. Für eine einzelne Detailansicht ist das meist zu viel.
Beispiel: vom Helper zum Presenter umbauen
Ausgangslage ist ein typisches Modul für eine Bestellansicht, wie es in fast jeder älteren Rails-Anwendung bereits vorkommt:
# app/helpers/orders_helper.rb
module OrdersHelper
def order_status_badge(order)
css = case order.status
when "paid" then "badge badge--green"
when "shipped" then "badge badge--blue"
else "badge badge--grey"
end
tag.span(t("orders.status.#{order.status}"), class: css)
end
def order_total(order)
number_to_currency(order.total_cents / 100.0, unit: "€")
end
end
Beide Funktionen bekommen die Bestellung als Argument, gehören also erkennbar zu diesem einen Objekt. Ein Presenter macht das sichtbar:
# app/presenters/order_presenter.rb
class OrderPresenter < SimpleDelegator
STATUS_CSS = {
"paid" => "badge badge--green",
"shipped" => "badge badge--blue"
}.freeze
def initialize(order, view)
super(order)
@view = view
end
def status_css = STATUS_CSS.fetch(status, "badge badge--grey")
def status_badge
@view.tag.span(I18n.t("orders.status.#{status}"), class: status_css)
end
def total
@view.number_to_currency(total_cents / 100.0, unit: "€")
end
end
Damit du den View-Kontext nicht in jedem Template von Hand übergibst, bleibt genau eine kleine Funktion in ApplicationHelper übrig:
# app/helpers/application_helper.rb
module ApplicationHelper
def present(record, klass = "#{record.class.name}Presenter".constantize)
klass.new(record, self)
end
end
<% order = present(@order) %>
<h1><%= order.number %></h1>
<%= order.status_badge %>
<p>Summe: <%= order.total %></p>
Die Prüfung kommt ohne Controller und ohne Rendern aus:
# test/presenters/order_presenter_test.rb
require "test_helper"
class OrderPresenterTest < ActiveSupport::TestCase
test "bezahlte Bestellung bekommt die grüne CSS-Klasse" do
order = Order.new(status: "paid")
assert_equal "badge badge--green", OrderPresenter.new(order, nil).status_css
end
end
Die Funktionalität ist dieselbe wie vorher. Der Unterschied: Was der Presenter braucht, steht im Konstruktor. Die einzeilige Schreibweise def status_css = ... gibt es seit Ruby 3.0, die Implementierung läuft also unter Ruby 3.3 mit Rails 7.2 oder 8 ohne Anpassung.
Caching von Views: wer ist zuständig?
Formatierung im Presenter macht das Caching nicht überflüssig. Lokal schaltest du es mit bin/rails dev:cache ein, in Produktion regelt die Konfiguration config.action_controller.perform_caching den Schalter. Fragment-Caching gehört ins Template, denn nur dort weiß Rails, welcher Teil der Ausgabe gespeichert wird:
<% cache @order do %>
<%= render partial: "orders/line_item", collection: @order.line_items, cached: true %>
<% end %>
class LineItem < ApplicationRecord
belongs_to :order, touch: true
end
Das ist Russian-Doll-Caching: Die Positionen liegen als Fragmente im äußeren Fragment der Bestellung. Ändert sich eine Position, setzt touch: true das updated_at der Bestellung neu, und der äußere Cache-Key verfällt. Mit cached: true liest Rails die Einträge der Collection in einem einzigen Multi-Fetch. Webanwendungen, die ab Rails 8.0 erzeugt werden, bringen dafür Solid Cache als Standard-Store mit, der die Fragmente in der Datenbank ablegt.
Eine Falle betrifft beide Ansätze gleich: Der Cache-Key enthält einen Digest des Templates, deine Ruby-Dateien fließen nicht ein. Der Rails-Guide zum Caching beschreibt, dass Änderungen an externen Hilfsfunktionen den Digest nicht ändern. Passt du status_badge an, bleibt das alte HTML im Cache. Abhilfe schafft eine Version im Key, etwa cache [@order, "badge-v2"].
Die Zuständigkeiten sind damit verteilt:
- Modell: Geschäftslogik und
touch-Beziehungen. - Presenter: Darstellung einzelner Werte.
- Template: entscheidet, was gecacht wird und mit welchem Key.

Modell, Presenter und Template: Jede Schicht hat eine Aufgabe, das Caching sitzt im Template.
Controller schlank halten
Ein Controller lädt Daten, prüft Rechte der Benutzer und entscheidet über Antwort oder Umleitung. Darstellung hat dort nichts verloren. Wichtig sind klare Abläufe, wie ein häufiger Fehler zeigt. Ein Beispiel:
def show
@order = Order.find(params[:id])
redirect_to root_path unless @order.visible_to?(Current.user)
render :show
end
Greift die Umleitung, ruft die Action danach trotzdem render auf. Rails wirft dann AbstractController::DoubleRenderError, weil pro Request nur eine Antwort erlaubt ist. So sieht die saubere Version aus:
def show
@order = Order.find(params[:id])
return redirect_to(root_path) unless @order.visible_to?(Current.user)
end
Das explizite render :show fällt weg, Rails rendert das passende Template von selbst. Baut der Controller Strings für die Ansicht zusammen, gehört das in den Presenter. Der Controller kennt dann nur noch das Modell und die Rechte des Benutzers.
Checkliste: wann was?
- [ ] Eine Funktion, ein Argument, keine Daten aus anderen Objekten: Helper.
- [ ] Mehrere Funktionen drehen sich um dasselbe Modell: Presenter mit
SimpleDelegator. - [ ] Ganze Collections und Assoziationen sollen dekoriert werden: Draper prüfen.
- [ ] Dasselbe Markup mit Verhalten taucht in drei oder mehr Views auf: View-Komponente.
- [ ] Eine Funktion liest Instanzvariablen: umbauen, die Daten gehören in die Signatur.
- [ ] Gecachte Fragmente zeigen alte Ausgaben: Version im Cache-Key erhöhen.
- [ ] Der Controller formatiert Werte: in den Presenter verschieben.

Faustregel: Je mehr Logik und Markup zusammenkommen, desto mehr Struktur braucht die Ansicht.
Wer in einer bestehenden Rails-Anwendung mit dem Muster anfängt, nimmt sich zuerst das größte Modul unter app/helpers vor. Dort sucht man die Funktionen heraus, die alle dasselbe Objekt als Argument bekommen, und zieht sie in einen Presenter. Teil zwei folgt dann im Template: Fragmente mit Version im Key versehen. Weitere Beiträge zu Webentwicklung und Software findest du in der Rubrik Technik.




