Ein häufiges Problem bei der Arbeit mit Rails ist die Entscheidung, wo die Logik unserer Funktionen platziert werden soll.
Die Logik befindet sich oft in Controllern, Modellen oder, wenn wir Glück haben, in einem Serviceobjekt. Wenn wir also Service Objects haben, warum brauchen wir dann Use Cases?
Folgen Sie mir in diesem Artikel, um die Vorteile dieses Musters zu entdecken.
Anwendungsfall
Definition
Ein Anwendungsfall ist eine Liste von Aktionen oder Ereignisschritten, die typischerweise die Interaktionen zwischen einer Rolle und einem System definieren, um ein Ziel zu erreichen.
Es ist erwähnenswert, dass dieses Muster auf viele verschiedene Arten angewendet wird und alternative Namen hat. Wir können es finden als Interakteure, Betreiber oder Befehleaber in der Rubinrot Gemeinschaft, bei der wir bleiben Anwendungsfall. Jede Implementierung ist anders, hat aber denselben Zweck: die Nutzung des Systems durch einen Benutzer.
Auch wenn in unserem Projekt wir definieren die Anforderungen nicht mit Anwendungsfalls und UML ist dieses Muster immer noch nützlich, um die Geschäftslogik auf praktische Weise zu strukturieren.
Regeln
Unser Anwendungsfälle sein muss:
Framework-unabhängig
Datenbank-Agnostiker
Nur für eine Sache zuständig (Definition der Schritte, um das Ziel des Benutzers zu erreichen)
Vorteile
Lesbarkeit: Leicht zu lesen und zu verstehen, da die Schritte klar definiert sind.
Entkopplung: Verschieben Sie die Logik von Controllern und Modellen und schaffen Sie eine neue Abstraktionsebene.
Sichtbarkeit: Die Codebasis gibt Aufschluss über die im System verfügbaren Funktionen.
In die Praxis
Nehmen wir das Beispiel eines Benutzers, der in unserem System etwas kaufen möchte.
Modul UseCases
Modul Buyer
class Kauf
def initialize(käufer:, warenkorb:)
@buyer = Käufer
@cart = Einkaufswagen
end
def aufruf
return unless check_stock
return unless create_purchase
benachrichtigen end
privat
attr_leser :käufer, :wagen
def check_stock
Dienste::CheckStock.call(Warenkorb: Warenkorb)
end
def create_purchase
Services::CreatePurchase.call(käufer: käufer, warenkorb: warenkorb).call
end
def notify
Services::NotifyBuyer.call(käufer: käufer)
end
end
end
end
Wie Sie in diesem Artikel sehen können Code Beispiel haben wir eine neue Anwendungsfall namens Kauf. Wir haben nur eine öffentliche Methode definiert aufrufen. Innerhalb der Aufrufmethode finden wir ziemlich grundlegende Schritte, um einen Kauf zu tätigen, und alle Schritte sind als private Methoden definiert. Jeder Schritt ruft ein Serviceobjekt auf, so dass unser Anwendungsfall definiert nur die Schritte, um einen Kauf zu tätigen, nicht aber die Logik selbst. Dies gibt uns ein klares Bild davon, was in unserem System getan werden kann (einen Kauf tätigen) und die Schritte, um dies zu erreichen.
Jetzt sind wir bereit, unsere erste Anwendungsfall von einem Controller.
class Controller
def Kauf
UseCases::Buyer::Purchase.new(
buyer: purchase_params[:buyer],
cart: purchase_params[:cart]
).call
...
end
...
Ende
Unter diesem Gesichtspunkt ist die Anwendungsfall sieht einem Service Object ziemlich ähnlich, aber der Zweck ist ein anderer. Ein Service-Objekt erfüllt eine Aufgabe auf niedriger Ebene und interagiert mit verschiedenen Teilen des Systems wie der Datenbank, während das Anwendungsfall erstellt eine neue Abstraktion auf hoher Ebene und definiert die logischen Schritte.
Verbesserungen
Unser erstes Anwendungsfall funktioniert, könnte aber besser sein. Wie können wir es verbessern? Lassen Sie uns die trocken Edelsteine. In diesem Fall werden wir Folgendes verwenden Dry-Transaction.
Lassen Sie uns zunächst unsere Basisklasse definieren.
Klasse UseCase
einschließen Dry::Transaction
class << self
def call(**args)
new.call(**args)
end
end
end
Auf diese Weise können wir Attribute an die UseCase-Transaktion übergeben und sie verwenden. Dann können wir unseren Anwendungsfall "Kauf" neu definieren.
Modul UseCases
Modul Buyer
class Kauf
def initialize(käufer:, warenkorb:)
@buyer = Käufer
@cart = Einkaufswagen
end
def aufruf
return unless check_stock
return unless create_purchase
benachrichtigen
end
privat
attr_leser :käufer, :wagen
def check_stock
Dienstleistungen::CheckStock.call(Warenkorb: Warenkorb)
end
def create_purchase
Services::CreatePurchase.call(käufer: käufer, warenkorb: warenkorb).call
end
def notify
Services::NotifyBuyer.call(käufer: käufer)
end
end
end
end
Mit den neuen Änderungen können wir klar sehen, wie unsere Schritte definiert sind, und wir können das Ergebnis jedes Schrittes mit Success() und Failure() verwalten.
Wir sind bereit, unseren neuen Anwendungsfall im Controller aufzurufen und unsere Antwort in Abhängigkeit vom Endergebnis vorzubereiten.
class Controller
def Kauf
UseCases::Buyer::Purchase.new.call(
buyer: purchase_params[:buyer],
cart: purchase_params[:cart]
) do |result|
result.success do
...
end
result.failure do
...
end
end
...
end
...
Ende
Dieses Beispiel könnte durch Validierungen noch weiter verbessert werden, aber es reicht aus, um die Leistungsfähigkeit dieses Musters zu zeigen.
Schlussfolgerungen
Seien wir doch mal ehrlich, die Anwendungsfall-Muster ist ziemlich einfach und sieht einem Service Object sehr ähnlich, aber diese Abstraktionsebene kann Ihre Anwendung stark verändern.
Stellen Sie sich vor, ein neuer Entwickler tritt in das Projekt ein und öffnet den Ordner use_cases. Als ersten Eindruck erhält er eine Liste aller im System verfügbaren Funktionen, und nachdem er einen Use Case geöffnet hat, sieht er alle notwendigen Schritte für diese Funktion, ohne tief in die Logik einzudringen. Dieses Gefühl der Ordnung und Kontrolle ist der größte Vorteil dieses Musters.
Nehmen Sie dies mit in Ihren Werkzeugkasten, und vielleicht werden Sie es in Zukunft gut gebrauchen können.