Freigeben über


Entwerfen eines DDD-orientierten Microservice

Tipp

Dieser Inhalt ist ein Auszug aus dem eBook .NET Microservices Architecture for Containerized .NET Applications, verfügbar auf .NET Docs oder als kostenlose herunterladbare PDF, die offline gelesen werden kann.

.NET Microservices-Architektur für containerisierte .NET-Anwendungen eBook-Cover-Thumbnail.

Domänengesteuertes Design (DDD) befürwortet die Modellierung basierend auf der Realität des Unternehmens, die für Ihre Anwendungsfälle relevant ist. Im Kontext der Erstellung von Anwendungen spricht DDD über Probleme als Domänen. Es beschreibt unabhängige Problembereiche als Gebundene Kontexte (jeder gebundene Kontext korreliert mit einem Mikroservice), und betont eine gemeinsame Sprache, um über diese Probleme zu sprechen. Es schlägt auch viele technische Konzepte und Muster vor, z. B. Domänenentitäten mit umfangreichen Modellen (kein Anemic-Domain-Modell), Wertobjekte, Aggregate und Aggregatstammregeln (oder Stammentitätsregeln), um die interne Implementierung zu unterstützen. In diesem Abschnitt wird der Entwurf und die Implementierung dieser internen Muster vorgestellt.

Manchmal werden diese technischen DDD-Regeln und -Muster als Hindernisse wahrgenommen, die eine steile Lernkurve für die Implementierung von DDD-Ansätzen aufweisen. Der wichtige Teil ist jedoch nicht die Muster selbst, sondern das Organisieren des Codes, sodass er an die Geschäftsprobleme ausgerichtet ist und die gleichen Geschäftsbegriffe verwendet (allgegenwärtige Sprache). Darüber hinaus sollten DDD-Ansätze nur angewendet werden, wenn Sie komplexe Microservices mit erheblichen Geschäftsregeln implementieren. Einfachere Zuständigkeiten wie ein CRUD-Dienst können mit einfacheren Ansätzen verwaltet werden.

Wo die Grenzen gezeichnet werden sollen, ist die wichtigste Aufgabe beim Entwerfen und Definieren eines Microservice. DDD-Muster helfen Ihnen, die Komplexität in der Domäne zu verstehen. Für das Domänenmodell für jeden gebundenen Kontext identifizieren und definieren Sie die Entitäten, Wertobjekte und Aggregate, die Ihre Domäne modellieren. Sie erstellen und verfeinern ein Domänenmodell, das in einer Grenze enthalten ist, die Ihren Kontext definiert. Das wird bei der Form eines Microservice deutlich. Die Komponenten innerhalb dieser Grenzen sind Ihre Microservices, obwohl in einigen Fällen ein BC- oder Business-Microservices aus mehreren physischen Diensten bestehen kann. DDD befasst sich mit Grenzen, ebenso wie Microservices.

Halten Sie die Microservice-Kontextgrenzen relativ klein

Wenn Sie bestimmen, wo Grenzen zwischen gebundenen Kontexten platziert werden sollen, werden zwei konkurrierende Ziele ausgeglichen. Zunächst möchten Sie zunächst die kleinsten möglichen Microservices erstellen, obwohl dies nicht der Haupttreiber sein sollte; Sie sollten eine Grenze für Dinge schaffen, die Zusammenhalt benötigen. Zudem sollten umfangreiche Kommunikationen zwischen Microservices vermieden werden. Diese Ziele können einander widersprechen. Sie sollten sie ausgleichen, indem Sie das System in so viele kleine Microservices wie möglich zerlegen, bis Sie sehen, dass die Kommunikationsgrenzen mit jedem zusätzlichen Versuch, einen neuen gebundenen Kontext zu trennen, schnell wachsen. Der Zusammenhalt ist ein Schlüssel innerhalb eines einzigen gebundenen Kontexts.

Dies ist vergleichbar mit dem unangemessenen Code-Smell „Intimacy“ bei der Implementierung von Klassen. Wenn zwei Microservices viel miteinander zusammenarbeiten müssen, sollten sie wahrscheinlich derselbe Microservice sein.

Eine weitere Möglichkeit, diesen Aspekt zu betrachten, ist Autonomie. Wenn sich ein Microservice auf einen anderen Dienst verlassen muss, um eine Anfrage direkt zu bedienen, ist er nicht wirklich autonom.

Ebenen in DDD-Microservices

Die meisten Unternehmensanwendungen mit erheblicher geschäftlicher und technischer Komplexität werden durch mehrere Ebenen definiert. Die Ebenen sind ein logisches Artefakt und beziehen sich nicht auf die Bereitstellung des Diensts. Sie sind vorhanden, damit Entwickler die Komplexität im Code verwalten können. Unterschiedliche Schichten (z. B. die Domänenmodellschicht im Vergleich zur Darstellungsschicht usw.) weisen möglicherweise unterschiedliche Typen auf, wodurch Übersetzungen zwischen diesen Typen erforderlich werden.

Beispielsweise könnte eine Entität aus der Datenbank geladen werden. Anschließend kann ein Teil dieser Informationen oder eine Aggregation von Informationen, einschließlich zusätzlicher Daten aus anderen Entitäten, über eine REST-Web-API an die Clientbenutzeroberfläche gesendet werden. Der Punkt hier ist, dass die Domänenentität in der Domänenmodellebene enthalten ist und nicht an andere Bereiche weitergegeben werden sollte, zu denen sie nicht gehört, z. B. zur Präsentationsebene.

Darüber hinaus müssen Sie über immer gültige Instanzen verfügen (siehe den Abschnitt "Entwerfen von Validierungen in der Domänenmodellebene") gesteuert durch Aggregatwurzeln (Stammentitäten). Daher sollten Entitäten nicht an Clientansichten gebunden werden, da einige Daten auf Ui-Ebene möglicherweise noch nicht überprüft werden. Dieser Grund ist das, wofür das ViewModel steht. Das ViewModel ist ein Datenmodell, das ausschließlich für Präsentationsebenenanforderungen verwendet wird. Die Domänenentitäten gehören nicht direkt zum ViewModel. Stattdessen müssen Sie zwischen ViewModels und Domänenentitäten übersetzen und umgekehrt.

Bei der Bewältigung der Komplexität ist es wichtig, dass ein Domänenmodell durch aggregierte Wurzeln gesteuert wird, die sicherstellen, dass alle Invarianten und Regeln, die mit dieser Gruppe von Entitäten (Aggregat) zusammenhängen, über einen einzelnen Einstiegspunkt oder ein Aggregatstamm durchgeführt werden.

Abbildung 7-5 zeigt, wie ein layeriertes Design in der eShopOnContainers-Anwendung implementiert wird.

Diagramm, das die Ebenen in einem domänengesteuerten Design-Microservice zeigt.

Abbildung 7-5. DDD-Ebenen für den Microservice für Bestellungen in eShopOnContainers

Die drei Ebenen in einem DDD-Mikroservice wie "Order". Jede Ebene ist ein VS-Projekt: Anwendungsschicht ist "Ordering.API", "Domain layer" ist "Order.Domain" und "Infrastructure layer" ist "Ordering.Infrastructure". Sie möchten das System so entwerfen, dass jede Ebene nur mit bestimmten anderen Ebenen kommuniziert. Dieser Ansatz ist möglicherweise einfacher zu erzwingen, wenn Layer als unterschiedliche Klassenbibliotheken implementiert werden, da Sie klar erkennen können, welche Abhängigkeiten zwischen Bibliotheken festgelegt sind. Auf der Domänenmodellebene sollte beispielsweise keine Abhängigkeit für eine andere Ebene ausgewählt werden (bei den Domänenmodellklassen sollte es sich um Plain Old Class Objects- bzw. um POCO-Klassen handeln). Wie in Abbildung 7-6 dargestellt, weist die Bibliothek "Ordering.Domain" nur Abhängigkeiten von den .NET-Bibliotheken oder NuGet-Paketen auf, aber nicht von einer anderen benutzerdefinierten Bibliothek wie etwa einer Daten- oder Persistenzbibliothek.

Screenshot der Abhängigkeiten von

Abbildung 7-6. Ebenen, die als Bibliotheken implementiert werden, ermöglichen eine bessere Kontrolle der Abhängigkeiten zwischen Ebenen

Die Domänenmodellebene

Eric Evanss hervorragendes Buch Domain Driven Design sagt Folgendes über die Domänenmodellschicht und die Anwendungsschicht.

Domänenschicht: Verantwortlich für die Darstellung von Unternehmenskonzepten, Informationen über die Geschäftssituation und Geschäftsregeln. Der Zustand, der die Geschäftslage widerspiegelt, wird hier kontrolliert und genutzt, auch wenn die technischen Details der Speicherung an die Infrastruktur delegiert sind. Diese Ebene ist das Herzstück der Geschäftssoftware.

Auf der Domänenmodellebene wird das Geschäft zum Ausdruck gebracht. Wenn Sie eine Microservice-Domänenmodellebene in .NET implementieren, wird diese Ebene als Klassenbibliothek mit den Domänenentitäten codiert, die Daten plus Verhalten erfassen (Methoden mit Logik).

Nach den Prinzipien der Persistenz-Unwissenheit und der Prinzipien der Infrastruktur-Unwissenheit muss diese Ebene Datenpersistenzdetails vollständig ignorieren. Diese Persistenzaufgaben sollten von der Infrastrukturebene ausgeführt werden. Daher sollte diese Ebene keine direkten Abhängigkeiten von der Infrastruktur übernehmen, was bedeutet, dass ihre Domänenmodellentitätsklassen POCOs sein sollten.

Domänenentitäten sollten keine direkte Abhängigkeit (z. B. die Ableitung von einer Basisklasse) zu jedem Datenzugriffsinfrastrukturframework wie Entity Framework oder NHibernate aufweisen. Im Idealfall sollten Ihre Domänenentitäten nicht von einem in einem Infrastrukturframework definierten Typ abgeleitet oder implementiert werden.

Die meisten modernen ORM-Frameworks wie Entity Framework Core ermöglichen diesen Ansatz, sodass Ihre Domänenmodellklassen nicht mit der Infrastruktur gekoppelt sind. Das Vorhandensein von POCO-Entitäten ist jedoch nicht immer möglich, wenn bestimmte NoSQL-Datenbanken und Frameworks verwendet werden, z. B. Akteure und zuverlässige Sammlungen in Azure Service Fabric.

Auch wenn es wichtig ist, dem Persistenz-Unwissenheitsprinzip für Ihr Domänenmodell zu folgen, sollten Sie Persistenzbedenken nicht ignorieren. Es ist weiterhin wichtig, das physische Datenmodell und die Zuordnung zum Entitätsobjektmodell zu verstehen. Andernfalls können Sie unmögliche Designs erstellen.

Außerdem bedeutet dieser Aspekt nicht, dass Sie ein Modell für eine relationale Datenbank verwenden und direkt in eine NoSQL- oder dokumentorientierte Datenbank verschieben können. In einigen Entitätsmodellen kann das Modell passen, in der Regel aber nicht. Es gibt weiterhin Einschränkungen, die ihr Entitätsmodell einhalten muss, basierend auf der Speichertechnologie und DER ORM-Technologie.

Die Anwendungsschicht

Wenn wir auf die Anwendungsschicht umsteigen, können wir wieder Eric Evanss Buch Domain Driven Design zitieren:

Anwendungsschicht: Definiert die Aufgaben, die die Software ausführen soll, und leitet die ausdrucksstarken Domänenobjekte an, um Probleme zu lösen. Die Aufgaben, für die diese Ebene verantwortlich ist, sind für das Unternehmen sinnvoll oder für die Interaktion mit den Anwendungsebenen anderer Systeme erforderlich. Diese Schicht wird dünn gehalten. Es enthält keine Geschäftsregeln oder Kenntnisse, sondern koordiniert nur Aufgaben und delegiert Arbeiten an Zusammenarbeiten von Domänenobjekten in der darunterliegenden Ebene. Sie verfügt nicht über einen Zustand, der die Geschäftssituation widerspiegelt, aber es kann einen Zustand haben, der den Fortschritt einer Aufgabe für den Benutzer oder das Programm widerspiegelt.

Die Anwendungsschicht eines Microservice in .NET wird häufig als ASP.NET Core Web API-Projekt codiert. Das Projekt implementiert die Interaktion der Microservices, den Fernzugriff auf Netzwerke sowie die externen Web-APIs, die von der Benutzeroberfläche oder Client-Anwendungen genutzt werden. Es enthält Abfragen, wenn ein CQRS-Ansatz verwendet wird, Befehle, die vom Microservice akzeptiert werden, und sogar die ereignisgesteuerte Kommunikation zwischen Mikroservices (Integrationsereignisse). Die ASP.NET Core Web-API, die die Anwendungsebene darstellt, darf keine Geschäftsregeln oder Domänenkenntnisse enthalten (insbesondere Domänenregeln für Transaktionen oder Updates); diese sollten der Domänenmodellklassenbibliothek gehören. Die Anwendungsschicht darf nur Aufgaben koordinieren und darf keinen Domänenstatus (Domänenmodell) enthalten oder definieren. Er delegiert die Ausführung von Geschäftsregeln an die Domänenmodellklassen selbst (aggregierte Wurzeln und Domänenentitäten), wodurch die Daten in diesen Domänenentitäten letztendlich aktualisiert werden.

Im Grunde ist die Anwendungslogik der Ort, an dem Sie alle Anwendungsfälle implementieren, die von einem bestimmten Front-End abhängen. Beispielsweise die Implementierung im Zusammenhang mit einem Web-API-Dienst.

Ziel ist, dass die Domänenlogik in der Domänenmodellebene, deren Invarianten, das Datenmodell und verwandte Geschäftsregeln vollständig unabhängig von den Präsentations- und Anwendungsebenen sein müssen. Vor allem darf die Domänenmodellebene nicht direkt von einem Infrastrukturframework abhängig sein.

Die Infrastrukturebene

Auf der Infrastrukturebene werden die Daten, die anfänglich in Domänenentitäten (im Arbeitsspeicher) gespeichert werden, in Datenbanken oder einem anderen beständigen Speicher gespeichert. Ein Beispiel ist die Verwendung von Entity Framework Core-Code zum Implementieren der Repositorymusterklassen, die einen DBContext zum Speichern von Daten in einer relationalen Datenbank verwenden.

Gemäß den oben genannten Grundsätzen Ignorieren der Persistenz und Ignorieren der Infrastruktur darf die Infrastrukturebene die Domänenmodellebene nicht „kontaminieren“. Sie müssen die Domänenmodellentitätsklassen unabhängig von der Infrastruktur beibehalten, die Sie zum Speichern von Daten (EF oder einem anderen Framework) verwenden, indem Sie keine harten Abhängigkeiten von Frameworks verwenden. Ihre Domänenmodellklassenbibliothek sollte nur Ihren Domänencode haben, nur POCO-Entitätsklassen, die das Herzstück Ihrer Software implementieren und vollständig von Infrastrukturtechnologien entkoppelt werden.

Daher sollten Ihre Schichten oder Klassenbibliotheken und Projekte letztendlich von Ihrer Domänenmodell-Schicht (Bibliothek) abhängen, nicht umgekehrt, wie in Abbildung 7-7 dargestellt.

Diagramm mit Abhängigkeiten, die zwischen DDD-Dienstebenen vorhanden sind.

Abbildung 7-7. Abhängigkeiten zwischen Ebenen in DDD

Abhängigkeiten in einem DDD-Dienst: Die Anwendungsschicht hängt von der Domäne und der Infrastruktur ab, und die Infrastruktur hängt von der Domäne ab, aber die Domäne hängt von keiner Ebene ab. Dieses Layerdesign sollte für jeden Microservice unabhängig sein. Wie bereits erwähnt, können Sie die komplexesten Microservices nach DDD-Mustern implementieren, während Sie einfachere datengesteuerte Microservices (einfache CRUD in einer einzigen Ebene) auf einfachere Weise implementieren.

Weitere Ressourcen