Es sollte nicht innovativ sein zu sagen, dass es beim Schreiben von Software nicht nur um das Schreiben von Code geht, sondern um die Lösung eines bestimmten Problems. Auch wenn es letztendlich die Entwickler sind, die die Lösung implementieren, sind es nicht die Entwickler, die das Problem überhaupt definieren. Diese Aufgabe wird von verschiedenen Geschäftsleuten ausgeführt, die Prozesse, Risiken und Ergebnisse berücksichtigen, um zu beschreiben, was das Problem ist, warum es besteht und wie es angegangen werden sollte. In einem domänengesteuerten Kontext werden diese Geschäftsleute als Domänenexperten bezeichnet.
Aus technischer Sicht scheinen Domänenexperten über ein wertvolles Gut zu verfügen: ihr Wissen über die Domäne. Allerdings wird dieses Wissen selten in seiner Rohform weitergegeben. Stattdessen werden sie meist in Anforderungen übersetzt, damit Entwickler sie verstehen und umsetzen können. Das Problem bei diesem Ansatz besteht darin, dass das Fachwissen von Geschäftsleuten und Entwicklern unterschiedlich sein kann. Dies bedeutet, dass die Perspektiven derjenigen, die das Problem definieren, und derjenigen, die an seiner Lösung arbeiten, möglicherweise nicht übereinstimmen, was zu Missverständnissen und Konflikten führt.
Also, was ist der Ausweg? Stellen Sie sicher, dass Geschäftsleute und Techniker die gleiche Sprache und Terminologie verwenden.
Domain-Driven Design (DDD) ist eine Methodik , die die Bedeutung der Schaffung eines gemeinsamen Verständnisses zwischen Domänenexperten und technischen Stakeholdern und der Ausrichtung der Softwarelösung auf die zugrunde liegenden Geschäftsanforderungen betont. Dies scheint eine allgemeine, nicht-technische Definition zu sein, kann aber auch in eine entwicklerfreundlichere Definition unterteilt werden:
DDD repräsentiert reale Konzepte im Code und seine Struktur durch die Kultivierung und Verwendung einer allgegenwärtigen Sprache, die durch Modellierung der Geschäftsdomäne erstellt wird.
Es müssen noch einige Terminologien eingeführt werden, sodass diese derzeit möglicherweise nicht 100 % klar sind. Am wichtigsten ist, dass DDD Tools und Aktivitäten bereitstellt, die das Schreiben und Strukturieren von Code ermöglichen, der auf die Geschäftsvision abgestimmt ist. Es geht dann nicht nur um Kommunikation, sondern auch darum, gestalterische Entscheidungen zu treffen, die tatsächlich die gemeinsame Sprache prägen.
Es sollte nicht überraschen, dass der wichtigste Begriff in der DDD-Welt die Domain ist. So beschreibt es Vlad Khononov, Autor von „Learning Domain-Driven Design“:
Eine Geschäftsdomäne definiert den Haupttätigkeitsbereich eines Unternehmens.
Dies bedeutet, dass die Geschäftsdomäne auch als Folgendes betrachtet werden könnte:
Die Domäne kann auch in Subdomänen – spezifischere Tätigkeitsbereiche – unterteilt werden. Obwohl es drei verschiedene Arten von Subdomains gibt, ist die Kerndomäne die wichtigste. Es beschreibt, wie das Unternehmen den Geschäftsvorteil erzielt. Bei den anderen beiden geht es um allgemeinere Probleme wie Authentifizierungssysteme oder interne Admin-Panels.
Ein tiefes Verständnis der Geschäftsdomäne eines Unternehmens ist absolut entscheidend, um die Vorteile von Domain-Driven Design voll auszuschöpfen. Die beste Quelle für dieses Verständnis sind niemand anderes als die Domain-Experten . Dies sind die Personen, deren Problem mit der Software angegangen wird – Stakeholder, verschiedene Geschäftsleute und sogar Benutzer. Das heißt nicht, dass Ingenieure nicht über den Bereich, an dem sie arbeiten, informiert sind, sondern vielmehr, dass die Experten die Quelle der Wahrheit des Fachwissens sind. Durch die Zusammenarbeit mit den Domänenexperten können Entwickler sicherstellen, dass die Modelle der Domäne korrekt und aktuell bleiben.
Dies führt zu einem weiteren kritischen, aber möglicherweise mehrdeutigen Begriff: Modell . Eric Evans beschreibt das Modell in seinem Buch über das DDD wie folgt:
Es handelt sich um eine Interpretation der Realität, die die für die Lösung des vorliegenden Problems relevanten Aspekte abstrahiert und überflüssige Details ignoriert.
Vlad Khononov erklärt diese Idee weiter in verständlicheren Worten:
Ein Modell ist keine Kopie der realen Welt, sondern ein menschliches Konstrukt, das uns hilft, reale Systeme zu verstehen.
Zusammenfassend lässt sich sagen, dass ein Modell eine Darstellung eines Geschäftskonzepts oder -prozesses ist, die das Verständnis und die Kommunikation der zugrunde liegenden Komplexität der Domäne erleichtert.
Vlad nutzte eine Karte, um das Konzept eines Domänenmodells effektiv zu veranschaulichen. Karten sind ein perfektes Beispiel dafür, dass sie nur Informationen anzeigen, die für den Kartentyp relevant sind, etwa Topografie, Straßen oder Grenzen. Eine Karte, die alle Details auf einmal anzeigt, wäre überwältigend und praktisch nutzlos. Domänenmodelle gibt es auch in anderen Formen, wie zum Beispiel:
Das letzte Teil des Domain-Driven Design (DDD)-Terminologiepuzzles ist die Ubiquitous Language. Es bezieht sich auf die gemeinsame Sprache, die sowohl von technischen als auch von geschäftlichen Beteiligten in einem Projekt verwendet wird. Bei DDD ist es von entscheidender Bedeutung, eine gemeinsame Sprache zur Beschreibung der aus dem Domänenmodell abgeleiteten Geschäftsdomäne zu haben. Dadurch wird sichergestellt, dass alle Teammitglieder ein klares Verständnis des Problembereichs, seiner Konzepte und ihrer Beziehungen haben. Dies führt zu einer besseren Abstimmung und verringert das Risiko von Missverständnissen. Durch die Verwendung von Ubiquitous Language kann die Softwarelösung die zugrunde liegenden Geschäftsanforderungen genau widerspiegeln, was sie zu einer entscheidenden Komponente von DDD macht.
Da der Großteil der Terminologie abgedeckt ist, sollte es einfacher sein, zu verstehen , was domänengesteuertes Design ist . Jetzt ist es an der Zeit, sich mit dem eigentlichen Wie zu befassen – den Bausteinen des DDD.
DDD-Bausteine dienen als Grundlage für die Erstellung eines effektiven und effizienten Domänenmodells . Vlad Chononov definiert das Domänenmodell wie folgt:
Ein Domänenmodell ist ein Objektmodell der Domäne, das sowohl Verhalten als auch Daten umfasst.
Das Domänenmodell besteht aus verschiedenen Bausteinen und Strukturen. Die wichtigsten sind:
Wertobjekte sind die grundlegendsten verfügbaren Bausteine. Dabei handelt es sich um Objekte, die durch eine Reihe von Attributen und Werten definiert werden. Sie haben keine eindeutige Kennung – ihre Werte definieren ihre Identität. Sie sind in dem Sinne unveränderlich, dass unterschiedliche Werte bereits ein unterschiedliches Wertobjekt darstellen. Beispiele für Wertobjekte sind:
So könnte ein einfaches Wertobjekt in Python implementiert werden:
from pydantic import BaseModel class Address(BaseModel): """Customer address.""" country: str city: str street: str house_number: str class Config: frozen = True
Das bedeutet, dass die Verwendung des Gleichheitsoperators ( ==
) zum Vergleich zweier Adressen nur dann True
zurückgibt, wenn beiden Objekten genau die gleichen Werte zugewiesen sind.
Entitäten sind die nächste Art von Bausteinen. Entitäten stellen einzelne Objekte in der Domäne mit einer eindeutigen Identität dar, beispielsweise eine Person oder einen Auftrag. Sie ähneln den Wertobjekten in der Art und Weise, wie sie auch die Daten speichern, aber ihre Attribute können und werden sich voraussichtlich ändern und benötigen daher eine eindeutige Kennung. Bestellungen und persönliche Informationen sind nur zwei einfache Instanzen der Entitäten:
import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.value_objects import Address class Person(BaseModel): """Personal data.""" id: uuid.UUID = Field(default_factory=uuid.uuid4) first_name: str last_name: str address: Address class Order(BaseModel): """Customer order.""" id: uuid.UUID = Field(default_factory=uuid.uuid4) description: str value: float
Da die Werte der Instanzen in beiden Fällen veränderbar sind, benötigen sie eine Identifikation, die eine UUID sein kann. Noch wichtiger ist, dass Entitäten in den meisten Fällen nicht direkt, sondern über ein Aggregat verwaltet werden sollen.
Ein Aggregat ist ein Entitätstyp, da es veränderbar ist und eine eindeutige Kennung erfordert. Seine Hauptaufgabe besteht jedoch nicht darin, Daten zu speichern, sondern eine Reihe verwandter Objekte (Entitäten und Wertobjekte) zu einer einzigen Konsistenzeinheit zusammenzufassen. Das Aggregat ist das Stammobjekt mit einer genau definierten Grenze, die seinen internen Zustand kapselt und Invarianten erzwingt, um die Konsistenz der gesamten Gruppe sicherzustellen. Aggregate ermöglichen eine natürlichere und intuitivere Schlussfolgerung über die Domäne, indem sie sich auf die Beziehungen zwischen Objekten und nicht auf die Objekte selbst konzentrieren.
In Anlehnung an die vorangegangenen Beispiele könnte ein Aggregat als Kunde dargestellt werden:
import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.entities import Person, Order from practical_ddd.building_blocks.value_objects import Address class Customer(BaseModel): """Customer aggregate. Manages personal information as well as orders. """ id: uuid.UUID = Field(default_factory=uuid.uuid4) person: Person orders: list[Order] = Field(default_factory=list) def change_address(self, new_address: Address) -> None: self.person.address = new_address def add_order(self, order: Order) -> None: if self.total_value + order.value > 10000: raise ValueError("Order cannot have value higher than 10000") self.orders.append(order) def remove_order(self, order_id: uuid.UUID) -> None: order = next((order for order in self.orders if order.id == order_id), None) if order is None: raise IndexError("Order not found") self.orders.remove(order) @property def total_value(self) -> float: return sum(order.value for order in self.orders)
Der Kunde ist direkt mit den persönlichen Daten verknüpft und speichert alle Bestellungen. Darüber hinaus bietet das Aggregat eine Schnittstelle zur Verwaltung der Adresse der Person sowie zum Hinzufügen und Entfernen von Bestellungen. Dies liegt daran, dass der Zustand des Aggregats nur durch die Ausführung der entsprechenden Methoden geändert werden kann.
Während das vorherige Beispiel relativ einfach ist und nur eine Einschränkung aufweist (der Bestellwert darf nicht größer als 10.000 sein), sollte es die Verwendung von DDD-Bausteinen und deren Beziehungen veranschaulichen. In tatsächlichen Systemen sind Aggregate oft komplexer, mit mehr Einschränkungen, Grenzen und möglicherweise mehr Beziehungen. Schließlich besteht ihre bloße Existenz darin, diese Komplexität zu bewältigen. Darüber hinaus werden Aggregate in der realen Welt typischerweise in einem Datenspeicher, beispielsweise einer Datenbank, gespeichert. Hier kommt das Repository- Muster ins Spiel.
Die Zustandsänderungen des Aggregats sollten alle transaktional in einer einzigen atomaren Operation festgeschrieben werden. Es liegt jedoch nicht in der Verantwortung des Aggregats, „auf sich selbst zu bestehen“. Das Repository- Muster ermöglicht es, die Details der Datenspeicherung und des Datenabrufs zu abstrahieren und stattdessen mit Aggregaten auf einer höheren Abstraktionsebene zu arbeiten. Einfach ausgedrückt kann ein Repository als Schicht zwischen Aggregat und Datenspeicher betrachtet werden. Eine JSON-Datei ist eine faire einfaches Beispiel für einen solchen Speicher. Das Kundenaggregat könnte ein Repository haben, das mit JSON-Dateien arbeitet:
import json import uuid from practical_ddd.building_blocks.aggregates import Customer class CustomerJSONRepository: """Customer repository operating on JSON files.""" def __init__(self, path: str) -> None: self.path = path def get(self, customer_id: uuid.UUID) -> Customer: with open(self.path, "r") as file: database = json.load(file) customer = database["customers"].get(str(customer_id)) if customer is None: raise IndexError("Customer not found") person = database["persons"][str(customer["person"])] orders = [database["orders"][order_id] for order_id in customer["orders"]] return Customer( id=customer["id"], person=person, orders=orders, ) def save(self, customer: Customer) -> None: with open(self.path, "r+") as file: database = json.load(file) # Save customer database["customers"][str(customer.id)] = { "id": customer.id, "person": customer.person.id, "orders": [o.id for o in customer.orders], } # Save person database["persons"][str(customer.person.id)] = customer.person.dict() # Save orders for order in customer.orders: database["orders"][str(order.id)] = order.dict() file.seek(0) json.dump(database, file, indent=4, default=str)
Natürlich könnte (und sollte) diese Klasse noch viel mehr können, aber sie ist nicht als perfektes, multifunktionales ORM gedacht. Es sollte einen Überblick über die Repository-Verantwortlichkeiten geben, die in diesem Fall die Speicherung und den Abruf des Kundenaggregats in der JSON-Datei umfassen. Es ist auch erwähnenswert, wie das Repository mit dem Aggregat verknüpfte Entitäten verarbeitet. Da personenbezogene Daten und Bestellungen eng mit dem Kundenlebenszyklus verknüpft sind, müssen sie genau dann verwaltet werden, wenn das Aggregat verarbeitet wird.
Ein weiterer zu berücksichtigender Fall besteht darin, dass eine Geschäftslogik einfach nicht in das Aggregat oder eine seiner Entitäten oder Wertobjekte passt. Dabei kann es sich um eine Logik handeln, die von mehreren Aggregaten oder dem Zustand des Datenspeichers abhängt. In solchen Fällen kann eine Struktur namens Domain Service hilfreich sein. Der Domänendienst muss in der Lage sein, Aggregate zu verwalten, beispielsweise mithilfe des Repositorys, und kann dann Domänenlogik speichern, die nicht zum Aggregat gehört. Beispielsweise benötigt ein Kunde möglicherweise Logik, um zu vermeiden, dass zu viele Bestellungen verloren gehen:
import uuid from typing import Protocol from practical_ddd.building_blocks.aggregates import Customer class CustomerRepository(Protocol): """Customer repository interface.""" def get(self, customer_id: uuid.UUID) -> Customer: ... def save(self, customer: Customer) -> None: ... class CustomerService: """Customer service.""" def __init__(self, repository: CustomerRepository) -> None: self.repository = repository def get_customer(self, customer_id: uuid.UUID) -> Customer | None: try: return self.repository.get(customer_id) except IndexError: return None def save_customer(self, customer: Customer) -> None: existing_customer = self.get_customer(customer.id) # If customer is already in the database and has more than 2 orders, # he cannot end up with half of them after a single save. if ( existing_customer is not None and len(existing_customer.orders) > 2 and len(customer.orders) < (len(existing_customer.orders) / 2) ): raise ValueError( "Customer cannot lose more than half of his orders upon single save!" ) self.repository.save(customer)
Aggregate kann nicht sicherstellen, dass sich sein Status von dem in der JSON-Datei unterscheidet, da es überhaupt keine Kenntnis von der JSON-Datei hat. Deshalb muss die Vergleichslogik im Domain Service enthalten sein. Es ist auch wichtig zu beachten, dass der Domänendienst mit Repository-Abstraktion arbeiten sollte. Dies macht es einfach, die konkrete Implementierung durch eine alternative Implementierung mithilfe der Abhängigkeitsinjektion auszutauschen.
Da nun alle Stücke abgedeckt sind, können sie nun als Arbeitsprogramm betrachtet werden:
import uuid from practical_ddd.building_blocks import aggregates, entities, value_objects from practical_ddd.database.repository import CustomerJSONRepository from practical_ddd.service import CustomerService # Initialize domain service with json repository srv = CustomerService(repository=CustomerJSONRepository("test.json")) # Create a new customer customer = aggregates.Customer( person=entities.Person( first_name="Peter", last_name="Tobias", address=value_objects.Address( country="Germany", city="Berlin", street="Postdamer Platz", house_number="2/3", ), ), ) srv.save_customer(customer) # Add orders to existing customer customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.add_order(entities.Order(description="Order 1", value=10)) customer.add_order(entities.Order(description="Order 2", value=210)) customer.add_order(entities.Order(description="Order 3", value=3210)) srv.save_customer(customer) # Remove orders from existing customer # If there are only 3 orders, it's gonna fail customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.remove_order(uuid.UUID("0f3c0a7f-67fd-4309-8ca2-d007ac003b69")) customer.remove_order(uuid.UUID("a4fd7648-4ea3-414a-a344-56082e00d2f9")) srv.save_customer(customer)
Alles hat seine Verantwortlichkeiten und Grenzen. Aggregate ist für die Verwaltung seiner Entitäten und Wertobjekte sowie für die Durchsetzung seiner Einschränkungen verantwortlich. Der Domänendienst verwendet das eingefügte JSON-Repository, um die Daten in der JSON-Datei beizubehalten und zusätzliche Domänengrenzen durchzusetzen. Letztendlich hat jede Komponente eine eigene Funktion und Bedeutung innerhalb des angegebenen Bereichs.
Domain-Driven Design ist ohne Zweifel eine komplexe Idee. Es bietet Praktiken, Muster und Tools, die Softwareteams dabei helfen, die schwierigsten Geschäftsprobleme zu bewältigen, indem sie einen starken Schwerpunkt auf den Geschäftsbereich legen. DDD ist jedoch mehr als nur eine Reihe von Bausteinen. Es handelt sich um eine Denkweise, die Zusammenarbeit und Kommunikation zwischen technischen und geschäftlichen Interessengruppen erfordert. Ein gemeinsames Verständnis der Domäne, ausgedrückt durch eine allgegenwärtige Sprache, ist entscheidend für den Erfolg eines DDD-Projekts. Bei guter Umsetzung kann DDD zu Software führen, die besser auf die Anforderungen des Unternehmens abgestimmt ist und komplexe Probleme effektiver lösen kann.
Dieser Artikel sollte nie so etwas wie „DDD: From Zero To Hero“ sein, sondern vielmehr als Einführung in das DDD-Universum dienen. Ich wollte die wichtigsten Konzepte des Domain-Driven Design auf sehr einfache und praktische Weise demonstrieren. Ich glaube, dass das Erlernen von Domain-Driven Design eine hervorragende Möglichkeit ist, die Programmierkenntnisse zu verbessern. Allerdings hört man nicht allzu oft davon – zumindest nicht so oft wie „11 Wahnsinnige JavaScript-Tipps und Tricks – ein Thread 🧵“.
Wenn Sie etwas davon interessant fanden, können Sie auf jeden Fall im Abschnitt „Quellen“ nach Büchern und Artikeln suchen, die mich überhaupt dazu inspiriert haben, diesen Artikel zu schreiben. Es gibt einige Konzepte, die ich nicht behandelt habe, weil ich dachte, sie würden den Rahmen dieser Einführung sprengen, aber es lohnt sich, sie genauer zu untersuchen:
Sie werden sie zweifellos in den unten aufgeführten Quellen finden.
Im Artikel verwendete Codebeispiele finden Sie hier: Link .
Domain-Driven Design lernen von Vlad Khononov. Ein erstaunliches Buch, das für mich eine große Inspirationsquelle war. Erklärt alle in diesem Artikel behandelten Konzepte ausführlicher.
Architekturmuster in Python von Harry Percival und Bob Gregory. Ich habe das Buch vor fast zwei Jahren gelesen und es hatte einen großen Einfluss auf mich als Entwickler. Beim Schreiben dieses Artikels bin ich darauf zurückgekommen, und es hat mir erneut geholfen.
DDD in Python von Przemysław Górecki. Ich habe diesen Blog gegen Ende des Schreibens des Artikels entdeckt, aber er hat mein Interesse geweckt, weil er so wahnsinnig professionell ist. Lustige Tatsache: Ich habe in derselben Firma wie Przemysław gearbeitet und war mir dessen überhaupt nicht bewusst.
Auch hier veröffentlicht.