Über unsMediaKontaktImpressum
Jens Seekamp 03. März 2020

Implementierung von Domain-Driven Design (DDD) mit JEE

Das Domain-Driven Design (DDD) wurde von E. Evans in seinem Buch "Domain-Driven Design: Tackling Complexity in the Heart of Software" (2004) eingeführt [1]. In der aktuellen Diskussion um Microservice-Architekturen fällt der Begriff des Domain-Driven Design oftmals in dem Moment, wenn es um den fachlichen Zuschnitt von Microservices geht. Und auch bei der strategischen Ausrichtung von komplexen Enterprise-Anwendungen spielt der DDD-Ansatz offensichtlich zunehmend eine wichtige Rolle.

Grundbegriffe des Domain-Driven Design

Zunächst rückt DDD mit drei Begriffen die eigentliche Fachlichkeit von Software in das Zentrum der Betrachtung.

Da ist zu aller erst die Domäne (domain) selbst. Dies ist der Ausschnitt der realen Welt, der Geschäftsbereich, das Aufgabenfeld usw. wofür ein Anwendungs-System realisiert wird. Als Fallbeispiel nutzen wir das Flight Information System (FIS). In dieser Domäne gibt es Aircrafts die viele Flights auf bestimmten Routes ausführen, es gibt Customer, die eine Booking für einen bestimmten Flight durchführen usw.

Um eine solche Domäne für die Softwareentwicklung greifbar zu machen, wird sie durch ein Domänen-Modell (domain model) in einer abstrahierten Form beschrieben. Zur Beschreibung von Domänen-Modellen sind grafische Software-Spezifikationssprachen wie insbesondere die Unified Modeling Language (UML) oder spezielle textuelle Software-Spezifikationssprachen in Form einer Domain Specific Language (DSL) geeignet. Das Domänen-Modell spezifiziert die Gesamtheit von Daten, Verhalten, Prozessen, Regeln, Ereignissen usw., welche die Domäne ausmachen.

Die implementierte Anwendung ist eine exakte Repräsentation des Domänen-Modells in Form von Source-Code. Damit dies gelingt, müssen für die Erstellung des Domänen-Modells und dessen Implementierung auf der einen Seite die Softwareentwickler und auf der anderen Seite die fachlichen Experten sehr eng und permanent zusammen arbeiten. Im Zuge dieser Zusammenarbeit entsteht nach und nach die Ubiquitous Language für die Domäne. Dies ist die allgegenwärtige fachliche Sprache, welche alle für die betrachtete Domäne relevanten Begriffe eindeutig definiert.

Das Resultat einer erfolgreichen Umsetzung des DDD-Ansatzes ist ein Anwendungs-System, welches das spezifizierte Domänen-Modell mit den Begriffen der Ubiquitous Language implementiert.

Domain-Driven Design und JEE

Wir gehen einmal von der Prämisse aus, dass wir unser Anwendungs-System mit der Programmiersprache Java implementieren wollen. Da DDD insbesondere für komplexe und unternehmensweite Anwendungen geeignet erscheint, liegt die Realisierung auf Basis der Java Enterprise Edition (JEE) als Plattform nahe.

Das Kernstück einer jeden betrieblichen Anwendung ist die Verwaltung von persistenten Daten und die Implementierung der benötigten Geschäftslogik auf diesen Daten. Für Klassen, welche die Anwendungsdaten repräsentieren, ist das Konzept der Entität (entity) allgemein bekannt. Wie wird nun das Entwurfsmuster einer Entität – vielleicht typischerweise – in einem JEE-Projekt umgesetzt?

Die Implementierung einer Entität wie z. B. Person erfolgt oft als anämische ("blutleere") Klasse: Es wird lediglich die Datenstruktur des Geschäftsobjektes und mittels Java Persistence (JPA) das Object-Relational-Mapping (ORM) auf eine Datenbanktabelle definiert, aber das – über triviale Methoden hinaus gehende – Verhalten (das "Blut") fehlt. Dieses Verhalten auf den Geschäftsobjekten wird daher in einer korrespondierenden, statuslosen Service-Klasse PersonService implementiert. Bei Betrachtung solcher Implementierungsmuster fragt man sich beinahe zwangsläufig, was aus dem objektorientierten Konzept der Einkapselung von Daten und Verhalten geworden ist.

Der Building-Block Entity im DDD

Das DDD definiert eine Reihe von Building-Blocks als die konstituierenden Elemente eines Anwendungs-Systems. Im Sinne der Objektorientierung kann man sich jeden Building-Block als ein Entwurfsmuster für eine bestimmte Art von Klassen mit einer festgelegten Aufgabenstellung vorstellen.

Innerhalb der Menge der Building-Blocks spielt dabei das Entity eine zentrale Rolle. Ein Entity repräsentiert ein Geschäftsobjekt im Rahmen des Domänen-Modells und im klassischen Sinn der Objektorientierung indem es die benötigten Geschäftsdaten zusammen mit den darauf operierenden Methoden einkapselt. Jedes Entity besitzt eine eindeutige und unveränderbare Objekt-Identifikation. Entities sind in den allermeisten Fällen persistente Objekte, was wiederum die Implementierung von Entities als JPA-Entity-Klassen nahelegt. Schauen wir uns daher im Java-Code an, wie die wichtige Geschäftsobjekt-Klasse Booking aus dem FIS schematisch aussieht:

@Entity
@Table(name = "OR_BOOKING")
@Access(AccessType.FIELD)
public class Booking extends DomainEntityImpl<BookingId> {

  @EmbeddedId
  private BookingId id;

  @Embedded
  private BookingTimestamp timestamp;

  @Enumerated(EnumType.STRING)
  private BookingState state;

  @OneToOne(mappedBy = "referringToBooking")
  private Invoice paidByInvoice;

  @OneToMany(mappedBy = "subordinatedToBooking")
  private Set<BookingPosition> composedByBookingPositions;

  public void cancel()
  {
    this.state = BookingState.CANCELED;
    ...
  }

  public InvoiceAmount calculateInvoiceAmount()
  {
    BigDecimal totalPrice = this.composedByBookingPositions.stream()
      .map(bp -> bp.calculateTotalPrice().getValue())
      .reduce(BigDecimal.ZERO, BigDecimal::add);
    return new InvoiceAmount(totalPrice);
  }
  1. Oberklasse: Wir haben für jeden Building-Block des DDD einen spezifischen Bezeichner festgelegt und im Rahmen einer Bibliothek entsprechende Interfaces und abstrakte Oberklassen für jeden Building-Block zur Verfügung gestellt. Darüber wird jede in unserem Anwendungs-System FIS implementierte Java-Klasse eindeutig einem Building-Block zugeordnet und es können allgemeine Eigenschaften eines Building-Block in einer (generischen) Oberklasse implementiert werden.
  2. Attribut-Typen: Als Datentyp für Basis-Attribute wie die Objekt-Identifikation id und den Buchungs-Zeitstempel timestamp verwenden wir durchgängig nicht die vordefinierten Java-Datentypen (z. B. Long und LocalDateTime), sondern spezielle Attribut-Klassen, welche als JPA-Embeddables implementiert sind. Alle Basis-Attribute sind daher mit den JPA-Annotationen @EmbeddedId bzw. @Embedded ausgezeichnet. Das Basis-Attribut state zeigt die Verwendung von Enumerations als Attribut-Typen.
  3. Beziehungen: Wie üblich werden Beziehungen zwischen den Geschäftsobjekten über entsprechende Beziehungs-Attribute auf eine objektorientierte Art und Weise implementiert. Das Geschäftsobjekt Booking hat eine 1:1-Beziehung zu der zugehörigen Invoice sowie eine 1:n-Beziehung (Komposition) zu den enthaltenen BookingPositions. Zum besseren Verständnis des Java-Code im Sinne der Ubiquitous Language nutzen wir Beziehungen mit Rollennamen.
  4. Spezifische Mutator-Methoden: Wir verzichten auf die standardmäßigen Setter für die privaten Attribute und realisieren stattdessen spezifische Mutator-Methoden. Am Beispiel der Methode cancel() erschließt sich der Gedankengang: Nur im Rahmen der fachlichen Funktionalität "Buchung stornieren" macht es Sinn, den Buchungs-Status auf den Enumeration-Wert BookingState.CANCELED zu setzen. Ein allgemeiner und öffentlicher Setter setState(BookingState) würde hingegen das willkürliche Setzen eines beliebigen Buchungs-Status von außerhalb des Geschäftsobjektes erlauben.
  5. Fachliche Methoden für Geschäftslogik: Dies umfasst in jedem Fall alle Operationen, die auf Basis der von der Geschäftsobjekt-Klasse eingekapselten fachlichen Attribute realisiert werden können. Ein Beispiel gibt die dargestellte Methode calculateInvoiceAmount(), welche zur Rechnungslegung den Gesamtpreis einer Booking aus den zugeordneten BookingPositions berechnet. Diese Operation kann problemlos  – und mit Lambda-Ausdrücken sowie Streams auch sehr elegant – in der Geschäftsobjekt-Klasse Booking selbst implementiert werden, so dass überhaupt keine Notwendigkeit für eine Auslagerung in eine etwaige Service-Klasse BookingInvoiceCalculationService besteht.

Rich Domain-Entity

Unser Vorgehen beim DDD-Ansatz ist nun, soviel fachliche Funktionalität wie irgend möglich unmittelbar bei dem entsprechenden Geschäftsobjekt zu implementieren. Wünschenswert ist daher eine Möglichkeit, aus einer Domain-Entity heraus die Methoden ganz anderer Klassen aufrufen zu können. Die JEE bietet mit der CDI-Technologie eine elegante Möglichkeit, um Objekte zur Programm-Laufzeit miteinander zu verknüpfen. Aber leider besitzen JPA-Entities eine solche Injektions-Fähigkeit von Hause aus nicht.

Eine elegante Lösung besteht nun darin, dass bereits bei der Erzeugung eines Domain-Entity darin befindliche Injektionen direkt befriedigt werden. Unser Ziel ist demnach, dass wir in einer Geschäftsobjekt-Klasse per @Inject beliebige andere Objekte anfordern können, und das zur Laufzeit bei der Erzeugung eines solchen Geschäftsobjektes die angeforderten Objekte automatisch miterzeugt und passend in das Geschäftsobjekt injiziert werden. Die gewünschte Injektions-Fähigkeit lässt sich durch eine einfache Utility-Klasse DependencyInjector mit einer Methode injectFields(Object) leicht nachrüsten, wobei nur die Möglichkeiten des BeanManager von CDI ausgenutzt werden. Den Aufruf dieser Utility-Klasse platzieren wir in den Konstruktor der Oberklasse für die Domain-Entities:

@MappedSuperclass
public abstract class DomainEntityImpl<I> implements DomainEntity<I> {

  protected DomainEntityImpl()
  {
    DependencyInjector.injectFields(this);
  }

Im folgenden Code-Auszug wird eine beispielhafte Anwendung dargestellt, indem in der Domain-Entity Booking des FIS ein "Dienst" zum Persistieren von Geschäftsobjekten vom Typ BookingPositionRepository per @Inject injiziert wird. Analog wird ein weiterer "Dienst" vom Typ BookingPositionFactory injiziert, welcher gemäß dem Factory-Entwurfsmuster zur Erzeugung von BookingPositions dient. Im Konstruktor der Oberklasse werden diese Injektionen aufgelöst, so dass innerhalb der Methode addPassenger() der Klasse Booking eine zugeordnete BookingPosition erzeugt und persistiert werden kann.

public class Booking extends DomainEntityImpl<BookingId> {

  @Inject
  @Transient
  BookingPositionFactory factoryBookingPosition;

  @Inject
  @Transient
  BookingPositionRepository repositoryBookingPosition;

  public void addPassenger(...)
  {
    ...
    BookingPosition bookingPosition = this.factoryBookingPosition.create(...);
    ...
    this.repositoryBookingPosition.insert(bookingPosition);
    ...
  }

Mit dieser Erweiterung erhalten wir die von uns so genannten Rich Domain-Entities. Durch die Möglichkeit, sich per CDI beliebige andere "Dienste" in Form von entsprechenden Objekten injizieren zu lassen, haben wir die Ausdruckskraft von Domain-Entities entscheidend erweitert. In einer Geschäftsobjekt-Klasse kann damit auch reichhaltige Funktionalität in Form von Methoden implementiert werden. Damit erübrigt sich die Notwendigkeit, dass solche Funktionalität in mehr oder weniger willkürlich gewählten Service-Klassen implementiert werden muss, weil nur dort Injektionen mit @Inject möglich sind. Da die Geschäftslogik ganz überwiegend direkt an den Geschäftsobjekten implementiert wird, bleibt diese fachliche Logik im gesamten Anwendungs-System einfach und schnell lokalisierbar.

Fazit und Ausblick

Die Rich Domain-Entities dienen uns als der "Motor" – d. h. als Kernstück und wichtigster Bestandteil – bei der Nutzung von Domain-Driven Design in der Anwendungs-Entwicklung. Darüber hinaus umfasst unsere Vorgehensweise aber viele weitere Building-Blocks wie Domain-Value (immutable Geschäftsobjekt), Domain-Repository (Persistenz), Domain-Factory (Objekt-Erzeugung), Domain-Service (fachlich-gebundener Service), Domain-Event (asynchrones Geschäftsereignis) oder Application-Service (transaktionaler Geschäftsvorgang). Durch die Nutzung des Ansatzes "Clean Architecture" nach R. C. Martin (2018) für die Anordnung dieser Building-Blocks gelangen wir zu einer gut wartbaren und entwicklungsfähigen Software-Architektur [2].

Über die Strukturierungsebenen Domain-Aggregate (Gruppe von Geschäftsobjekten) und Bounded-Context (Teilbereich des Domänen-Modells) werden schließlich die von uns so genannten Business-Components (lose gekoppelte Software-Komponenten) zusammengesetzt. Wenn nun bei der Wahl der Schnittstellen-Technologien (insbesondere RESTful-Webservices) und der Datenhaltung (insbesondere dezentral) ein stärkerer Fokus auf die Eigenschaft "self-contained system" gelegt wird, dann gelangen wir in die Welt der Microservices.

Quellen
  1. E. Evans: Domain-Driven Design: Tackling Complexity in the Heart of Software (2004)
  2. R. C. Martin: Clean Architecture (2018)

Autor

Jens Seekamp

Jens Seekamp entwickelt seit Anfang der 90er Jahre mit objektorientierten Programmiersprachen wie Smalltalk und Java. Er ist bei der GEDOPLAN GmbH (Bielefeld und Berlin) als Senior Consultant und Trainer beschäftigt.
>> Weiterlesen
Das könnte Sie auch interessieren
Kommentare (2)
  • Jens Seekamp
    am 22.06.2021
    Die Utility-Klasse DependencyInjector implementiert die folgende Methode:

    public static void injectFields(Object object)
    {
    if (object != null) {
    CreationalContext creationalContext = null;
    try {
    BeanManager beanManager = CDI.current().getBeanManager();
    creationalContext = beanManager.createCreationalContext(null);
    AnnotatedType annotatedType = beanManager.createAnnotatedType(object.getClass());
    InjectionTarget injectionTarget = beanManager.createInjectionTarget(annotatedType);
    injectionTarget.inject(object, creationalContext);
    }
    finally {
    creationalContext.release();
    }
    }
    }

    Diese wird bei der Konstruktion neuer Entity-Objekte in deren Basisklasse aufgerufen:

    protected DomainEntityBaseImpl()
    {
    DependencyInjector.injectFields(this);
    }

    Bitte wenden Sie sich bei weiteren Fragen zur Software-Architektur oder dem Domain-Driven Design gern an mich oder besuchen Sie unser Schulungs- und Beratungs-Angebot unter www.gedoplan.de.
  • Klaus Causemann
    am 20.06.2021
    Ein sehr interessanter Ansatz. Eine genauere Beschreibung des DependencyInjectors wäre hilfreich.

Neuen Kommentar schreiben