Neuschreiben in Rails
(dirkjonker.bearblog.dev)- Eine Anwendung zum Erstellen von Zollanmeldungen wurde nach der Prüfung mehrerer Stacks zunächst mit dem vertrauten Ruby on Rails umgesetzt, weil sich damit Funktionen gefühlt deutlich schneller bauen ließen als mit neuen Alternativen
- Die mit einem Rust-Backend und einem SvelteKit-Frontend erstellte „version 2“ war schnell und sah gut aus, deckte aber nur rund 10 % der benötigten Funktionen ab, während Infrastrukturarbeit statt Business-Code immer größer wurde
- Eine schrittweise Migration hätte erfordert, Routing, Datenzugriff, Berechtigungen, Änderungsverfolgung, Suche, Background Jobs, pub/sub, E-Mail, OAuth 2.0, SMTP/POP, Objektspeicher und PDF-Erzeugung selbst zu verbinden, brachte den Nutzern aber kaum zusätzlichen Wert
- Man entschied sich, nur die bereits auf den neuen Stack migrierten Teile wieder nach Rails zurückzuschreiben; die Situation, eine Rails-App allein zu betreiben, der Stress durch unerfüllte Nutzeranforderungen und die Komplexität, Änderungen an vielen Stellen gleichzeitig vornehmen zu müssen, beeinflussten die Entscheidung
- Nach der Rückkehr zu Rails nahm das Tempo bei Feature-Entwicklung, Verbesserung bestehender Funktionen, UI-Updates und Bugfixes wieder zu; die Nachteile von Ruby/Rails werden mit Tests, Refactoring, Turbo und Caching gehandhabt, während der Fokus auf Produktentwicklung liegt
Der Ausgangspunkt: Rails als vorläufige Wahl
- Beim Start der Entwicklung einer Anwendung zum Erstellen von Zollanmeldungen im Jahr 2022 wurden verschiedene Sprachen und Frameworks wie .NET/F#, Go, Rust und React geprüft und auch tatsächlich Code damit geschrieben
- Die endgültige Wahl fiel auf Ruby on Rails
- Es war ein bereits vertrautes Werkzeug und fühlte sich etwa 10-mal schneller an als Optionen, die erst neu gelernt werden mussten
- Die damalige Rails-Implementierung war eher ein vorläufiger Proof of Concept, und der Plan war, sie später in einer anderen Sprache neu zu schreiben, die nicht so fehleranfällig ist wie Ruby
- Die Rails-Anwendung gewann schnell an Funktionen, und auch die Kollegen mochten das Ergebnis
- Produktivitätssteigernde Funktionen konnten schnell gebaut werden
- Fehlersituationen, die anfangs häufig auftraten, konnten im Code verhindert werden
Wo „version 2“ mit Rust und SvelteKit ins Stocken geriet
- Für „version 2“ wurden Rust im Backend und SvelteKit im Frontend gewählt
- Die frühe Version sah gut aus und lief schnell, hatte aber nur etwa 10 % der benötigten Funktionen
- Da es in Rust kein mit Rails vergleichbares integriertes Framework gibt, musste mehr Basis- und Verbindungs-Code geschrieben werden als Geschäftslogik
- Es wurde klar, dass für eine vollständige version 2 die Entwicklung von version 1 komplett gestoppt werden müsste; am Ende wurde version 2 verworfen
- Dabei zeigte sich, dass ein Rewrite ungefähr so lange dauern kann wie die ursprüngliche Erstellung der Anwendung
Operative Komplexität durch schrittweise Migration
- Wegen Rusts starkem Typsystem, hoher Performance und dem Vertrauen nach einem erfolgreichen Compile blieb der Wunsch bestehen, Rust in die Anwendung einzubringen
- Obwohl Ruby seit über vier Jahren genutzt wurde, traten weiterhin Runtime-Probleme auf
- Vor allem null-Werte und unbehandelte Exceptions waren problematisch
- Nach Deployments wurden Produktionsprobleme schnell behoben, indem Fehlermeldungen aus dem Error-Reporting-Tool beobachtet wurden
- Am Rails-Frontend störte, dass Logik und Markup über mehrere Dateien verstreut waren; Svelte machte es einfacher, zusammengehörige Logik in einer Datei zu bündeln
- Der Plan war, neue Frontends nach und nach mit SvelteKit und neue Backends mit Rust zu schreiben, bestehende Rails-Teile schrittweise zu ersetzen und Rails am Ende vollständig zu entfernen
- Anfangs ging die Migration von „index“-Seiten mit Datensatzlisten und Filtern auf den neuen Stack schnell voran
- Der neue Stack lief schnell, und die Rust-Teile hatten kaum Runtime-Probleme
- Die Deployment-Regeln dafür, welche Route zu Rails, Rust oder SvelteKit gehen sollte, wurden komplex
- Diese Komplexität wurde als Übergangszustand bis zur vollständigen Umstellung akzeptiert
Technische Arbeit wurde größer als der Nutzwert
- Mit der Zeit kamen auf der Rails-Seite weiterhin nützliche neue Funktionen hinzu, und bestehende Funktionen wurden tiefer und ausgefeilter
- Um falsche Anfangsannahmen zu korrigieren, wurden mehrere Refactorings vorgenommen; ein gut entworfenes Datenbankschema wurde als hilfreich für jede Version eingeschätzt
- Für die Rails-UI sollte nicht mehr viel Zeit aufgewendet werden, da sie bald ersetzt werden sollte
- Einige Monate später war das Geschäft erfolgreich genug, um festangestellte Entwickler einstellen zu können, und es wurde erwartet, dass die Rust-Migration dadurch schneller würde
- Tatsächlich wurde im neuen Stack viel Zeit mit dem Schreiben von Abstraktionen und Mechanismen verbracht
- Datenbankzugriff
- Berechtigungslogik, damit Nutzer nur erlaubte Daten sehen
- Nachverfolgung aller Änderungen
- Suche und Filter
- Durchsetzung von Constraints über das Typsystem
- Danach wurde auch die Liste der benötigten Funktionen immer länger
- Background Jobs
- Ein pub/sub-System für Client-Updates
- E-Mail-Funktionen
- OAuth 2.0 zur Anbindung externer Dienste
- SMTP und POP
- Objektspeicher
- PDF-Erzeugung
- Es gab genügend Rust-Bibliotheken, aber die passende Bibliothek auszuwählen und alles miteinander zu verbinden war ein großer Aufwand
- Diskussionen über technische Implementierungsdetails wie die Wahl eines Rust-ORMs sowie den Veröffentlichungszeitpunkt und die Verbesserungen von Svelte 5 nahmen zu
- Nach etwa einem Jahr Rust- und Svelte-Entwicklung wurde deutlich, dass die Nutzer daraus kaum Nutzen zogen
- Einige Ansichten im neuen Stack waren flüssig und schnell, betrafen aber nicht die ursprünglich problematischen Bereiche
- Die am häufigsten genutzten Seiten liefen weiterhin in Rails und hatten zehnmal mehr Funktionen als die „Betaversion“ des neuen Stacks
- Tage oder Wochen wurden mit Arbeiten verbracht, die keinen direkten Wert hinzufügten
Die Entscheidung, zu Rails zurückzukehren
- Es wurde beschlossen, nur die Teile, die bereits im neuen Stack liefen, wieder in Rails neu zu schreiben
- Es ging nicht darum, die gesamte Anwendung neu zu schreiben, sondern die nach Rust/SvelteKit migrierten Teile zurück nach Rails zu holen
- Die Entscheidung war schwierig, aber vernünftig
- Es waren bereits viel Zeit und viel Code investiert worden
- Es war möglich, dass Teammitglieder Ruby oder Rails nicht mögen würden
- Zum Glück verstand das Team die Entscheidung und war offen dafür, Neues zu lernen
- Die Stärken von Rails liegen darin, dass viele Funktionen standardmäßig enthalten sind, ein ausgereiftes und stabiles Bibliotheks-Ökosystem vorhanden ist und der Code dank Konventionen und einer stark meinungsbehafteten Architektur knapp bleibt
- Rails ermöglicht es, sich auf den Bau des Produkts zu konzentrieren, und reduziert die Belastung, unterstützenden Unterbau-Code selbst schreiben zu müssen
Erkenntnisse nach der Rückkehr zu Rails
- Der Versuch mit Rust und Svelte war keine vollständige Verschwendung
- Durch das Lernen neuer Sprachen und Frameworks entstanden Einsichten und Ideen, die auch in anderen Sprachen hilfreich sind
- Webanwendungsentwicklung ist eine ständige Folge von Entscheidungen und Kompromissen
- Man muss entweder die Schwächen eines Frameworks akzeptieren oder in eigene Implementierung investieren und abwägen, ob diese Investition Wettbewerbsvorteile und langfristigen Ertrag bringt
- Die Rückmigration nach Rails war zu großen Teilen unkompliziert und reduzierte auch den Stress
- In den letzten Wochen stellte sich bei der Entwicklung neuer Funktionen, der Verbesserung bestehender Funktionen, UI-Updates und Bugfixes wieder ein Gefühl von Produktivität ein
- Die Nachteile von Ruby und Rails werden als Bedingungen akzeptiert, die beim Schreiben von Code berücksichtigt werden müssen
- Wenn es viele Runtime-Probleme gibt, muss mehr getestet werden
- Schwer wartbarer Spaghetti-Code entsteht eher durch Entwickler als durch eine Sprache oder ein Framework
- Für Refactoring und Code-Organisation muss mehr Zeit aufgewendet werden
- Wenn Rails schwer zu skalieren ist, ist wahrscheinlich auch anderes schwer zu skalieren
- Rails ist schnell genug, und mit Turbo und Caching gibt es Ausweichmöglichkeiten
- Aus Nutzersicht war der Rust-Stack nicht spürbar schneller
- Die P95 duration der schwersten Seite in Rails beträgt 338 ms; es gibt Verbesserungsmöglichkeiten, aber sie fühlt sich ausreichend schnell an
- Es gibt keine perfekte Sprache und kein perfektes Framework; nötig ist ein Umgang, der Stärken und Schwächen kennt
- Rails ist eines der sehr guten Frameworks für Menschen, die sich darauf konzentrieren wollen, gute Produkte zu bauen; andere Frameworks, die lange gepflegt werden und viele aktive Beitragende haben, können ähnlich gute Optionen sein
2 Kommentare
Schreib es in Ruby neu
Schreib es in Rails neu
Meinungen auf Hacker News
Rails ist meiner Ansicht nach anderen Stacks zum Bau von Web-Apps weit voraus, auch im Vergleich zu Django oder Laravel.
Es hat die langweiligen Teile erledigt, kennt die nötigen Anforderungen und steht beim Bau echter Apps weniger im Weg. In 15 Jahren Web-App-Entwicklung habe ich Meteor, Remix, Next.js, Node.js auf Basis von Express usw. verwendet, aber Webentwicklung fühlt sich für mich inzwischen wie ein weitgehend gelöstes Problem an. Auf der JavaScript-Seite wirkt es, getrieben vom Geld großer Unternehmen, eher so, als würden Entwickler mit neuem Spielzeug spielen; Rust oder Go sind gut für Systementwicklung und Deployment-Infrastruktur, aber sie mit Gewalt in die Webentwicklung zu pressen, ergibt für mich keinen Sinn.
Es lässt sich leicht mit zahllosen Java-Bibliotheken integrieren, und als statisch typisierte Sprache hilft es in vielen Situationen. Schon das konventionsbasierte Data Binding über Jackson ist seit Langem hervorragend; bei dynamisch typisierten Sprachen wirkt es stark so, als seien solche Dinge später angeflanscht worden.
Wenn man aber tief in Rails, Django oder Laravel steckt und dann etwas tun muss, das von den Konventionen abweicht, kann es knifflig werden. Weil man den Glue Code nicht selbst geschrieben hat, entsteht oft eher eine Verständnislücke als eine technische Barriere. Wenn man die Interna aber richtig lernt, kann man allein durch das Lesen des Codes enorm viel Coding-Zeit sparen. Natürlich macht man das in der Praxis nicht, und weil es viel zu viel Spaß macht, alles neu zu schreiben, genießt man neue Anläufe, selbst wenn sie geschäftlich keinen Sinn ergeben.
Man muss nicht nur meiner Meinung glauben, man kann sich die Daten ansehen: https://www.youtube.com/watch?v=Qp9SOOtgmS4
Wenn man Rails für einen Dienst verwendet, bei dem man die Nutzerzahl oder Requests pro Sekunde nicht sicher kennt, spart man zwar Kosten beim Launch, kann aber am Ende mehr für Server ausgeben.
Die Rails-Demo war für mich nicht sonderlich überzeugend, außer dass sie gezeigt hat, wie wichtig Glück bei Adoption und Aufmerksamkeit ist. Auch heute erkenne ich den Wert nicht recht und würde Spring oder ASP.NET wählen. Die Gründer dieses Tcl-basierten Startups haben später OutSystems gegründet, eines der wenigen RAD-Tools, die in der Webentwicklung im Enterprise-Maßstab erfolgreich waren, und einer der Erfolgsgeschichten der portugiesischen IT-Branche.
Früher habe ich Rails bevorzugt, aber durch das starke Wachstum von Python in den letzten gut zehn Jahren ist Django die praktischere Wahl geworden. Entwickler aus Machine Learning und Data Science sind bereits mit Python vertraut, und die Django-Dokumentation ist so gut, dass man bei Bedarf in sehr kurzer Zeit produktiv werden kann. Das habe ich in mehreren Projekten selbst gesehen.
Django reduziert außerdem Entscheidungen und Debatten über die Anwendungsstruktur gegenüber weniger meinungsstarken Bibliotheken oder Microframeworks. Die Django-/FLOSS-Community ist ebenfalls viel aktiver als erwartet, und der Austausch war angenehm. Allerdings wünschte ich mir Generatoren wie bei Rails und integriertes Data Seeding, etwa Funktionen wie
rake db:seed.Ich nutze Rails seit 17 Jahren, mag es immer noch und bevorzuge es auch fürs Frontend.
Es gibt einem alle Möglichkeiten, Code zu trennen, ohne dass der Kopf jeder Datei zu einer unnötig langen Liste von
includeswird, wie man es in Angular-Apps sieht. Ich sehe keinen Nachteil darin, Logik und Markup im Frontend zu trennen. Man kann die Logik unabhängig testen, und wenn man Bugs sucht, weiß man normalerweise, ob es ein Markup- oder ein Logikproblem ist, was die Nachverfolgung erleichtert. Ich habe auch ziemlich viele Ruby-Methoden mit mehr als 200 Zeilen geschrieben, aber das war wirklich meine Schuld; ein feineres Unit-Test-System hätte das verhindern können.includes-Listen zu vermeiden, besteht darin, alles global zu machen.Ob das ein Vorteil ist, weiß ich nicht. Imports können zwar viel Platz einnehmen, zeigen aber explizit, wie alles miteinander verbunden ist. Wenn es stört, kann man sie in der IDE einklappen.
Mir gefällt der Artikel, weil er realistisch ist. Der zentrale Satz ist einer, den man sich immer ans Kopfende legen könnte.
„Interessanterweise stören mich die Dinge, die mich früher an Ruby und Rails genervt haben, heute viel weniger. Ich habe akzeptiert, dass es keine perfekte Sprache und kein perfektes Framework gibt. Man muss die Stärken und Schwächen kennen und damit umgehen.“
In vielen Fällen verstehen sie das Framework nicht ausreichend und wissen nicht, warum es so ist, wie es ist. Es ist aber viel effizienter, die Grenzen zu kennen und mit dem Framework zu arbeiten. Man sollte Zeit investieren, um die Werkzeuge, die man nutzt, wirklich zu verstehen.
„In der Zwischenzeit begann ich, eine ‚Version 2‘ der Anwendung zu bauen. Als Backend-Sprache wählte ich Rust, für das Frontend SvelteKit.“
Da v1 mit Rails begonnen hat, lese ich den Artikel als Warnung, vorsichtig zu sein, wenn man ein Framework aufgibt, das bereits gut funktioniert.
Rust ist eine Sprache, die von sehr klugen Leuten entwickelt wurde, aber der Fokus scheint eher auf Typentheorie und dem Einsparen einiger Allokationen zu liegen als darauf, dass Entwickler in der Praxis wie ich damit etwas Nützliches und Konsistentes bauen können.
Ich nutze Rust seit 2020, und trotzdem verstehe ich beim Lesen von Rust-Blogposts buchstäblich die Hälfte nicht.
Besonders im Vergleich zu großen Mainstream-Sprachen. Traits decken zum Beispiel die Anwendungsfälle von abstrakten Klassen und Concepts in C++ sowie abstrakten Klassen und Protokollen in Python ab. Es gibt auch keine Inkonsistenz zwischen Fehlerbehandlung per Exception und per Rückgabewert. Es gibt Panics, aber sie werden wie Assertions verwendet. Wenn
if-elseein Ausdruck ist, braucht man auch keinen separaten ternären Operator.In letzter Zeit gibt es den Trend, Rust für alles verwenden zu wollen, aber die Sprache wurde nicht für CRUD-Apps gemacht. Dieser Beitrag ist letztlich eher eine Fallstudie für: „Nimm das passende Werkzeug für die Aufgabe. Die Lieblingssprache aus der Freizeit oder die neueste Trend-Sprache ist nicht unbedingt die richtige.“
Wenn man sich einen Garbage Collector leisten kann, muss man nicht Rust oder C++ verwenden und sich um Speicherverwaltung kümmern. Es gibt Leute, die Rust als allgemeine Programmiersprache bewerben, und theoretisch stimmt das auch, aber C++ wurde in den 90ern genauso beworben. Speicherallokationen zu minimieren ist normalerweise genau die Eigenschaft, wegen der man Rust oder C++ auswählt.
Eine große Webanwendung schnell in Rust zu schreiben ist schwierig.
Eine große Anwendung in Rails läuft möglicherweise nicht schnell, kann viele Bugs haben oder schwer zu warten und zu erweitern sein. Optionen wie Java, C# oder Go wirken vielleicht nicht sexy oder cool, können aber Dinge ähnlich schnell erledigen wie Rails und dabei mit einer Geschwindigkeit laufen, die näher an Rust liegt. Meist entstehen dabei auch weniger Bugs, und die Projekte lassen sich leichter warten und erweitern.
Egal wie sehr man Rails nicht mag: Das ist ungefähr so, als würde man sagen, man könne mit C++ schneller einen Blog bauen als mit WordPress.
Wenn man sorgfältig baut, muss das keineswegs so sein. Ich habe viele große Rails-Apps gesehen, die kaum Bugs hatten, stabil waren und sich leicht ändern ließen, und ich habe auch chaotische Rails-Apps und Go-Apps gesehen. Manche Sprachen oder Frameworks können es leichter machen, in so einen Zustand zu geraten, aber möglich ist alles, und es gibt Trade-offs, die aus anderen Gründen schwierig werden. Mit Disziplin kann man alles zum Laufen bringen.
Bei ähnlich fähigen Entwicklern wird Rails für die meisten Webanwendungsfunktionen schneller entwickelt als Java, C# oder Go. Meiner Erfahrung nach war es bis zu dreimal schneller. Ich habe sowohl mit großen Monolithen als auch mit serviceorientierten Architekturen gearbeitet und auch mit Java-Teams zusammengearbeitet. Eine Zeit lang habe ich aus genau dieser Überzeugung heraus Go in eine Rails-Organisation eingeführt.
Am Ende war das eigentliche Problem, dass MVC nicht für alles passt und dass Namespaces nicht streng genug genutzt wurden. Wenn man diese Probleme in kleinen, gezielten Bereichen behebt, ist Rails völlig in Ordnung.
Daher denke ich, dass es mit ausreichend Pflege, guter Architektur und Wartung auch mit Rails möglich ist.
In letzter Zeit habe ich begonnen, AdonisJS als TypeScript-Alternative zu Rails zu testen.
Wenn euch „Rails in TypeScript“ interessiert, empfehle ich, es einmal auszuprobieren. Es fühlt sich deutlich näher an Rails oder Laravel an als die populärsten TypeScript-Stacks und ist batteries included, statt einen jede Schicht des Stacks selbst auswählen oder auf Drittanbieter-Templates vertrauen zu lassen.
In letzter Zeit ziehen mich batteries-included Backend-Frameworks wie Rails, Laravel und Django an, aber da ich fast meine ganze Karriere lang TypeScript genutzt habe, wäre es einfacher, dabei zu bleiben, statt jetzt zu wechseln. Mich würden deine Eindrücke vom Testen interessieren und ob du es mit den anderen erwähnten Frameworks vergleichen kannst.
Für ein aktuelles Nebenprojekt habe ich NestJS gewählt, dann aber bald Adonis entdeckt, und jetzt überlege ich, tatsächlich umzuziehen.
Technologien wie Next.js und SvelteKit sind unter JavaScript-Entwicklern beliebt, weil sie die schlechte Backend-Entwicklungserfahrung von Node.js abmildern.
Ein Backend mit TypeScript zu bauen fühlt sich ähnlich an wie mit C#, nur ohne die Vorteile einer ordentlich entworfenen Sprache. Das ist ein großer Unterschied zur Erfahrung, ein Backend mit Rails zu bauen. Node.js-Entwickler beginnen ebenfalls zu erkennen, dass es nicht nur um Ökosystem und Tools geht, sondern dass Frameworks wichtig sind, die eine ganzheitliche Lösung zum Bau von Anwendungen bieten. Genau an dieser Stelle helfen Next.js und SvelteKit, und genau dort war Rails 2006 innovativ.
Wird hier, nur weil man Code im Backend ausführen kann, schon mit Rails verglichen? Wo sind Datenbankzugriff, Migrationen, ORM, Queues, geplante Jobs, Validierung, Übersetzung, Authentifizierung und Autorisierungsunterstützung? Das Einzige, wobei Next.js hilft, ist, Leute auf die Vercel-Plattform zu schieben.
Im Frontend bevorzuge ich TypeScript gegenüber JavaScript.
Die Kommentare, dass Rust für Web-Backends nicht besonders geeignet sei, verwirren mich etwas
Bei kleinen Diensten wie beampaint.com oder probablyup.net habe ich eher die gegenteilige Erfahrung gemacht. Wenn man größere Apps baut, kann die Komplexität stark steigen – und das wird sie vermutlich auch –, aber um einem in großen Codebasen das nötige Vertrauen zu geben, ist Rust hervorragend
Ältere Frameworks bringen solche Dinge standardmäßig mit, bei Rust-Frameworks ist das aufgrund ihres Alters weniger wahrscheinlich. Man muss sich nur vorstellen, dass aus zwei fehlenden Funktionen sechs werden
Dieser Artikel fühlt sich fast wie meine eigene Lebensgeschichte an
Ich habe Rails verwendet und 2013 eine in Go geschriebene SaaS-App veröffentlicht, musste aber alles selbst herausfinden. Migrationen, Datenbankschicht, Asset-Pipeline, Deployment, Validierung, Logging, Zahlungen und vieles mehr, was Rails einfach mitliefert
Ich mag Go nicht besonders, aber hier ist Go nicht der Schuldige. Go für eine CRUD-Anwendung zu verwenden, war von Anfang an eine furchtbare Entscheidung
Bei Django fühlt es sich ähnlich an
Performance-Probleme entstanden viel häufiger durch Datenbankabfragen als durch Django selbst
Zu den einfachen Optimierungen, die oft vergessen werden, gehören das Anlegen von Datenbankindizes, mehr Logik an die Datenbank statt an den Code zu delegieren – zum Beispiel durch MongoDB-Pipelines oder partielle Indizes in Postgres –, Brotli-/zstd-Kompression für die meisten Requests zu aktivieren, Objekte oder Requests mit einem In-Memory-Cache wie Redis zu cachen und passende Cache-Control-Header zu setzen. Im Frontend braucht es ebenfalls Optimierungen, etwa nicht Tausende von Objekten als DOM-Nodes zu rendern, sondern Canvas zu verwenden. Dieses Problem habe ich kürzlich bei einer Kartenanwendung gesehen