Programmierung

Dec 30, 2022

Rückrufe mit dry-rb wegfegen

Rückrufe mit dry-rb wegfegen

Rückrufe mit dry-rb wegfegen

Mateusz Jarosz

Senior Backend-Entwickler

Weg mit den Callbacks, hin zu den Dry-RB Monads: eine neue Version der Rails-Konventionen, um Ihre Codebasis zu verbessern.

Willkommen zum wahrscheinlich wichtigsten Artikel, wenn es um die Verbesserung von Rails mit dry-rb geht. Nachdem wir in unserem letzten Artikel Rails-Kritik geübt haben, gehen wir nun einen Schritt weiter (natürlich mit etwas mehr Kritik!). Diesmal werden wir uns mit der Wurzel allen Übels beschäftigen - Callbacks.

Ähnlich wie im vorherigen Fall, in dem wir Validierungen besprochen haben, ist es wichtig zu verstehen, wann es in Ordnung ist, solche Mechanismen zu verwenden und sie zu schätzen, auch wenn ihre Möglichkeiten schnell enden. Schließlich haben wir sie alle durchlaufen, verstanden und in unseren Projekten eingesetzt. Eine überraschende Aussage, wenn man bedenkt, dass wir Callbacks im ersten Absatz als die Wurzel allen Übels bezeichnet haben, aber jetzt kommt's - sogar Callbacks können Ihnen unter bestimmten Umständen bei der Entwicklung Ihrer App helfen.

Nun zum Punkt. Wir sind erwachsen und jeder weiß, was Callbacks sind, also werden wir das nicht behandeln, aber warum benutzt Rails sie? Wie auch bei den Validierungen ist die Antwort auf diese Frage prosaisch: Sie sind einfach zu implementieren. Das ist keine Überraschung. Wir werden das bei jeder möglichen Gelegenheit betonen - Rails wurde für Anfänger entwickelt und soll so einsteigerfreundlich wie möglich sein. Und darin sind sie absolute Rockstars. Für fortgeschrittene Webentwickler müssen wir leider die Diagnose aus dem vorherigen Artikel wiederholen: Rails skaliert nicht, und das ist eine Tatsache

Das Problem

Es ist an der Zeit, genau zu erklären, warum wir Callbacks für die Wurzel allen Übels halten. Es ist ganz einfach: Es geht um Kontrolle, oder besser gesagt um das Fehlen von Kontrolle. Bei der Gestaltung von Prozessen ist Explizitheit immer besser als Implizitheit. Wenn Sie an einem Ablauf arbeiten, wollen Sie ihn so klar wie möglich definieren, für den Fall, dass ein anderer Entwickler Ihren Platz einnimmt und unterwegs etwas ändern oder einen Fehler beheben muss. Oder, was besonders schrecklich ist, Sie selbst müssen Ihren alten Code noch einmal durchsehen und ihn verstehen. Man könnte argumentieren, dass es in manchen Situationen gar nicht so notwendig ist, viel Wert auf die Sichtbarkeit zu legen, und dass Eindeutigkeit nicht die ultimative Antwort auf alles ist. Dem kann man nur zustimmen. Aber nicht, wenn es um Prozesse geht. Nicht im Falle des logischen Ablaufs von Vorgängen. Beim Entwerfen von Aktionen müssen Einfachheit und die richtige Reihenfolge der Operationen unsere primären Ziele sein.

Oh, und wenn wir von Prozessen sprechen, meinen wir die umfassende Bearbeitung einer Aufgabe als Ganzes, in unserem Fall wäre es ein Endpunkt. Ein Beispiel für einen Prozess wäre die Anmeldung oder die Bearbeitung eines Profils. Er beginnt mit der Anfrage, erledigt seine Aufgabe und endet mit der Antwort. In unserem Verständnis bestehen Prozesse aus Operationen, die kleinere, atomare Aufgaben sind. Authentifizierung, Autorisierung, Persistenz oder Serialisierung sind alles Beispiele für Operationen, die in einem Prozess vorkommen können.

Ok, aber das ist eine Menge abstraktes Geplapper auf hohem Niveau, ohne dass bisher klar ist, warum Rückrufe so versteinernd sind. Woran liegt das? Woher kommt dieser Hass auf Rückrufe? Hier ist es: Callbacks are side effects. Wir könnten den Artikel hier beenden und Sie selbst fragen lassen. Vielleicht finden Sie diese Enthüllung genauso monumental wie wir. Das ist der springende Punkt: Durch die Implementierung von Rückrufen führen wir Seiteneffekte in den Code ein. Durch die Wahl von Seiteneffekten akzeptieren wir, dass Operationen indirekt und implizit auf andere Operationen abgefeuert werden, was zu einem Dominoeffekt führt, bei dem sich Teile ohne Kontrolle verzweigen. Das ist der Kern des Problems, das Rückrufe für das Software-Design darstellen.

Lassen Sie uns eine kleine Übung machen, um das Problem besser zu visualisieren. Welche Aktion ist für Sie verständlicher?

Diejenige, die Rückrufe verwendet...

...oder die ohne sie?

Bevor ihr mit der Analyse dieser Prozesse beginnt, beachtet bitte, dass die in den beiden Grafiken gezeigten Vorgänge nur Beispiele sind und möglicherweise überhaupt keinen Sinn ergeben. Sie sollen nur einige allgemeine Systemanforderungen aufzeigen, sie sind nicht unbedingt konsistent (obwohl ich dieses Mal zugeben muss, dass ich bereits 2 Kaffee getrunken habe).

{

In Ordnung, jetzt sollten wir alle gut aufeinander abgestimmt sein. Aber lassen Sie uns noch etwas mehr ins Detail gehen, falls Sie immer noch nicht überzeugt sind, oder? Es gibt nichts Einfacheres als schrittweise Prozesse, wir erledigen immer nur eine Sache auf einmal. Natürlich ist es immer noch möglich, Mitarbeiter zu entlassen oder Aufträge zu planen, aber der Ablauf bleibt im Allgemeinen in Form eines einzigen Vorgangs. Sogar das Erklären des Prozesses ist so viel einfacher. Mit Rückrufaktionen wird plötzlich alles schwieriger. Auf den ersten Blick ist zu erkennen, dass der Prozess nun fragmentiert und auf eine unvorhersehbare Anzahl von Klassen verteilt ist. Das ist schon nicht gut. Ein Callback löst einen anderen aus, und wir sind gezwungen, alle Assoziationen durchzugehen. In vielen Fällen bedeutet dies, einen komplexen Baum von Modellen zu durchlaufen. Das ist ein Albtraum. Wir verlieren nicht nur die Kontrolle darüber, was passiert und in welcher Reihenfolge, sondern müssen auch alle verschiedenen Arten dieser Rückrufe lernen und analysieren. Wir müssen alle feinen Unterschiede zwischen nach_erstellen, nach_validieren kennen oder überprüfen, after_save, after_commit, and others. Können Sie die richtige Reihenfolge der Ausführung auf Anhieb erkennen? Und nicht zuletzt sind die Rückrufe unabhängig von allen anderen Prozessen. Das bedeutet, dass sie in den meisten Fällen gemeinsam genutzt werden und ein Callback in vielen verschiedenen Aktionen ausgelöst wird. Oftmals ist es das, was wir wollen, doch so oft ist es das, was Probleme verursacht und uns auf Fehlersuche gehen lässt.

Lassen Sie uns zu den eigenständigen Prozessen zurückkehren. Sie sind schön, aber wo ist der Haken? Es gibt keinen, wenn man seine Karten richtig ausspielt. Natürlich werden wir früher oder später feststellen, dass einige der Operationen in vielen verschiedenen Aktionen ausgelöst werden müssen, und es wird entweder mühsam, die gleichen oder ähnliche Methoden für alle zu implementieren, oder es entsteht die Notwendigkeit, diese Operationen zu extrahieren. Natürlich ist es keine gute Idee, genau denselben Code zu wiederholen, nur um unabhängige Prozesse zu haben. Auf diese Weise würden wir ein ernsthaftes Wartungsproblem schaffen. Zwar würden wir die Wiederholung von Code im Allgemeinen nicht so sehr verteufeln, da wir sehen, dass es manchmal Bedingungen gibt, unter denen dies sinnvoll ist, aber bei Prozessen innerhalb desselben Kontexts sollten wir den Code nicht unter der Androhung einer späteren Divergenz duplizieren. Zum Glück sind wir erwachsen und sprechen Ruby gut. Das Extrahieren von Codestücken ist keine Aufgabe, die uns normalerweise nachts wach hält.

Das Makeover

Bereit für etwas Action? Wir sind es auf jeden Fall! Lassen Sie uns Ihnen etwas vorstellen, das wir nicht wirklich verstehen... Ganz im Ernst. Was sind Monaden? Wir haben die Definition gelesen, aber wir haben uns nicht einmal die Mühe gemacht, sie zu verstehen. Das ist uns zu hoch, sorry.

Alles, was wir wissen müssen, ist, dass dry-monads eine Art Nachfolger von dry-transaction ist. Wir empfehlen dringend, dry-transaction zu lesen, bevor wir uns mit monads beschäftigen, es sollte die Dinge für Sie klarer machen.

Wir werden später in einige Details einsteigen.

{

Zunächst sehen wir uns eine Beispielimplementierung einer regulären Anmeldung mit Callbacks an.

Noch einmal, dies dient nur als Beispiel.

Und es gibt hier nichts Ausgefallenes, also werden wir nicht in die Tiefe der Analyse gehen. Eine Sache, die zu beachten ist, ist, dass dieser Code sehr unschuldig aussieht, wenn man Erfahrung mit Rails hat. Dank des begrenzten Umfangs, da wir nur Callbacks implementiert haben, sieht er auch optisch nicht so schlecht aus. Wir hoffen nur, dass Ihnen an dieser Stelle bereits klar ist, wo das Problem liegt.

Lassen Sie uns sehen, wie die Alternative aussehen könnte!

{

Die Modelle können komplett von Callbacks befreit werden. An der Controller-Aktion hat sich nicht viel geändert und wir haben eine neue Klasse erstellt, die Monaden verwendet. Jetzt können Sie alles, was der Endpunkt tut, einfach durch einen Blick auf die #call Methode erkennen. Wir sind die Seiteneffekte los (juhu! 🎉). Das gleiche Ergebnis könnten wir im Controller selbst erreichen, ohne eine weitere Klasse zu erstellen, aber natürlich würden wir ihn ziemlich schnell unübersichtlich machen. Die ganze Idee, Controller zu haben, die viele Endpunkte in nur einer Klasse behandeln, ist ohnehin suboptimal, um es mal so zu sagen. Also benutzen wir sie als eine Art Gateway zu einzelnen Aktionen, oder Transaktionen, wenn wir der trockenen Nomenklatur folgen wollen.

Die obige Implementierung ist noch weit davon entfernt, perfekt zu sein, aber wir haben einen sehr wichtigen Schritt gemacht. Jetzt findet die gesamte Verarbeitung der Anfrage in einer Klasse statt, abgesehen vom Rendern der Antwort in diesem Fall. Wir haben ein Wirrwarr von Callbacks in eine schöne, saubere, explizite Kette von Operationen verwandelt. Das macht das Debugging extrem einfach und ist außerdem eine außergewöhnlich skalierbare Lösung. Wenn ein wilder Fehler auftritt, wissen Sie genau, wo Sie danach suchen müssen, da es eine Klasse gibt, die speziell für die Bearbeitung der Anfrage entwickelt wurde, und alle Operationen haben eine bestimmte Aufgabe zu erfüllen. Nichts wird Sie überraschen, wenn es keine Seiteneffekte gibt.

Aber warum haben wir genau Monaden verwendet, wenn wir die gleichen Ergebnisse mit einfachen Ruby-Objekten hätten erzielen können? Wo genau liegt hier die Optimierung? Was hat diese trockene Bibliothek mit unserer Lösung zu tun? Bisher haben wir eine etwas seltsame Verwendung des yield Schlüsselwortes gesehen. Um nicht zu sagen, es fühlt sich irgendwie missbräuchlich an.

Nun, das sind alles berechtigte Fragen, lassen Sie uns erklären. Im obigen Beispiel sieht es so aus, als hätten wir eine ziemlich glückliche Programmierung™ gemacht. Die Operationen werden eine nach der anderen abgefeuert, genau wie normaler prozeduraler Code, ohne Rücksicht auf die Möglichkeit, dass eine von ihnen fehlschlagen könnte. Oder um genau zu sein: stillschweigend fehlschlagen könnte, ohne eine Ausnahme oder einen Fehler auszulösen. Wenn so etwas passiert, ist normalerweise die Hölle los. Alles, was nach der fehlgeschlagenen Operation folgt, ist nicht mehr in Ordnung, da es auf einer falschen Prämisse beruht. An dieser Stelle kommen Monaden ins Spiel. Und deshalb haben wir die Ergebnisse der Methoden mit Erfolg oder Fehlschlag umhüllt. Solange wir erfolgreich sind, wird alles genau so ablaufen wie vorhergesagt, aber sobald ein Fehler auftritt, stoppt die gesamte Ausführung. Das bedeutet, dass kein Transaktionscode über den Fehlerpunkt hinaus ausgeführt wird. Und das ist dank der do-notation, die von dry-monads eingeführt wurde. die Schnittstelle Failure erlaubt uns auch, einige Argumente zu übergeben, die wir bei der Behandlung des Fehlers verwenden können. Bitte beachten Sie, dass wir konsequent darauf verzichten, zu erklären, wie bestimmte dry-rb-Mechanismen funktionieren. Die Details der dry-rb-Werkzeuge sind auf ihrer Website gut dokumentiert. Es macht keinen Sinn, sie zu duplizieren.

Bevor wir auf die Nachteile dieser Lösung eingehen, wollen wir sehen, wie sie noch verbessert werden kann. Wir haben uns darauf konzentriert, zu zeigen, wie Trockenmonaden verwendet werden können, und versucht, nicht zu viele Konzepte auf einmal einzuführen. Beachten Sie, wie wir den Vertrag verwendet haben, den wir im vorherigen Artikel besprochen haben. Wir übergeben ihn als abstrakte Abhängigkeit, indem wir ein Schlüsselwortargument mit einem Standardwert verwenden. Die Vertragsinstanz erfüllt ihre Aufgabe und dank der Methode #to_monad ist der Schritt #validate nur 1 Zeile lang. Die letztgenannten Erstellungsmethoden verwenden Modelle direkt. Anstatt Modelle in den Körper der Methoden einzubetten und sie sozusagen "hart zu kodieren", könnten wir eine andere Klasse - ein Repository - erstellen und es als Abhängigkeit mit einem Schlüsselwortargument übergeben, wie wir es mit #validate getan haben. Diese Technik hat viele Vorteile, aber das ist ein ganz anderes Thema. Konzentrieren wir uns auf die Repositories selbst. In realen Szenarien ist die Arbeit mit Daten viel komplexer als in unserem Beispiel, und es wird mehr Platz benötigt. Wir können ihn finden, indem wir völlig neue Klassen entwerfen. Auf diese Weise wären unsere Transaktionsmethoden von den Repositories abhängig, und die Datenverarbeitungslogik würde ausgeblendet werden, da sie uns hier und jetzt nicht interessiert. Die Transaktionen wiederum wären schlanker. Gewinne überall. Wir würden auch Hilfsmethoden loswerden, die in entsprechende Repository-Klassen verlagert würden. Oh, und wir könnten dieselben Repository-Aktionen über viele Transaktionen hinweg gemeinsam nutzen. Einfach herrlich.

Ok, jetzt die Nachteile. Ähmmm... da muss es doch welche geben, oder? Natürlich gibt es die. Der Hauptunterschied zu Callbacks besteht darin, dass Sie eine Konsole oder Skripte verwenden. Für einige manuelle Aufgaben werden Sie Repositories oder andere Komponenten wiederverwenden, für einige andere werden Sie die gesamten Transaktionen ausführen wollen. Wenn diese Transaktionen eine Anfrage oder eine bestimmte Datenstruktur akzeptieren, müssen Sie diese zunächst vorbereiten, was viel Zeit in Anspruch nimmt. Im Allgemeinen müssen Sie bei der manuellen Arbeit etwas mehr Sorgfalt walten lassen und manchmal auch mehr Aufwand betreiben. Ein weiterer strittiger Punkt bei diesem Ansatz ist, dass man bei jedem neuen Endpunkt besonders vorsichtig sein muss. Jede neue Transaktion muss von Grund auf neu erstellt werden. Man fängt mit 0 Code an und kann natürlich bereits implementierten Code wiederverwenden, aber man muss es ausdrücklich selbst tun. Wir sagen, dass dies fragwürdig ist, denn obwohl es so aussieht, als ob mehr Arbeit anfällt und es schwieriger ist, befreit es gleichzeitig von höchstwahrscheinlich undokumentierten und unklaren Nebeneffekten, die vor Monaten, wenn nicht Jahren, implementiert wurden. Mit großer Macht kommt große Verantwortung.

The wrap

  • Die Konventionen von Rails sind nicht skalierbar und eignen sich nicht für komplexe Projekte.

  • Die Modelle von Rails sind mit Verantwortlichkeiten überladen, was zu Wartungsproblemen führt.

  • Die beiden obigen Schlussfolgerungen wurden in exakt derselben Form aus dem vorherigen Artikel über Validierungen übernommen. Sie gelten immer noch.

  • {

    Die Controller von Rails verwalten zu viele Endpunkte, was oft zu deren Überwucherung führt. Niemand mag fettige Klassen.

    Callbacks sind einfach zu implementieren, aber sie verkomplizieren die Dinge sehr, da sie indirekt abgefeuert werden.

    Die indirekte Ausführung von Callbacks führt dazu, dass alle Prozesse vage und verworren sind.

  • {

    Die Verwaltung von Rückrufen, besonders auf lange Sicht, ist schwieriger als die Verwaltung einzelner Prozesse.

  • Es gibt kein verzweifelteres Gefühl für einen Entwickler als nicht zu wissen, was passiert und nicht in der Lage zu sein, das Problem zu finden. Callbacks machen dies alles einfach.

  • {

    Das Entwerfen von eigenständigen Prozessen anstelle der Pflege von Callbacks wird endlich etwas Ruhe in Ihr Leben bringen.

  • Proper operations design enhances visibility. Wenn es richtig gemacht wird, gibt uns die #call Methode der Aktionsklasse ein klares Verständnis auf den ersten Blick.

  • Die wirkliche Verbesserung des Anwendungsdesigns besteht in der Umstellung des Ansatzes von verwickelten impliziten Prozessen mit mehreren Startpunkten auf eigenständige explizite Prozesse mit einem Startpunkt. Dies stellt eine Änderung des Softwaredesigns dar.

  • Die obige Schlussfolgerung soll eine aus dem vorangegangenen Artikel imitieren, in dem wir eine Software-Design-Änderung im Zusammenhang mit Validierungen hervorhoben. Beide sind wesentlich für die Verbesserung der täglichen Arbeit. Der Rest ist lediglich eine Art und Weise, diese Änderungen auszuführen.

  • {

    Sie müssen nicht auf uns hören und dry-rb verwenden. Sie können verwenden, was Sie wollen, sogar einfache Ruby-Objekte oder Bibliotheken, die Sie selbst programmieren können. Solange Sie sich an die hier beschriebenen Designregeln halten, sollten Sie mit Ihrem komplexen Projekt keine Probleme haben.

    Auch hier haben wir kaum an der Oberfläche der dry-rb Monaden gekratzt. Bitte besuchen Sie die dry-rb Hauptseite und sehen Sie sowohl dry-transaction als auch dry-monads mit eigenen Augen.

  • Trockenmonaden sehen seltsam aus und sind schwer zu begreifen, wenn man ihre Natur wirklich verstehen will, aber sie sind einfach in der Anwendung und hilfreich beim Aufbau von Prozessen.

  • dry-monads erleichtern das Fehlermanagement und machen es überflüssig, ständig Ausnahmen zu behandeln oder unseren Code mit Bedingungen zu versehen.

✍️

ABOUT THE AUTHOR

Mateusz Jarosz

Senior Backend-Entwickler

Senior backend developer

Sie haben eine Projektidee? Lassen Sie uns darüber reden und sie zum Leben erwecken

Ihre hochqualifizierten Spezialisten sind da. Nehmen Sie Kontakt auf, um zu sehen, was wir gemeinsam tun können.

Dariusz Michalski

Dariusz Michalski, CEO

dariusz@useo.pl

Konrad Pochodaj

Konrad Pochodaj, CGO

konrad@useo.pl

Sie haben eine Projektidee? Lassen Sie uns darüber reden und sie zum Leben erwecken

Ihre hochqualifizierten Spezialisten sind da. Nehmen Sie Kontakt auf, um zu sehen, was wir gemeinsam tun können.

Dariusz Michalski

Dariusz Michalski, CEO

dariusz@useo.pl

Konrad Pochodaj

Konrad Pochodaj, CGO

konrad@useo.pl

Sie haben eine Projektidee? Lassen Sie uns darüber reden und sie zum Leben erwecken

Ihre hochqualifizierten Spezialisten sind da. Nehmen Sie Kontakt auf, um zu sehen, was wir gemeinsam tun können.

©2009 - 2025 Useo sp. z o.o.