Serviceobjekte mit Rails mit Aldous

Eines der Konzepte, mit denen wir im Tuts + Team großen Erfolg hatten, sind Serviceobjekte. Wir haben Serviceobjekte verwendet, um die Kopplung in unseren Systemen zu reduzieren, sie testbarer zu machen und wichtige Geschäftslogik für alle Entwickler im Team verständlicher zu machen. 

Als wir uns dazu entschieden haben, einige der Konzepte, die wir in unserer Rails-Entwicklung verwendet haben, in einen Ruby-Edelstein (Aldous) umzuwandeln, standen Service-Objekte ganz oben auf der Liste.

Was ich heute tun möchte, ist ein kurzer Überblick über die Serviceobjekte, die wir in Aldous implementiert haben. Hoffentlich erfahren Sie hier die meisten Dinge, die Sie wissen müssen, um Aldous-Serviceobjekte in Ihren eigenen Projekten verwenden zu können.

Die Anatomie eines Basisdienstobjekts

Foto von Dennis Skley

Ein Service-Objekt ist im Grunde eine Methode, die in ein Objekt eingeschlossen ist. Ein Service-Objekt kann manchmal mehrere Methoden enthalten, aber die einfachste Version ist nur eine Klasse mit einer Methode, z.

class DoSomething def perform # do stuff end end

Wir sind alle daran gewöhnt, unsere Objekte mit Nomen zu benennen, aber manchmal ist es schwierig, ein gutes Nomen zu finden, um einen Begriff darzustellen, während es im Hinblick auf eine Aktion (oder ein Verb) einfach und natürlich ist, darüber zu sprechen. Ein Serviceobjekt ist das, was wir bekommen, wenn wir mit dem Fluss „gehen“ und das Verb einfach in ein Objekt verwandeln.

Natürlich können wir aufgrund der obigen Definition jede Aktion / Methode in ein Serviceobjekt umwandeln, wenn wir dies wünschen. Folgende…

Klasse customer def createPurchase (order) # do stuff end end

… Könnte umgewandelt werden in:

Klasse CreateCustomerPurchase def initialisieren (Kunde, Auftrag) Ende def Ausführen # do stuff end end

Wir könnten mehrere andere Beiträge über die Auswirkungen von Service-Objekten auf das Design Ihres Systems, die verschiedenen Abwägungen, die Sie vornehmen werden, schreiben. Wir werden uns jetzt über diese Konzepte als ein Konzept informieren und sie nur als ein anderes Werkzeug betrachten Wir haben in unserem Arsenal.

Warum Serviceobjekte in Rails verwenden?

Da die Apps von Rails immer größer werden, neigen unsere Modelle dazu, ziemlich groß zu werden. Daher suchen wir nach Möglichkeiten, einige Funktionen aus ihnen herauszuholen und in „Helfer-Objekte“ zu integrieren. Dies ist jedoch oft leichter gesagt als getan. Rails hat in der Modellebene kein Konzept, das granularer ist als ein Modell. Am Ende müssen Sie viele Urteilsaufrufe machen:

  • Erstellen Sie ein PORO-Modell oder erstellen Sie eine Klasse in der lib Mappe?
  • Welche Methoden ziehen Sie in diese Klasse ein??
  • Wie benennen Sie diese Klasse sinnvoll, wenn wir die Methoden angeben, in die wir sie eingeführt haben?? 

Sie müssen jetzt den anderen Entwicklern in Ihrem Team und allen neuen Mitarbeitern, die später beitreten, mitteilen, was Sie getan haben. In einer ähnlichen Situation könnten andere Entwickler natürlich unterschiedliche Urteilsaufrufe treffen, was zu Inkonsistenzen führen könnte.

Serviceobjekte geben uns ein Konzept, das granularer ist als ein Modell. Wir können einen einheitlichen Standort für alle unsere Services haben und Sie ziehen immer nur eine Methode in einen Service ein. Sie benennen diese Klasse nach der Aktion / Methode, die sie repräsentiert. Wir können die Funktionalität in feinkörnigere Objekte extrahieren, ohne zu viele Urteilsaufrufe durchführen zu müssen. Dadurch bleibt das gesamte Team auf derselben Seite, sodass wir uns mit der Erstellung einer großartigen Anwendung befassen können. 

Durch die Verwendung von Serviceobjekten wird die Kopplung zwischen Ihren Rails-Modellen reduziert, und die daraus resultierenden Services sind aufgrund ihrer geringen Größe und ihres geringen Platzbedarfs sehr gut wiederverwendbar. 

Service-Objekte sind auch sehr gut überprüfbar, da sie normalerweise nicht so viel Test-Boilerplate erfordern wie schwerere Objekte, und Sie müssen nur die einzige Methode testen, die das Objekt enthält. 

Sowohl die Serviceobjekte als auch ihre Tests sind leicht zu lesen / zu verstehen, da sie sehr kohäsiv sind (auch ein Nebeneffekt ihrer geringen Größe). Sie können beide Service-Objekte und ihre Tests fast nach Belieben verwerfen und neu schreiben, da die Kosten dafür relativ gering sind und die Benutzeroberfläche sehr einfach zu warten ist.

Serviceobjekte haben definitiv viel zu bieten, insbesondere wenn Sie sie in Ihre Rails-Apps einführen. 

Serviceobjekte mit Aldous

Foto von Trevor Leyenhorst

Warum brauchen wir überhaupt einen Edelstein, wenn Serviceobjekte so einfach sind? Warum erstellen Sie nicht einfach POROs, und Sie müssen sich nicht um eine andere Abhängigkeit kümmern? 

Das könnte man definitiv tun, und in Tuts + haben wir das schon eine ganze Weile gemacht, aber durch ausgiebige Nutzung haben wir schließlich ein paar Muster für Services entwickelt, die unser Leben ein bisschen einfacher gemacht haben, und genau das haben wir in Aldous geschoben. Diese Muster sind leicht und erfordern nicht viel Magie. Sie machen unser Leben ein bisschen leichter, aber wir behalten die Kontrolle, wenn wir sie brauchen.

Wo sollen sie wohnen?

Erste Dinge zuerst, wo sollen Ihre Dienste wohnen? Wir neigen dazu, sie einzusetzen App / Dienste, also brauchst du folgendes in deiner app / config / application.rb:

config.autoload_paths + =% W (# config.root / app / services) config.eager_load_paths + =% W (# config.root / app / services)

Wie sollen sie genannt werden?

Wie ich oben erwähnt habe, neigen wir dazu, Service-Objekte nach Aktionen / Verben zu benennen (z. CreateUser, RückerstattungKauf), aber wir neigen auch dazu, "Service" an alle Klassennamen anzuhängen (z. CreateUserService, RefundPurchaseService). Unabhängig davon, in welchem ​​Kontext Sie sich befinden (beim Betrachten der Dateien im Dateisystem, beim Anzeigen einer Serviceklasse an einer beliebigen Stelle in der Codebase), wissen Sie immer, dass Sie mit einem Serviceobjekt zu tun haben.

Dies wird vom Edelstein in keiner Weise erzwungen, ist aber als gelernte Lektion zu berücksichtigen.

Serviceobjekte sind unveränderlich

Wenn wir unveränderlich sagen, meinen wir, dass sich der interne Zustand des Objekts nach der Initialisierung nicht mehr ändert. Dies ist wirklich großartig, da es viel einfacher ist, den Status jedes Objekts sowie das System als Ganzes zu beurteilen.

Damit das Obige wahr ist, kann die Service-Objektmethode den Status des Objekts nicht ändern. Daher müssen alle Daten als Ausgabe der Methode zurückgegeben werden. Dies lässt sich nur schwer direkt durchsetzen, da ein Objekt immer Zugriff auf seinen eigenen internen Status hat. Mit Aldous versuchen wir, dies durch Konventionen und Schulungen durchzusetzen, und die nächsten beiden Abschnitte zeigen Ihnen, wie das geht.

Erfolg und Misserfolg repräsentieren

Ein Aldous-Serviceobjekt muss immer einen von zwei Objekttypen zurückgeben:

  • Aldous :: Service :: Ergebnis :: Erfolg
  • Aldous :: Service :: Ergebnis :: Fehler

Hier ist ein Beispiel:

Klasse CreateUserService < Aldous::Service def perform user = User.new(user_data_hash) if user.save Result::Success.new else Result::Failure.new end end end

Weil wir von erben Aldous :: Service, Wir können unsere Rückgabeobjekte als konstruieren Ergebnis :: Erfolg. Wenn Sie diese Objekte als Rückgabewerte verwenden, können Sie Folgendes tun:

hash =  result = CreateUserService.perform (hash) wenn result.success? # Erfolge sonst noch # result.failure? do failure failure failure failure failure failure failure failure failure failure

Theoretisch könnten wir einfach nur wahr oder falsch zurückkehren und dasselbe Verhalten wie oben erhalten. Wenn wir das tun, könnten wir keine zusätzlichen Daten mit unserem Rückgabewert mitführen und möchten oft Daten mitnehmen.

DTOs verwenden

Der Erfolg oder Misserfolg einer Operation / Dienstleistung ist nur ein Teil der Geschichte. Oft haben wir ein Objekt erstellt, das wir zurückgeben möchten, oder Fehler erzeugt, von denen wir den aufrufenden Code benachrichtigen möchten. Aus diesem Grund ist die Rückgabe von Objekten, wie oben gezeigt, hilfreich. Diese Objekte dienen nicht nur dazu, Erfolg oder Misserfolg anzuzeigen, sondern sind auch Datenübertragungsobjekte.

Mit Aldous können Sie eine Methode in der Basis-Serviceklasse überschreiben, um einen Satz von Standardwerten anzugeben, die von dem Service zurückgegebene Objekte enthalten würden, z. B .:

Klasse CreateUserService < Aldous::Service attr_reader :user_data_hash def initialize(user_data_hash) @user_data_hash = user_data_hash end def default_result_data user: nil end def perform user = User.new(user_data_hash) if user.save Result::Success.new(user: user) else Result::Failure.new end end end

Die Hash-Schlüssel enthalten in default_result_data wird automatisch zu Methoden auf der Ergebnis :: Erfolg und Ergebnis :: Fehler Vom Dienst zurückgegebene Objekte. Wenn Sie für einen der Schlüssel in dieser Methode einen anderen Wert angeben, wird der Standardwert überschrieben. Also im Fall der obigen Klasse:

hash =  result = CreateUserService.perform (hash) wenn result.success? result.user # ist eine Instanz von User result.blah # würde einen Fehler verursachen, sonst # result.failure? result.user # wird null sein result.blah # würde ein Fehlerende auslösen

Die Hash-Schlüssel in der default_result_data Methode sind ein Vertrag für die Benutzer des Serviceobjekts. Wir garantieren, dass Sie jeden Schlüssel in diesem Hash als Methode für jedes Ergebnisobjekt aufrufen können, das aus dem Service herauskommt.

Fehlerfreie APIs

Bild von Roberto Zingales

Wenn wir über fehlerfreie APIs sprechen, meinen wir Methoden, die niemals Fehler auslösen, aber immer einen Wert zurückgeben, um Erfolg oder Misserfolg anzuzeigen. Ich habe schon über fehlerfreie APIs geschrieben. Aldous-Dienste sind fehlerfrei, je nachdem, wie Sie sie anrufen. Im obigen Beispiel: 

result = CreateUserService.perform (Hash)

Dies wird niemals einen Fehler auslösen. Intern umschließt Aldous Ihre Perform-Methode in eine Rettung blockieren und wenn Ihr Code einen Fehler auslöst, wird a zurückgegeben Ergebnis :: Fehler mit dem default_result_data als Daten. 

Dies ist ziemlich befreiend, da Sie nicht länger darüber nachdenken müssen, was mit dem von Ihnen geschriebenen Code schief gehen kann. Sie sind nur an dem Erfolg oder Misserfolg Ihres Services interessiert, und jeder Fehler führt zu einem Fehler. 

Dies ist für die meisten Situationen großartig. Aber manchmal möchten Sie einen Fehler generieren. Das beste Beispiel ist, wenn Sie ein Service-Objekt in einem Hintergrund-Worker verwenden und ein Fehler dazu führen würde, dass der Hintergrund-Worker einen erneuten Versuch unternimmt. Aus diesem Grund bekommt ein Aldous-Service auch eine magische ausführen! Methode und ermöglicht das Überschreiben einer anderen Methode aus der Basisklasse. Hier noch einmal unser Beispiel:

Klasse CreateUserService < Aldous::Service attr_reader :user_data_hash def initialize(user_data_hash) @user_data_hash = user_data_hash end def raisable_error MyApplication::Errors::UserError end def default_result_data user: nil end def perform user = User.new(user_data_hash) if user.save Result::Success.new(user: user) else Result::Failure.new end end end

Wie Sie sehen, haben wir das jetzt außer Kraft gesetzt raisable_error Methode. Manchmal möchten wir, dass ein Fehler erzeugt wird, aber wir möchten auch nicht, dass es sich um einen Fehler handelt. Andernfalls müsste unser aufrufender Code auf jeden möglichen Fehler aufmerksam werden, den der Dienst erzeugen kann, oder er muss einen der Basisfehlertypen abfragen. Deshalb verwenden Sie die ausführen! Methode, Aldous fängt immer noch alle Fehler für Sie ein, erhöht dann aber die raisable_error Sie haben den ursprünglichen Fehler als Ursache angegeben und festgelegt. Sie könnten jetzt folgendes haben:

hash =  begin service = CreateUserService.build (hash) result = service.perform! rescue service.raisable_error => e # Fehler Zeug endet

Testen von Aldous-Serviceobjekten

Möglicherweise haben Sie die Verwendung der Factory-Methode bemerkt:

CreateUserService.build (Hash) CreateUserService.perform (Hash)

Sie sollten diese immer verwenden und niemals Service-Objekte direkt erstellen. Die werkseigenen Methoden ermöglichen es uns, die netten Funktionen wie automatische Rettung und das Hinzufügen des Systems sauber einzuhängen default_result_data.

Bei Tests möchten Sie sich jedoch keine Gedanken darüber machen, wie Aldous die Funktionalität Ihrer Serviceobjekte erweitert. Konstruieren Sie beim Testen einfach die Objekte direkt mit dem Konstruktor und testen Sie dann Ihre Funktionalität. Sie erhalten Spezifikationen für die von Ihnen geschriebene Logik und vertrauen darauf, dass Aldous das tut, was es tun soll (Aldous hat eigene Tests), wenn es um die Produktion geht.

Fazit

Hoffentlich haben Sie dadurch eine Vorstellung davon bekommen, wie Service-Objekte (und insbesondere Aldous-Service-Objekte) ein gutes Werkzeug in Ihrem Arsenal sein können, wenn Sie mit Ruby / Rails arbeiten. Probieren Sie Aldous aus und lassen Sie uns wissen, was Sie denken. Schauen Sie sich auch den Aldous-Code an. Wir haben es nicht nur geschrieben, um nützlich zu sein, sondern auch lesbar und leicht verständlich zu sein.