Domain-Driven Design in Clojure

Gut bewertet

Obwohl DDD meist im Zusammenhang mit Objektorientierung genannt wird, sind die Prinzipien auch gut mit einer funktionalen Sprache umsetzbar. Eine Einführung in Clojure, dem Lisp für die JVM, bietet beispielsweise 1 2 3.

Auch in dieser Kolumne war Clojure und die Integration mit Java bereits ein Thema 4. Da für uns in dieser Kolumne das Domain-Driven Design aber noch neu ist, stellen wir das nun kurz vor.

Domain-Driven Design

Domain-Driven Design bietet zwei Facetten: Das strategische DDD liefert Hilfestellungen für die Systementwicklung im Großen. Das taktische DDD ist unserer Erfahrung nach deutlich bekannter und ähnelt der objektorientierte Analyse und Design, geht aber darüber hinaus.

Ein wesentliches Konzept des Domain-Driven Designs ist die Definition von klar abgegrenzten fachlichen Bereichen. Diese nennt man Bounded Contexts. Für einen Bounded Context modellieren Techniker und Domänenexperten gemeinsam, wie die Software die fachlichen Prozesse am Besten umsetzt.

Größere Systeme decken meist mehrere (Sub-)Domänen ab, die dann abhängig von organisatorischen Rahmenbedingungen über verschiedene Strategien integriert werden. Wir werden im Folgenden die Shared Kernel Strategie kennen lernen. Kasten Integrationsstrategien liefert einen Überblick über die verschiedenen Integrationsstrategien. Weitere Informationen bieten 5 oder auch 6.

Beim strategischen DDD konzentriert man sich in erster Linie auf das Identifizieren und Abgrenzen der fachlichen Kontexte (der Bounded Contexts) und dem Identifizieren von (Sub-)Domänen.

Für die Modellierung einer Domäne stehen beim taktischen DDD verschiedene Stereotypen zur Verfügung: Aggregat, Entität, Wertobjekt, Service, Repository und einige mehr. Jede dieser Arten unterliegt eigenen Regeln für zulässige Abhängigkeiten und Verantwortung im System.

Im Folgenden stellen wir die Domäne Rating als Beispiel vor und beschränken uns zu Beginn auf das strategische Modellieren.

Beispiel-Domäne Rating

Rating-Vorgang

Im Alltag eines Unternehmens - zum Beispiel in einer Bank - gibt es Vorgänge, in denen die Preisgestaltung sowie die Entscheidung über den Ausgang des Vorgangs von dem konkreten Rating-Ergebnis abhängt. Das Rating-Ergebnis beinhaltet einerseits die Ausfallwahrscheinlichkeit und andererseits auch eine Ratingklasse, die ähnlich einer Ampel signalisiert, wie das Vorhaben des Antragstellers/Kunden einzuschätzen ist.

Im Rahmen des Rating-Vorgangs wird sichergestellt, dass die relevanten Daten und Nachweise des Antragstellers/Kunden zusammengetragen werden, um mit Hilfe des Formelsystems das Rating-Ergebnis berechnen zu können.

Der Rating-Vorgang erlaubt im Rahmen einer Feedback-Schleife das Berechnen von vorläufigen Rating-Ergebnissen. Durch das Anpassen der Datenbasis für die Berechnung in gewissen Parametern können Kundenberater auf ein für den konkreten Fall angemessenes Ergebnis hinwirken.

Portfolio-Zuordnung

Ein Portfolio gruppiert Verträge, Kunden, Pakete mit gemeinsamer Risikoklasse und Anlagen. Ein Portfolio kann in Subsegmente unterteilt werden. Ein Rating-Formelsystem bezieht sich häufig auf ein Portfolio und Subsegment. Mit der Portfolio-Zuordnung wird ausgewählt, nach welchen Regeln, mit welchen konkreten Formeln und mit welchen erforderlichen Daten das Rating tatsächlich berechnet werden muss. Der Gestaltungsspielraum des Kundenbetreuers innerhalb eines Rating-Vorgangs hängt auch vom Portfolio (und dem konkreten Subsegment) ab. Beispielsweise besitzen Kundenbetreuer in einer Rückversicherung (Underwriter genannt) oder Berater für VIP-Kunden häufig einen ganz anderen Handlungsspielraum als der gemeine Kundenberater für Retail-Kunden.

Subdomänen

Wir können die Domäne Rating in verschiedene Subdomänen unterteilen. Für unser Beispiel unterscheiden wir die Subdomänen Retail, Corporate und gewerbliche Immobilienfinanzierung mit jeweils eigenen Anforderungen an die Datenbasis und die Berechnungen.

Bei der Portfolio-Zuordnung muss anhand aussagekräftiger Entscheidungsmerkmale erkannt werden, in welcher Subdomäne die Berechnung für einen aktuellen Vorgang definiert ist.

Strategischer Entwurf der Domäne Rating

Als Integrationsstrategie für die Subdomänen innerhalb der Domäne Rating wählen wir die Shared Kernel-Strategie: Die Subdomänen teilen sich einen gemeinsamen Kern. Diesen kapseln wir in einem Paket namens core.

Abb 1: Überblick: Context-Mapping der Domäne Rating und ihre Subdomänen

Rating Subdomäne core

Die Subdomäne core repräsentiert die Informationen, die allgemein für die Berechnung eines Rating-Ergebnisses benötigt werden. Das sind typischerweise Hardfacts und Softfacts. Hardfacts sind belastbare, klar belegbare Daten wie zum Beispiel ein Einkommensnachweis für Angestellte oder betriebswirtschaftliche Auswertungen für Gewerbebetriebe. Softfacts sind eher weichere Einflussfaktoren wie zum Beispiel die Branchenerfahrung, die Einschätzung der Marktsituation oder die Bewertung der Führungs- oder Vertriebskompetenz bei Gewerbetreibenden.

Abb. 2 zeigt das Modell, mit dem die zu einem Rating-Vorgang gehörigen Daten sowie die Information über die zu verwendende Berechnungsfunktion verwaltet und gespeichert werden können. Diese Daten sollten abstrakt ausreichen, um im Rahmen eines Rating-Vorgangs ein Rating-Ergebnis zu berechnen.

Abb 2: Domänenmodell der Subdomäne rating-core

Subdomäne Retail

Die Domäne Retail dient hier als Beispiel für die Portfolio-spezifischen Subdomänen. Retail-Rating erfordert für die meisten Vorgänge einen Einkommensnachweis (als Spezialisierung von Hardfacts) sowie das wahrheitsgemäße Ausfüllen eines Fragebogens (als Spezialisierung der Softfacts).

Dies wird in der entsprechenden Subdomäne definiert.

Abb 3: Domänenmodell der Subdomäne retail

Gemäß DDD stimmen die Techniker und die Fachleute gemeinsam ab, wie die Domäne technisch umgesetzt werden soll. Durch Verzicht auf technische Details können auch Beteiligte des Fachbereichs verstehen, wie durch die Geschäftsvorfälle softwaretechnisch unterstützt werden.

Jetzt da wir uns über den Zusammenhang zwischen Portfolien, Subsegmenten, Rating-Funktionen, Rating-Vorgängen und den dafür erforderlichen Daten grundsätzlich klar geworden sind, können wir über die konkrete Umsetzung nachdenken.

Im Folgenden orientieren wir uns an dem Grobentwurf und vertiefen ihn an der einen oder anderen Stelle sinnvoll, um eine mögliche Umsetzungsstrategie mit Clojure vorzustellen. Wir wenden hier das taktische DDD an.

Umsetzung in Clojure

Rating-Service

Der Rating-Service definiert im Wesentlichen eine Funktion rate( vorgang, kunde, fakten ) : RatingErgebnis.

In Clojure ist der Service realisiert wie in Listing 1 dargestellt.

(ns ddd-clojure-rating.domain.rating-service
  (:require [ddd-clojure-rating.domain.validation :as v]))

(defmulti required-facts (fn [rating customer]
                           [(:portfolio rating) (:segment customer)]))

(defmulti calculate-rating-result (fn [rating customer facts]
                          [(:portfolio rating) (:segment customer)]))

(defn rate [rating customer facts]
  (let [req-facts (required-facts rating customer)
        missing-facts (v/validate req-facts facts)]
    (if missing-facts
      (throw (IllegalArgumentException. "missing facts"))
      (calculate-rating-result rating customer facts))))
Listing 1: Rating-Service in Clojure

Zu Beginn erfolgt die Namensraum-Deklaration sowie der Import des validation-Namensraums (in Clojure per :require). Die dann folgenden defmulti-Ausdrücke definieren für die Portfolio-spezifischen Sub-Domänen die Schnittstelle, die hier im core/rating-service benötigt wird, um ein Rating-Ergebnis zu berechnen.

In der Funktion rate werden in der let-Anweisung die benötigten und die fehlenden Daten ermittelt. Sollten Daten fehlen wirft die Funktion eine IllegalArgumentException. Wenn alle erforderlichen Daten vorhanden sind, wird ein transientes RatingErgebnis erzeugt und zurückgeliefert.

Aggregat Rating

Das Aggregat wird in seinem eigenen Namensraum als Menge von Funktionen definiert.

(ns ddd-clojure-rating.domain.rating
  (:require [ddd-clojure-rating.domain.rating-service :as r]
            [schema.core :as s]
            [ddd-clojure-rating.domain.validation :as v]))
Listing 2: Namensraumdefinition des Aggregats Rating

Zur Laufzeit wird ein Rating durch eine Map - eine Standard-Datenstruktur in Clojure - repräsentiert. Die folgenden Definitionen beschreiben mit Hilfe der Bibliothek Prismatic Schema 7 die zulässigen Strukturen für ein Portfolio und einen Rating-Vorgang in Clojure.

(def valid-portfolio (s/enum :real-estate :instant-loan))

(def valid-rating {:id v/valid-id
                   :customer-id v/valid-id
                   :credit-amount v/positive-amount
                   :portfolio valid-portfolio
                   :created long
                   (s/optional-key :latest-rating) r/valid-rating})
Listing 3: Strukturen für ein Portfolio und einen Rating-Vorgang in Clojure

Die folgenden Funktionen dienen zur Zustandsmanipulation innerhalb eines Rating-Vorgangs.

(defn create [id credit-amount portfolio customer-id]
  (s/validate v/valid-id id)
  (s/validate v/positive-amount credit-amount)
  (s/validate valid-portfolio portfolio)
  (s/validate v/valid-id customer-id)
  {:id id
   :credit-amount credit-amount
   :portfolio portfolio
   :customer-id customer-id
   :created (System/currentTimeMillis)})

(defn rate [the-rating customer-provider facts-provider]
  (s/validate valid-rating the-rating)
  (let [cid (:customer-id the-rating)
        customer (customer-provider cid)
        facts (facts-provider cid)
        rating-result (r/rate the-rating customer facts)]
    (assoc the-rating :latest-rating-result
                      {:facts facts
                       :result rating-result
                       :on (System/currentTimeMillis)})))
Listing 4: Zustandsmanipulation

Multimethods für Portfolio-spezifische Rating-Funktionen

Der in Listing 1 vorgestellte Rating-Service implementiert den Ablauf eines Rating-Vorgangs, der für alle Vorhaben – unabhängig vom Portfolio – gleich ist. Zu diesem Zweck definiert er die Schnittstelle, die alle Portoflio-spezifischen Rating-Vorgänge erfüllen müssen. Neben den aus der Java-Welt bekannten Mitteln stehen in Clojure zwei weitere Möglichkeiten zur Verfügung: Protocols und Multimethods.

Die Portfolio-spezifischen Funktionen zum Prüfen auf Vollständigkeit der Datenbasis (required-facts) und der Berechnung des Rating-Ergebnisses (calculate-rating-result) setzen wir hier mit Multimethods um. Wie in Listing 1 zu sehen ist, werden Multimethods mit defmulti deklariert.

In der Deklaration einer Multimethod wird direkt eine Dispatch-Funktion angegeben, mit deren Hilfe zur Laufzeit bei jedem Aufruf der Multimethod die konkrete Implementierung ausgewählt wird. Im Unterschied zu gewöhnlicher Polymorphie objektorientierter Sprachen kann eine Dispatch-Funktion alle Funktionsargumente auswerten. In unserem Beispiel wird anhand des Portfolios des Vorgangs und des Marktsegments des Kunden die Implementierung gewählt.

Für jedes Portfolio werden nun mittels defmethod die gewünschten Implementierungen vorgenommen. Jede Implementierung der Funktion required-facts deklariert die benötigten Hard- und Softfacts in Form einer Map:

(defmethod r/required-facts [:real-estate :retail]
  [rating customer]
  {:personal-data c/valid-personal-data
   :property-location {:postal-code Integer}})
Listing 5: Erforderliche Hard- und Softfacts

Nach erfolgreicher Prüfung der Datenbasis kann das Rating-Ergebnis einfach berechnet werden:

(defmethod r/calculate-rating-result [:real-estate :retail]
  [rating customer facts]
  (let [postal-code (get-in facts [:property-location :postal-code])]
    (if (>= postal-code 60000)
      (r/rating-result 7 0.5)
      (r/rating-result 5 0.6))))
Listing 6: Dummy-Berechnung des Rating-Ergebnisses

Anwendungsfälle realisieren

Nun haben wir die Domäne Rating nach DDD modelliert und entsprechend unserer Konstruktionsregeln in Clojure umgesetzt. Es fehlt jetzt nur noch eine Anwendung, die auf der Funktionalität aufsetzt. Die in der Anwendung realisierten Anwendungsfälle verwenden das Domänenmodell. Den Einstiegspunkt bilden die Aggregate. Um ein Rating-Ergebnis zu berechnen laden wir die zum Rating gehörigen Daten aus dem Repository. Mit Hilfe der Aggregatsfunktionen (create und rate) können Ratings (mit create) nun konsistent angelegt und mit rate berechnet werden. Diese Änderungen werden danach einfach wieder über eine Repository-Funktion persistiert.

Fazit

Die Grundprinzipien des Domain-Driven Designs sind auch in einer funktionalen Programmiersprache anwendbar. Die Verwendung von Standard-Abstraktionen wie Maps, Collections und Sets fördern auf den ersten Blick das Erstellen anämischer Domänenmodelle, bei denen die Aggregate und Entitäten tendenziell nur zum Datentransport taugen und selbst keinerlei Fachlogik kapseln.

In Clojure kann dies durch geeignetes Gruppieren in Namensräumen leicht vermieden werden. Sind die Abbildungsvorschriften einmal bekannt, ist das Finden von zugehörigen Methoden zu Entitäten und Aggregaten einfach.

weitere Informationen zu den Integrationsstrategien im DDD

Quellen, Links und Interessantes

Referenzen

  1. B. Neppert, St. Tilkov, Einführung in Clojure – Teil 1: Überblick, in: Java SPEKTRUM, 2/2010, https://www.sigs–datacom.de/uploads/txdmjournals/nepperttilkovJS02_10.pdf  ↩

  2. B. Neppert, St. Tilkov, Einführung in Clojure – Teil 2, in: Java SPEKTRUM, 3/2010, https://www.sigs–datacom.de/uploads/txdmjournals/nepperttilkovJS03_10.pdf  ↩

  3. B. Neppert, St. Tilkov, Clojure – Teil 3: Nebenläufigkeit, in: JavaSPEKTRUM, 4/2010, https://www.sigs–datacom.de/uploads/txdmjournals/nepperttilkovJS04_10.pdf  ↩

  4. Ph. Ghadir, Java–Programme mit Clojure würzen, in: JavaSPEKTRUM, 4/2012  ↩

  5. E. Evans, Domain–Driven Design, Tackling Complexity in the Heart of Software, Addison–Wesley, 2003  ↩

  6. V. Vernon, Implementing Domain–Driven Design, Addison–Wesley Longman, 2013  ↩

  7. Prismatic Schema, https://github.com/prismatic/schema  ↩

TAGS

Kommentare

Um die Kommentare zu sehen, bitte unserer Cookie Vereinbarung zustimmen. Mehr lesen