EinfĂŒhrung
In den vorherigen Artikeln haben wir beschrieben: Umfang , methodische Grundlagen , ein Beispiel fĂŒr Architektur und Struktur . In diesem Artikel möchte ich erlĂ€utern, wie die Prozesse beschrieben werden, welche Prinzipien zum Sammeln von Anforderungen gelten, wie sich GeschĂ€ftsanforderungen von funktionalen unterscheiden und wie von Anforderungen zu Code gewechselt wird. Sprechen Sie ĂŒber die Prinzipien der Verwendung des Anwendungsfalls und wie sie uns helfen können. Sehen Sie sich Beispiele fĂŒr Implementierungsoptionen fĂŒr Interactor- und Service Layer-Entwurfsmuster an.

Die im Artikel angegebenen Beispiele werden mit unserer LunaPark- Lösung gegeben. Sie hilft Ihnen bei den ersten Schritten in den beschriebenen AnsÀtzen.
Trennen Sie funktionale Anforderungen von GeschÀftsanforderungen.
Immer wieder kommt es vor, dass viele GeschĂ€ftsideen nicht wirklich zum endgĂŒltigen, beabsichtigten Produkt werden. Dies ist hĂ€ufig darauf zurĂŒckzufĂŒhren, dass der Unterschied zwischen GeschĂ€ftsanforderungen und funktionalen Anforderungen nicht verstanden werden kann, was letztendlich zu einer unzureichenden Erfassung von Anforderungen, unnötiger Dokumentation, Projektverzögerungen und gröĂeren Projektfehlern fĂŒhrt.
Oder manchmal sind wir mit Situationen konfrontiert, in denen die endgĂŒltige Lösung zwar den BedĂŒrfnissen der Kunden entspricht, die GeschĂ€ftsziele jedoch irgendwie nicht erreicht werden.
Daher ist es unbedingt erforderlich, die GeschÀftsanforderungen von den funktionalen Anforderungen zu trennen, bis Sie mit der Definition beginnen. Nehmen wir ein Beispiel.
Angenommen, wir schreiben einen Antrag fĂŒr eine Pizza-Lieferfirma und haben beschlossen, ein Kurier-Tracking-System zu erstellen. Die GeschĂ€ftsanforderungen lauten wie folgt:
"EinfĂŒhrung eines webbasierten Systems und eines mobilen, auf Mitarbeitern basierenden Mitarbeiterverfolgungssystems, das Kuriere auf ihren Routen erfasst und die Effizienz verbessert, indem die AktivitĂ€ten der Kuriere, ihre Abwesenheit von der Arbeit und die ArbeitsproduktivitĂ€t ĂŒberwacht werden."
Hier können wir eine Reihe von charakteristischen Merkmalen unterscheiden, die darauf hinweisen, dass dies Anforderungen aus dem GeschÀft sind:
- GeschÀftsanforderungen werden immer aus Sicht des Kunden geschrieben;
- Dies sind breite Anforderungen auf hoher Ebene, die jedoch immer noch teilorientiert sind.
- Sie sind keine Unternehmensziele, sondern helfen dem Unternehmen, Ziele zu erreichen.
- Beantworten Sie die Fragen â Warum â und â Was â. Was möchte das Unternehmen erhalten? Und warum braucht sie es?
Funktionale Anforderungen sind Aktionen , die das System ausfĂŒhren muss, um GeschĂ€ftsanforderungen zu implementieren. Somit hĂ€ngen die funktionalen Anforderungen mit der entwickelten Lösung oder Software zusammen. Wir formulieren die funktionalen Anforderungen fĂŒr das obige Beispiel:
- Das System sollte den LĂ€ngen- und Breitengrad des Mitarbeiters ĂŒber GPS / GLONASS anzeigen.
- Das System sollte die Positionen der Mitarbeiter auf der Karte anzeigen.
- Das System sollte es Managern ermöglichen, Benachrichtigungen an ihre AuĂendienstmitarbeiter zu senden.
Wir heben die folgenden Merkmale hervor:
- funktionale Anforderungen werden immer aus Sicht des Systems geschrieben;
- sie sind spezifischer und detaillierter;
- Dank der ErfĂŒllung der funktionalen Anforderungen wird eine effektive Lösung entwickelt, die den Anforderungen des Unternehmens und den Zielen des Kunden entspricht.
- Beantworten Sie die Frage " wie ". Wie das System GeschÀftsanforderungen löst.
Es sollten einige Worte zu nicht funktionalen Anforderungen (auch als âQualitĂ€tsanforderungenâ bezeichnet) gesagt werden, die das Design oder die Implementierung einschrĂ€nken (z. B. Anforderungen an Leistung, Sicherheit, VerfĂŒgbarkeit, ZuverlĂ€ssigkeit). Solche Anforderungen beantworten die Frage, was das System sein soll.
Entwicklung ist die Ăbersetzung von GeschĂ€ftsanforderungen in funktionale. Angewandte Programmierung ist die Implementierung funktionaler Anforderungen und des Systems - nicht funktionsfĂ€hig.
AnwendungsfÀlle
Die Implementierung funktionaler Anforderungen ist in kommerziellen Systemen hĂ€ufig die komplexeste. In einer reinen Architektur werden funktionale Anforderungen ĂŒber die Use-Case- Ebene implementiert.
Aber fĂŒr den Anfang möchte ich mich an die Quelle wenden. Ivar Jacobson - der Autor der Definition von Use Case , einer der Autoren von UML, und der RUP-Methodik in seinem Artikel Use-Case 2.0 Der Hub of Software Development identifiziert 6 Prinzipien fĂŒr die Verwendung von Use Cases :
- mach sie einfach durch GeschichtenerzÀhlen;
- einen strategischen Plan haben, das ganze Bild kennen;
- Fokus auf Bedeutung;
- Richten Sie das System in Schichten aus.
- liefern Sie das System Schritt fĂŒr Schritt;
- die BedĂŒrfnisse des Teams erfĂŒllen.
Wir betrachten kurz jedes dieser Prinzipien, sie sind fĂŒr uns zum weiteren VerstĂ€ndnis nĂŒtzlich. Unten finden Sie meine kostenlose Ăbersetzung mit AbkĂŒrzungen und Beilagen. Ich empfehle Ihnen dringend, sich mit dem Original vertraut zu machen.
Einfachheit durch GeschichtenerzÀhlen
ErzĂ€hlung ist Teil unserer Kultur; Dies ist der einfachste und effektivste Weg, um Wissen und Informationen von einer Person zur anderen zu ĂŒbertragen. Dies ist der beste Weg, um zu kommunizieren, was das System tun soll, und um dem Team zu helfen, sich auf gemeinsame Ziele zu konzentrieren.
AnwendungsfÀlle spiegeln die Systemziele wider. Um den Anwendungsfall zu verstehen, erzÀhlen wir eine bestimmte Geschichte. Die Geschichte erzÀhlt, wie man ein Ziel erreicht und Probleme löst, die auf dem Weg entstehen. AnwendungsfÀlle wie ein Storybook bieten eine Möglichkeit, die verschiedenen, aber verwandten Geschichten auf einfache und umfassende Weise zu identifizieren und zu behandeln. Dies erleichtert das Sammeln, Verteilen und Verstehen von Systemanforderungen.
Dieses Prinzip korreliert mit der Ubiques-Sprache aus dem DDD-Ansatz.
Das ganze Bild verstehen
UnabhĂ€ngig davon, welches System Sie entwickeln, ob groĂ, klein, Software, Hardware oder Unternehmen, ist es sehr wichtig, das Gesamtbild zu verstehen. Ohne das System als Ganzes zu verstehen, können Sie nicht die richtigen Entscheidungen darĂŒber treffen, was in das System aufgenommen werden soll, was ausgeschlossen werden soll, wie viel es kosten wird und welche Vorteile es bringen wird.
Ivar Jacobson schlĂ€gt vor, das Anwendungsfalldiagramm zu verwenden , das zum Sammeln von Anforderungen sehr praktisch ist. Wenn die Anforderungen kompiliert und klar sind, ist die Kontextkarte von Eric Evans die beste Option. Oft wird der Scrum-Ansatz so interpretiert, dass die Menschen keine Zeit mit einem strategischen Plan verbringen, da sie mehr als zwei Wochen spĂ€ter ein Relikt der Vergangenheit planen. Jeff Sutherlands Propaganda fiel auf den Wasserfluss, und Leute, die zweiwöchige Schulungen fĂŒr Scrum Masters absolvierten, die Projekte verwalten durften, machten ihre Arbeit. Der gesunde Menschenverstand erkennt jedoch die Bedeutung der strategischen Planung an. Es ist nicht erforderlich, einen detaillierten Strategieplan zu erstellen, dies sollte jedoch der Fall sein.
Wert im Fokus
Wenn Sie versuchen zu verstehen, wie das System verwendet wird, ist es immer wichtig, sich auf den Wert zu konzentrieren, den es seinen Benutzern und anderen interessierten Parteien bietet. Wert wird nur gebildet, wenn das System verwendet wird. Daher ist es viel besser, sich auf die Anwendung des Systems zu konzentrieren, als auf lange Listen von Funktionen, die es bieten kann.
AnwendungsfÀlle bieten diesen Fokus und helfen Ihnen, sich darauf zu konzentrieren, wie das System von einem bestimmten Benutzer verwendet wird, um sein Ziel zu erreichen. AnwendungsfÀlle decken viele Möglichkeiten zur Verwendung des Systems ab: diejenigen, die ihre Ziele erfolgreich erreichen, und diejenigen, die auftretende Schwierigkeiten lösen.
DarĂŒber hinaus gibt der Autor ein wunderbares Schema, dem die gröĂte Aufmerksamkeit geschenkt werden sollte:

Das Diagramm zeigt einen Anwendungsfall âBargeldbezug an einem Geldautomatenâ. Der einfachste Weg, um das Ziel zu erreichen, ist in der Grundrichtung (Grundablauf) beschrieben. Andere FĂ€lle werden als alternativer Fluss beschrieben. Diese Anweisungen helfen beim GeschichtenerzĂ€hlen, beim Strukturieren des Systems und beim Schreiben von Tests.
Schichtung
Die meisten Systeme erfordern viel Arbeit, bevor sie einsatzbereit sind. Sie haben viele Anforderungen, von denen die meisten von anderen Anforderungen abhĂ€ngen. Sie mĂŒssen implementiert werden, bevor die Anforderungen erfĂŒllt und bewertet werden.
Es ist ein groĂer Fehler, jeweils ein solches System zu erstellen. Das System sollte aus Teilen aufgebaut sein, von denen jedes fĂŒr die Benutzer einen eindeutigen Wert hat.
Diese Ideen finden Resonanz bei agilen AnsÀtzen und bei Domain- Ideen.
Schrittweise ProdukteinfĂŒhrung
Die meisten Softwaresysteme haben sich ĂŒber viele Generationen hinweg weiterentwickelt. Sie werden nicht gleichzeitig produziert; Sie bestehen aus einer Reihe von Versionen, von denen jede auf einer frĂŒheren Version basiert. Sogar die Veröffentlichungen selbst gehen oft nicht sofort aus, sondern entwickeln sich durch eine Reihe von Zwischenversionen. Jeder Schritt bietet eine klare, verwendbare Version des Systems. Auf diese Weise sollten alle Systeme erstellt werden.
ErfĂŒllen Sie die BedĂŒrfnisse des Teams
Leider gibt es keine universelle Lösung fĂŒr Softwareentwicklungsprobleme. Unterschiedliche Teams und unterschiedliche Situationen erfordern unterschiedliche Stile und unterschiedliche Detaillierungsgrade. UnabhĂ€ngig davon, fĂŒr welche Methoden und Techniken Sie sich entscheiden, mĂŒssen Sie sicherstellen, dass sie anpassungsfĂ€hig genug sind, um den aktuellen Anforderungen des Teams gerecht zu werden.
Eric Evans fordert Sie in seinem Buch auf, nicht viel Zeit damit zu verbringen, alle Prozesse ĂŒber UML zu beschreiben. Es reicht aus, visuelle Schemata zu verwenden. Unterschiedliche Teams, unterschiedliche Projekte erfordern unterschiedliche Detaillierungsgrade, da der UML-Autor selbst darĂŒber spricht.
Implementierung
In der reinen Architektur definiert Robert Martin die folgenden AnwendungsfÀlle :
Diese AnwendungsfÀlle koordinieren den Datenfluss zu und von den EntitÀten und weisen diese EntitÀten an, ihre kritischen GeschÀftsregeln zu verwenden, um die Ziele des Anwendungsfalls zu erreichen.
Versuchen wir, diese Ideen in Code zu ĂŒbersetzen. Erinnern wir uns an das Schema aus dem dritten Prinzip der Verwendung der AnwendungsfĂ€lle und nehmen es als Grundlage. Stellen Sie sich einen wirklich komplexen GeschĂ€ftsprozess vor: âKochen eines Kohlkuchensâ.
Versuchen wir es zu zerlegen:
- ProduktverfĂŒgbarkeit prĂŒfen;
- nimm sie aus dem Lager;
- den Teig kneten;
- lass den Teig aufgehen;
- bereite die FĂŒllung vor;
- einen Kuchen machen;
- backe einen Kuchen.
Wir implementieren diese gesamte Sequenz ĂŒber den Interactor , und jeder Schritt wird ĂŒber eine Funktion oder ein Funktionsobjekt auf der Serviceschicht implementiert.
Reihenfolge der Aktionen (Interaktor)

Ich empfehle dringend, die Entwicklung eines komplexen GeschĂ€ftsprozesses mit der Reihenfolge der Aktionen zu beginnen . Genauer gesagt, mĂŒssen Sie die Domain Domain bestimmen, zu der der GeschĂ€ftsprozess gehört. KlĂ€ren Sie alle geschĂ€ftlichen Anforderungen. Identifizieren Sie alle am Prozess beteiligten EntitĂ€ten . Dokumentieren Sie die Anforderungen und Definitionen jeder EntitĂ€t in der Wissensdatenbank.
Malen Sie alles schrittweise auf Papier. Manchmal benötigen Sie ein Sequenzdiagramm. Sein Autor ist derselbe, der den Anwendungsfall erfunden hat - Ivar Jacobson. Das Diagramm wurde von ihm erfunden, als er fĂŒr Erickson ein auf der Relaisschaltung basierendes Telefonnetz-Wartungssystem entwickelte. Ich mag dieses Diagramm sehr und der Begriff Sequenz ist meiner Meinung nach aussagekrĂ€ftiger als der Begriff Interactor . In Anbetracht der gröĂeren Verbreitung des letzteren werden wir den bekannten Begriff Interactor verwenden .
Ein kleiner Hinweis, wenn Sie einen GeschĂ€ftsprozess beschreiben, ist eine gute Hilfe fĂŒr Sie. Die Hauptregel der Dokumentenverwaltung kann lauten: âAls Ergebnis einer GeschĂ€ftstĂ€tigkeit sollte ein Dokument erstellt werden.â Zum Beispiel entwickeln wir ein Rabattsystem. Mit einem Rabatt schlieĂen wir aus geschĂ€ftlicher Sicht eine Vereinbarung zwischen dem Unternehmen und dem Kunden. Alle Bedingungen mĂŒssen in diesem Vertrag festgelegt werden. Das heiĂt, in der DiscountSystem-DomĂ€ne haben Sie Entites :: Contract. Binden Sie den Rabatt nicht an den Kunden, sondern erstellen Sie einen Unternehmensvertrag, in dem die Regeln fĂŒr seine Bereitstellung beschrieben werden.
Kehren wir zur Beschreibung unseres GeschĂ€ftsprozesses zurĂŒck, nachdem er fĂŒr alle an seiner Entwicklung beteiligten Personen transparent geworden ist und Ihr gesamtes Wissen feststeht. Ich empfehle Ihnen, Code mit der Reihenfolge der Aktionen zu schreiben.
Die Sequenzdesign- Vorlage ist verantwortlich fĂŒr:
- Abfolge von Aktionen ;
- Koordination der ĂŒbertragenen Daten zwischen Aktionen ;
- Verarbeitungsfehler, die von Aktionen wĂ€hrend ihrer AusfĂŒhrung begangen wurden;
- RĂŒckgabe des Ergebnisses der zugesagten Aktionen ;
- WICHTIG : Die wichtigste Verantwortung fĂŒr dieses Entwurfsmuster ist die Implementierung der GeschĂ€ftslogik.
Ich möchte nĂ€her auf die letzte Verantwortung eingehen, wenn wir einen komplexen Prozess haben - wir mĂŒssen ihn so beschreiben, dass klar ist, was passiert, ohne auf technische Details einzugehen. Sie sollten es so aussagekrĂ€ftig beschreiben, wie es Ihre Programmierkenntnisse zulassen . Vertrauen Sie diese Klasse dem erfahrensten Mitglied Ihres Teams an.
Kehren wir zum Kuchen zurĂŒck: Versuchen wir, den Prozess seiner Vorbereitung durch Interactor zu beschreiben.
Implementierung
Ich gebe eine Beispielimplementierung mit unserer LunaPark- Lösung, die wir in einem frĂŒheren Artikel vorgestellt haben.
module Kitchen module Sequences class CookingPieWithabbage < LunaPark::Interactors::Sequence TEMPERATURE = Values::Temperature.new(180, unit: :cel) def call! Services::CheckProductsAvailability.call list: ingredients dough = Services::BeatDough.call from: Repository::Products.get(beat_ingredients) filler = Services::MakeabbageFiller.call from: Repository::Products.get(filler_ingredients) pie = Services::MakePie.call dough, with: filler bake = Services::BakePie.new pie, temp: TEMPERATURE sleep 5.min until bake.call pie end private attr_accessor :beat_ingredients, :filler_ingredients attr_accessor :pie def ingredients_list beat_ingredients_list + filler_ingredients_list end end end end
Wie wir sehen können, ist der call!
beschreibt die gesamte GeschÀftslogik des Kuchenbackprozesses. Und es ist bequem zu verwenden, um die Logik der Anwendung zu verstehen.
AuĂerdem können wir den Prozess des Backens von Fischpastete leicht beschreiben, indem MakeabbageFiller
MakeFishFiller
durch MakeabbageFiller
MakeFishFiller
. Daher Ă€ndern wir den GeschĂ€ftsprozess sehr schnell, ohne wesentliche CodeĂ€nderungen. AuĂerdem können wir beide Sequenzen gleichzeitig belassen und GeschĂ€ftsfĂ€lle skalieren.
Arrangements
- Methode
call!
ist eine erforderliche Methode und beschreibt die Reihenfolge der Aktionen . - Jeder Initialisierungsparameter kann durch einen Setter oder
attr_acessor
:
class Foo < LunaPark::Interactors::Sequence
- Der Rest der Methoden sollte privat sein.
Anwendungsbeispiel
beat_ingredients = [ Entity::Product.new :flour, 500, :gr, Entity::Product.new :oil, 50, :gr, Entity::Product.new :salt, 1, :spoon, Entity::Product.new :milk, 150, :ml, Entity::Product.new :egg, 1, :unit, Entity::Product.new :yeast, 1, :spoon ] filler_ingredients = [ Entity::Product.new :cabbage, 500, :gr, Entity::Product.new :salt, 1, :spoon, Entity::Product.new :pepper, 1, :spoon ] cooking = CookingPieWithabbage.call( beat_ingredients: beat_ingredients, filler_ingredients: filler_ingredients )
Der Prozess wird durch das Objekt dargestellt und wir haben alle notwendigen Methoden, um es aufzurufen - war der Aufruf erfolgreich, ist wÀhrend des Aufrufs ein Fehler aufgetreten, und wenn ja, welche?
Fehlerbehandlung
Wenn wir uns jetzt an das dritte Prinzip der Use-Case-Anwendung erinnern, achten wir darauf, dass wir neben der Hauptzeile auch alternative Anweisungen hatten. Dies sind Fehler, die wir behandeln mĂŒssen. Stellen Sie sich ein Beispiel vor: Wir möchten sicher nicht, dass Ereignisse in diese Richtung verlaufen, aber wir können nichts dagegen tun. Die harte RealitĂ€t ist, dass die Kuchen regelmĂ€Ăig verbrannt werden.
Interactor fÀngt alle Fehler ab, die von der LunaPark::Errors::Processing
Klasse geerbt wurden.
Wie verfolgen wir den Kuchen? Definieren Sie dazu den Fehler Burned
in der BakePie
Aktion .
module Kitchen module Errors class Burned < LunaPark::Errors::Processing; end end end
Und stellen Sie beim Backen sicher, dass unser Kuchen nicht ausgebrannt ist:
module Kitchen module Services class BakePie < LunaPark::Callable def call
In diesem Fall funktioniert die Fehlerfalle, und wir können sie in
.
Fehler, die nicht von der Processing
geerbt wurden, werden als Systemfehler wahrgenommen und auf Serverebene abgefangen. Sofern nicht anders angegeben, erhÀlt der Benutzer einen 500 ServerError.
Ăbungsgebrauch
1. Versuchen Sie, alle Aufrufe in der Aufrufmethode zu beschreiben!
Sie sollten nicht jede Aktion in einer separaten Methode implementieren. Dadurch wird der Code aufgeblĂ€hter. Sie mĂŒssen die gesamte Klasse mehrmals betrachten, um zu verstehen, wie es funktioniert. Verwöhne das Rezept zum Backen eines Kuchens:
module Service class CookingPieWithabbage < LunaPark::Interactors::Sequence def call! check_products_availability make_cabbage_filler make_pie bake end private def check_products_availability Services::CheckProductsAvailability.call list: ingredients end
Verwenden Sie den Aktionsaufruf direkt im Klassenzimmer. Aus der Sicht von Ruby mag dieser Ansatz ungewöhnlich erscheinen, sodass er besser lesbar erscheint:
class DrivingStart < LunaPark::Interactors::Sequence def call! Service::CheckEngine.call Service::StartUpTheIgnition.call car, with: key Service::ChangeGear.call car.gear_box, to: :drive Service::StepOnTheGas.call car.pedals[:right] end end
2. Verwenden Sie nach Möglichkeit die Aufrufklassenmethode
3. Erstellen Sie keine funktionalen Objekte, um den Code einzugeben. Sehen Sie sich die Situation an
Serviceschicht

Der Interactor beschreibt, wie gesagt, die GeschĂ€ftslogik auf höchstem Niveau. Die Serviceschicht (Serviceschicht) zeigt bereits die Details der Implementierung funktionaler Anforderungen. Wenn wir ĂŒber die Herstellung eines Kuchens sprechen, sagen wir auf der Ebene des Interactor einfach âKnete den Teigâ, ohne auf Einzelheiten darĂŒber einzugehen, wie man ihn knetet. Der Knetvorgang wird auf Serviceebene beschrieben . Kehren wir zur ursprĂŒnglichen Quelle zurĂŒck, dem groĂen blauen Buch :
In der angewendeten DomĂ€ne gibt es Operationen, die keinen natĂŒrlichen Ort in einem Objekt vom Typ EntitĂ€t oder Wertobjekt finden können. Sie sind von Natur aus keine Objekte, sondern AktivitĂ€ten. Da die Grundlage unseres Modellierungsparadigmas jedoch der Objektansatz ist, werden wir versuchen, sie in Objekte umzuwandeln.
An dieser Stelle ist es leicht, einen hĂ€ufigen Fehler zu machen: den Versuch aufzugeben, die Operation in einem geeigneten Objekt dafĂŒr zu platzieren, und so zur prozeduralen Programmierung zu gelangen. Wenn Sie jedoch eine Operation mit Gewalt in ein Objekt mit einer ihm fremden Definition einfĂŒgen, verliert das Objekt selbst seine Reinheit, was das VerstĂ€ndnis und die Umgestaltung erschwert. Wenn Sie viele komplexe VorgĂ€nge in einem einfachen Objekt implementieren, kann dies zu einem unverstĂ€ndlichen Ergebnis fĂŒhren. Was Sie tun, ist nicht klar, was. Solche Operationen betreffen oft andere Objekte des Themenbereichs, und die Koordination zwischen ihnen wird durchgefĂŒhrt, um eine gemeinsame Aufgabe auszufĂŒhren. ZusĂ€tzliche Verantwortung schafft AbhĂ€ngigkeitsketten zwischen Objekten und mischt Konzepte, die unabhĂ€ngig voneinander betrachtet werden können.
Verwenden Sie bei der Auswahl eines Standorts fĂŒr die Implementierung einer Funktion immer den gesunden Menschenverstand. Ihre Aufgabe ist es, das Modell ausdrucksvoller zu machen. Schauen wir uns ein Beispiel an: "Wir mĂŒssen Holz hacken":
module Entities class Wood def chop
Diese Methode wird ein Fehler sein. Brennholz wird sich nicht selbst hacken, wir brauchen eine Axt:
module Entities class Axe def chop(sacrifice)
Wenn wir ein vereinfachtes GeschĂ€ftsmodell verwenden, reicht dies aus. Wenn der Prozess jedoch detaillierter modelliert werden muss, benötigen wir eine Person, die dieses Brennholz hackt, und möglicherweise ein Protokoll, das als StĂ€nder fĂŒr den Prozess verwendet wird.
module Entities class Human def chop_firewood(wood, axe, chock)
Wie Sie wahrscheinlich bereits vermutet haben, ist dies keine gute Idee. Nicht alle von uns sind mit dem Schneiden von Holz beschĂ€ftigt, dies ist keine direkte Pflicht einer Person. Wir sehen oft, wie ĂŒberlastet die Modelle in Ruby on Rails sind, die eine Ă€hnliche Logik enthalten: Rabatte erhalten, Waren in den Warenkorb legen, Geld auf den Kontostand abheben. Diese Logik gilt nicht fĂŒr die EntitĂ€t, sondern fĂŒr den Prozess, an dem diese EntitĂ€t beteiligt ist.
module Services class ChopFirewood
Nachdem wir herausgefunden haben, welche Logik wir in den Diensten speichern , werden wir versuchen, eine davon zu implementieren. In den meisten FĂ€llen werden Dienste ĂŒber Methoden oder Funktionsobjekte implementiert.
Funktionsobjekte
Ein Funktionsobjekt erfĂŒllt eine Funktionsanforderung. In seiner primitivsten Form hat ein funktionales Objekt eine einzige öffentliche Methode - den call
.
module Serivices class Sum def initialize(x, y) @x = x @y = y end def call x + y end def self.call(x,y) new(x,y).call end private attr_reader :x, :y end end
Solche Objekte haben mehrere Vorteile: Sie sind prĂ€zise und sehr einfach zu testen. Es gibt einen Nachteil, solche Objekte können sich als groĂe Anzahl herausstellen. Es gibt verschiedene Möglichkeiten, Ă€hnliche Objekte zu gruppieren. In einem Teil unserer Projekte teilen wir sie nach Typ:
- Serviceobjekt (Service) - Ein Objekt, das ein neues Objekt erstellt.
- Befehl (Befehl) - Ăndert das aktuelle Objekt.
- Guardian (Guard) - Gibt einen Fehler zurĂŒck, wenn ein Fehler aufgetreten ist.
Serviceobjekt
In unserer Implementierung implementiert Service - eine funktionale Anforderung und gibt immer einen Wert zurĂŒck.
module KorovaMilkBar module Services class FindMilk < LunaPark::Callable GLASS_SIZE = Values::Unit.wrap '200g' def initialize(fridge:) @fridge = fridge end def call fridge.shelfs.find { |shelf| shelf.has?(GLASS_SIZE, of: :milk) } end private attr_reader :fridge end end end FindMilk.call(fridge: the_red_one)
Befehl
In unserer Implementierung fĂŒhrt Command - eine Aktion aus und Ă€ndert das Objekt, wenn true true zurĂŒckgibt. TatsĂ€chlich erstellt das Team kein Objekt, sondern Ă€ndert ein vorhandenes.
module KorovaMilkBar module Commands class FillGlass < LunaPark::Callable def initialize(glass, with:) @glass = glass @content = with end def call glass << content true end private attr_reader :fridge end end end glass = Glass.empty milk = Milk.new(200, :gr) glass.empty?
WĂ€chter (Wache)
Der Watchman fĂŒhrt eine logische PrĂŒfung durch und gibt im Fehlerfall einen Verarbeitungsfehler aus. Diese Art von Objekt wirkt sich in keiner Weise auf die Hauptrichtung aus, sondern wechselt in die alternative Richtung, wenn etwas schief geht.
Beim Servieren von Milch ist darauf zu achten, dass sie frisch ist:
module KorovaMilkBar module Guards class IsFresh < LunaPark::Callable def initialize(product) @products = products end def call products.each do |product| raise Errors::Rotten, "
Möglicherweise ist es zweckmĂ€Ăig, Funktionsobjekte nach Typ zu trennen. Sie können beispielsweise Ihren eigenen Builder hinzufĂŒgen - erstellt ein Objekt basierend auf Parametern.
Arrangements
- Die
call
ist die einzige obligatorische öffentliche Methode. - Die
initialize
ist die einzige optionale öffentliche Methode. - Der Rest der Methoden sollte privat sein.
- Logische Fehler mĂŒssen von der Klasse
LunaPark::Errors::Processing
geerbt werden.
Fehlerbehandlung
Es gibt zwei Arten von Fehlern, die wĂ€hrend der AusfĂŒhrung einer Aktion auftreten können .
Laufzeitfehler
Solche Fehler können infolge einer Verletzung der Verarbeitungslogik auftreten.
Zum Beispiel:
- beim Erstellen eines Benutzers ist E-Mail reserviert;
- Wenn Sie versuchen, Milch zu trinken, ist es vorbei.
- Ein anderer Microservice lehnte die Aktion ab (aus einem logischen Grund und nicht, weil der Service nicht verfĂŒgbar ist).
Höchstwahrscheinlich möchte der Benutzer ĂŒber diese Fehler informiert werden. Auch das sind wahrscheinlich die Fehler
was wir vorhersehen können.
Solche Fehler sollten von LunaPark::Errors::Processing
geerbt werden
Systemfehler
Fehler, die infolge eines Systemabsturzes aufgetreten sind.
Zum Beispiel:
- Die Datenbank funktioniert nicht.
- etwas geteilt durch Null.
Höchstwahrscheinlich können wir diese Fehler nicht vorhersehen und dem Benutzer nichts sagen, auĂer dass alles sehr schlecht ist, und den Entwicklern einen Bericht senden, in dem MaĂnahmen gefordert werden. Solche Fehler sollten von SystemError
geerbt SystemError
Es gibt auch Validierungsfehler , auf die wir im nÀchsten Artikel nÀher eingehen werden.
Ăbungsgebrauch
1. Verwenden Sie Variablen, um die Lesbarkeit zu verbessern
module Fishing
2. Ăbergeben Sie Objekte, keine Parameter
Versuchen Sie, den Initialisierer einfach zu gestalten, wenn die Parameterverarbeitung nicht den Zweck hat.
Ăbergeben Sie Objekte, keine Parameter.
module Service
3. Verwenden Sie den Namen Actions - das Verb der Aktion und das Objekt des Einflusses.
4. Verwenden Sie nach Möglichkeit die Aufrufklassenmethode
Normalerweise eine Instanz der Actions- Klasse, die nur selten zum Schreiben verwendet wird, um einen Anruf zu tÀtigen.
5. Die Fehlerbehandlung ist keine Serviceaufgabe
Module
Bis zu diesem Moment haben wir die Implementierung der Service-Schicht als eine Reihe von Funktionsobjekten betrachtet. Aber wir können leicht Methoden auf dieser Ebene platzieren:
module Services def sum(a, b) a + b end end
Ein weiteres Problem, mit dem wir konfrontiert sind, ist eine groĂe Anzahl von Serviceeinrichtungen. Anstelle des "Rails Fat Model", das wir auf den neuesten Stand gebracht haben, erhalten wir den "Services Fat Folder". Es gibt verschiedene Möglichkeiten, die Struktur zu organisieren, um das AusmaĂ der Tragödie zu verringern. Eric Evans löst dieses Problem, indem er eine Reihe von Funktionen in einer Klasse kombiniert. Stellen Sie sich vor, wir mĂŒssen die GeschĂ€ftsprozesse des KindermĂ€dchens Arina Rodionovna modellieren, sie kann Puschkin fĂŒttern und ihn ins Bett bringen:
class NoonService def initialize(arina_radionovna, pushkin)
Dieser Ansatz ist aus Sicht von OOP korrekter. Wir empfehlen jedoch, es zumindest in der Anfangsphase aufzugeben. Nicht sehr erfahrene Programmierer beginnen in dieser Klasse viel Code zu schreiben, was letztendlich zu einer erhöhten KonnektivitĂ€t fĂŒhrt. Stattdessen können Sie das Modul verwenden, das die AktivitĂ€t als eine Abstraktion darstellt:
module Services module Noon class ToFeed def call!
Bei der Aufteilung in Module sollte eine niedrige externe Kopplung (niedrige Kopplung) mit hoher interner KohĂ€sion (hohe KohĂ€sion) beobachtet werden. Wir verwenden jedoch Module wie Services oder Interactors. Dies widerspricht auch den Vorstellungen einer reinen Architektur. Dies ist eine bewusste Entscheidung, die die Wahrnehmung erleichtert. Unter dem Namen der Datei verstehen wir, welches Entwurfsmuster diese oder jene Klasse implementiert. Wenn dies fĂŒr einen erfahrenen Programmierer offensichtlich ist, ist dies fĂŒr einen AnfĂ€nger nicht immer der Fall. Wenn Ihr Team bereit ist, verwerfen Sie diesen Ăberschuss.
Um einen weiteren kleinen Auszug aus dem groĂen blauen Buch zu zitieren:
WĂ€hlen Sie Module aus, die den Verlauf des Systems beschreiben und zusammenhĂ€ngende KonzeptsĂ€tze enthalten. Daraus ergibt sich oft eine geringe AbhĂ€ngigkeit der Module voneinander. Ist dies jedoch nicht der Fall, suchen Sie nach einer Möglichkeit, das Modell so zu Ă€ndern, dass die Konzepte voneinander getrennt werden, oder suchen Sie nach dem im Modell fehlenden Konzept, das die Grundlage fĂŒr das Modul bilden und die Elemente des Modells auf natĂŒrliche und sinnvolle Weise zusammenfĂŒhren könnte. Erzielen Sie eine geringe AbhĂ€ngigkeit der Module voneinander in dem Sinne, dass Konzepte in verschiedenen Modulen unabhĂ€ngig voneinander analysiert und wahrgenommen werden können. Verfeinern Sie das Modell, bis darin natĂŒrliche Grenzen gemÀà den ĂŒbergeordneten Konzepten des Themenbereichs angezeigt werden und der entsprechende Code nicht entsprechend unterteilt ist.
Geben Sie die Modulnamen an, die in der UNIFIED LANGUAGE enthalten sein sollen. Sowohl die MODULE selbst als auch ihre Namen sollten das Wissen und das VerstÀndnis des Themenbereichs widerspiegeln.
Das Thema Module ist groĂ und interessant, geht aber deutlich ĂŒber das Thema dieses Artikels hinaus. NĂ€chstes Mal werden wir mit Ihnen ĂŒber Repositories und Adapter sprechen. Wir haben einen gemĂŒtlichen Telegrammkanal eröffnet, in dem wir Materialien zum Thema DDD austauschen möchten. Wir freuen uns ĂŒber Ihre Fragen und Ihr Feedback.