Javaland 2019 - 19.03...Das Hexagon - Datenfluss User Interface Infrastructure Product-Service DB...
Transcript of Javaland 2019 - 19.03...Das Hexagon - Datenfluss User Interface Infrastructure Product-Service DB...
Hexagonale ArchitekturDomain zentrierte Microservices
Javaland 2019 - 19.03.2019
Christian Iwanzik@chrisIwanzik
Christian Iwanzik (33)SoftwareentwicklerDipl-Inf. (FH) - FH Köln
Steckenpferde:➢ JVM
○ Kotlin○ Scala○ auch java ;-)
➢ Domain Driven Design➢ Microservicearchitekturenö
Seit 2010 bei tarent Solutions in Bonn
www.tarent.de
Wir Bauen wir einen Warenkorb!
DB
Shoppingcart - Service Product-Service
GET /products/{sku}
GET /shoppingcart/{uuid}PUT /shoppingcart/{uuid}/items
Produktbild: http://tinobusiness.com/wp-content/uploads/2015/09/CONSUMER-PRODUCTS-e1442909155136.jpg
● Wir besitzen eine schöne Microservicearchitektur● Produkte und der Warenkorb sind in zwei verschiedene Services getrennt.● Aber wie bauen wir einen Microservice sauber auf?
Wir Bauen wir einen Warenkorb!Controller
Servicelayer
DB-Repository HTTP Client
top down
Separation of concerns!
Wenn wir mal die DB austauschen: Kein Problem!
Decoupling!
Dependency Inversion!
● Good old layers architecture● Wir haben einen Controller, einen Service, einen Client und ein Repository● Die Architektur sieht gut aus und streight forward
… und dann kam die FachlogikMaximaler Warenkorb-wert: 120€
Maximale Produktanzahl im Warenkorb: 50
Maximale Anzahl pro Produkt: 10
SKU (Stock Keeping Unit): alphanumerisch von 5 - 20 Zeichen
● Der PO kommt an und hat fachliche Anforderungen an den Warenkorb
… und dann kam die Fachlogik
0..n1
Controller
Servicelayer
DB-Repository HTTP Client
Klar: Hier im Service! Da ist die Logik!
Product
sku: String price: Int
ShoppingCart
products: Map <Product, Int>
● Das Team plant die Änderungen und malt sich eine kleine UML Skizze● Aber wo wird sie verortet? Natürlich in der Service Layer
Bob kommt neu ins TeamWas bauen wir denn hier?Ein
Warenkorb-service!
Ein µService mit Spring Boot!
Alles in Kotlin. Voll cool!
Oh und JPA und so!
Ja… Aber was MACHT denn der Service!?
Bob kommt neu ins Team. Was bauen wir denn hier?● Hauptsächlich technische Antworten.● Aber was genau macht denn der Service?
Was macht denn der Service?fun putProduct(@PathVariable uuid: String, sku: String): ResponseEntity<*> { val skuRegex = "[\\w\\d]{1,20}".toRegex()
return if(skuRegex.matches(sku)) {
val cart = service.addProduct(uuid, sku) … } else { ResponseEntity.badRequest().body("sku is not valid") }}
Controller
Also hier wird erst mal die SKU validiert. Beim Fehler geben wir HTTP 400 zurück.
● Der Kollege zeigt Bob den aktuellen Service und wo was verortet ist:● Im Controller finden wir die Datenvalidierung.
Was macht denn der Service?fun addProduct(uuid: String, sku: String): ShoppingCart? { val optionalCart = shoppingCartRepo.findById(uuid) return if(optionalCart.isPresent) { val cart = optionalCart.get() val product: Product = productServiceClient.getProduct(sku)
if(product != null) { cart.products?.add(ShoppingCartItem(product.sku, product, 1)) shoppingCartRepo.save(cart) } return cart
} else { null }}
Controller
ServicelayerUnd hier geben wir dann dem Warenkorb das gefundene Produkt.
Was sind denn● ShoppingCart● ShoppingCartItem● Product?
Hier holen wir ein Produkt beim Productservice ab.
● Und im Service finden wir die Hauptlogik des Service.○ Ein bestehender ShoppingCart wird geladen○ Ein Produkt wird vom Client gezogen○ Und das gefundende Produkt wird der Liste an Produkten des
ShoppingCarts hinzugefügt● Bob fallen beim Durchsehen, die Modelle auf. Und er fragt sich, was sie dort
noch machen.
Was macht denn der Service?@Entitydata class ShoppingCart ( @Id var uuid: String?,
var amount: Int?,
@OneToMany(cascade = [ALL]) var products: MutableList<ShoppingCartItem>?)
Controller
Servicelayer
Was sind denn● ShoppingCart● ShoppingCartItem● Product?
Achso. Das sind die Entities für den OR Mapper. Die nutzen wir aber auch im Client.
ShoppingCart
● Der Kollege zeigt ihm die Objekte. Es sind simple PoJos für den OR Mapper
Was macht denn der Service?@Entitydata class ShoppingCartItem( @Id var sku: String?,
@OneToOne(cascade = [ALL]) var product: Product?,
var quantity: Int?)
Aber ShoppingCartItem kommt doch im Modell gar nicht vor?
Ja, das brauchen wir aber für den ORM wegen der Quantity
ShoppingCart Product
0..n1
● Bob fällt auf, dass `ShoppingCartItem` gar nicht in der Modellskizze vorkommt. ● Dort gibt es doch nur `ShoppingCart` und `Product`● Die Gründe liegen in der Datenbank begründet, da sie eine Zwischentabelle
für die Quantity benötigt.
DB-Repository
Was macht denn der Service?-- schema.sqlALTER TABLE SHOPPING_CART_ITEM ADD CONSTRAINT max_quantity CHECK QUANTITY <= 10
Controller
Servicelayer
Von einem Produkt darf man doch nur maximal 10 haben.Wo wird das denn geprüft?
Na in der `schema.sql`
ShoppingCart
Ähem… Anna? Wo war noch mal der Constraint?
DB-Repository
● Bob fragt nach den Invarianten für den Warenkorb. ○ Wo sind sie denn?
● Peter weiß die Antwort. Sie sind direkt in der Datenbank als Constraint.
Saube Schichten. Verteilte FachlichkeitController
Servicelayer
DB-Repository HTTP Client
Formatvalidierung
Objektkomposition
Überprüfung von Invarianten
● Die Schichten sind gut getrennt und alles hat Hand und Fuß● Lediglich die Fachlichkeit, das eigentlich Wichtige am Service, ist wild im
System verteilt.● Sogar in der Datenbank findet man Constraints.
Ein paar Wochen später:
BIG BALL OF
MUD
Wo wird noch mal der Warenkorbwert berechnet?
● Reales Beispiel aus einem Projekt mit ausufernder Komplexität.● Wo ist die Fachlogik? Was MACHT mein Service eigentlich?● Je älter und komplexer ein Service wird, desto verteilter ist die Fachlichkeit.
Die IDee !
Erzähl mal!
Ich hab mal was von Hexagonaler Architektur gehört.
● Bob ist mittlerweile gut im Team angekommen und hat eine Idee, wie man dem Chaos begegenet.
Das Hexagon - Die Schichten
Domain
Application
Ports
Product-Service
DB
● Fachlogik● Modell● “Das Herz der Software”● keine Technik● Domain Driven Design
● Usecases ● Komposition
● Technische “Anhängsel”● Controller● Queue Publisher● SQL Client
Ursprünge:● von Alistair Cockburn 2005 propagiert. Die Itention war die Ports-and-adapter
Architektur.● Hexagonal war zunächst nur der Arbeitstitel. Hat sich aber bis heute behalten.
Die hexagonale Architektur besteht grob aus drei Schichten. Daher auch Schichtenarchitektur.
● Modell: Gekapselte Fachlogik. NUR die Fachlogik und Fachdaten. Berechnungen, Validierungen, Pre- Post-Conditions, Invarianten. Keine Technik.
● Application: Komposition - Lade das Modell aus der DB. Sag ihm was du willst. Speicher es zurück in die DB.
● PortsTechnische Anhängsel - HTTP Controller, Queue Subscriber oder Publisher, etc. Reine Technik.
Die Domain
Reine Fachklassen- und Funktionen
Product
SKU
Price
ProductName
simple Fachtypen:syntaktische- und semantische Validierung
zusammengesetzter Fachtyp
<< Aggregate >>ShoppingCart Quantity
AggregateRoot- Interface nach außen- alle Fachlogik- Invarianten- immer gültig!
putProductInto
amount
quantityOfProduct
checkMaximumProductCount
Nur diese Teile reden mit der Außenwelt!
WICHTIG: Nach außen gegebene Objekte, dürfen keinen Einfluss mehr auf die Domäne haben!Kopieren!
Im Zentrum steht die Domäne. Also alles, was unseren Service fachlich ausmacht. ● Wir nutzen hier keine Literale als Fachobjekte, sondern wohl benamte
Klassen, die hier syntaktisch und semantisch validieren. Ich nenne sie mal spaßeshalber “Fachliterale”
● Lediglich ein Typ definiert eine API nach außen: Das “AggregateRoot”. Nur hier gehen Informationen in die Domäne und aus der Domäne raus.Nur diese API manipuliert die Domäne. Validiert sie aber auch und redigiert falsche Inputwerte. Wichtig: Die Domäne ist IMMER gültig!
● Vorteile: Testbarkeit. Sicherheit in der Berechnung, Typsicherheit.
Die Domaindata class Product(val sku: SKU, val price: Price, val name: Name)
Product(SKU("132456"), Price(4, 99), Name("Brot"))
data class SKU(val value: String) { private val regex = "[\\w\\d]{1,20}".toRegex()
init { if(!regex.matches(value)) throw IllegalArgumentException("...") }}
fun putProductInto(product: Product, quantity: Quantity): ShoppingCart {
checkMaximumProductCount()
val newAmount: ShoppingCartAmount = overallAmount + (product.price * quantity) val existingQuantity: Quantity? = cartItems[product]
if(existingQuantity == null) { cartItems[product] = quantity } else { cartItems[product] = existingQuantity.copy(value = existingQuantity.value + quantity.value) }
overallAmount = newAmount
return this} Wie sieht so was im Code aus?
● Fachobjekte validieren sich selbst● Zusammengesetzte Objekte nutzen lediglich Fachobjekte● Das AggregateRoot definiert eine Anzahl an Businessfunktionen und nutzt die
darunter liegenden Fachobjekte.● Wir ziehen Fachlichkeiten aus dem Controller und der Datenbank direkt in
unsere Fachklassen.
Das Hexagon - Datenfluss
User Interface Infrastructure
Product-Service
DB
Domain
Application
Ports
Wir gehen noch mal ein bisschen hinaus und schauen uns nun den Datenfluss an.Wir können das Hexagon in zwei Seiten teilen. Auf die rechte Seite schreiben wir aktive Komponenten, auf die rechte Seite passive Komponenten.
Die Application
Application
Product-Service
DBUser Interface Infrastructure
Domain
CommandService
Report
Service
Usecaseinterface
Usecase interface
Repositoryport
Port
● Usecases.● Komposition der Ports und
Domains. ● Keine Fachlogik!
● Zwischen den Schichten kommunizieren wir über definierte Interfaces genannt Ports.
● Aktive Komponenten nutzen dir Driver Ports. Diese Komponenten wollen etwas von unserem System.
○ Sie wollen neue Daten eingeben○ Sie wollen aber auch bestehende Daten abfragen○ Beispiel Command und Report Service
● Passive Komponenten werden über driven Ports angesteuert.○ Unser System will etwas von ihnen.○ Wir wollen Daten an die Komponente schicken○ Wir wollen aber auch Daten von der Komponente beziehen.
● Services bilden Usecases ab, die von aktiven Komponenten angestoßen werden können.
○ z.B. Über den Query Port kommt die Anfrage: “Gib die Summe aller aktuellen Warenkörbe”
○ Service nutzt das Repository Interface um die Domänenobjekte zu laden
○ Nutzt die Domäne um die Werte zu ermitteln ○ Gibt sie an den Query Port zurück.
Die Application
≪ Aggregate ≫ShoppingCart
putProductInto
CommandService
≪ interface ≫ServiceInterface
Aufrufer
≪ interface ≫Repository
Aufgeru-fener
≪ implements ≫
≪ implements ≫interface ServiceInterface { fun putProductIntoShoppingCart(shoppingCartUuid: ShoppingCartUuid,...) ...}
@Serviceclass CommandService(val shoppingCartRepositoryPort: ...): ServiceInterface {
override fun putProductIntoShoppingCart(shoppingCartUuid ...): Optional<ShoppingCart> { shoppingCartRepositoryPort.load(shoppingCartUuid) .map { shoppingCart -> shoppingCart.putProductInto(...) } …. }
● Ports werden durch Interfaces umgesetzt. Aufrufer und Aufgerufener sind (noch) unbekannt.
● Der Service komponiert nur die Ports mit der Domäne.
Ports And Adapter
Application
Ports
REST
UI
Product-Service
DBUser Interface Infrastructure
Domain
Driver Adapters
● Technische Frameworks
● Keine Fachlogik!
● Wir zeichnen wieder die gesamten Schichten und schauen uns an, wie wir mit den Application-Stöpseln auf der nächsten Ebene umgehen können.
● Wir nennen diese “Stöpsel” Adapter ● Jetzt wird auch klar, warum die Architektur auch Ports-and-adapter Architektur
heißt.● Nutzende Schichten benutzen diese Ports als “Driving Adapters”.
○ Also Logiken, die das System steuern und “benutzen” ○ Sie übersetzen alle “Befehle” die von außen hereinkommen auf die
interne Logik.
Ports And Adapter
Application
Ports
Product-Service
DB
REST
User Interface Infrastructure
Domain
Driver Adapters
HTTP
SQL
Driven Adapters
REST
UI
● Genutzte Schichten benutzen äußere Ports als “Driven Adapters”○ Also Logik, welche aus dem System genutzt wird.
● Wichtig: Driving und Driven Adapters definieren nicht den Datenfluss, sondern die Richtung der Befehle.
Ports And Adapter
Application
Ports
Product-Service
DB
REST
User Interface Infrastructure
Domain
ViewsController
HTTP
SQL
RESTController
HTTP Client
OR Mapper
● Zwischen den Schichten kommunizieren wir über definierte Interfaces genannt Ports.
○ Daher spricht man auch oft von der Ports-and-adapter Architektur.● Nutzende Schichten benutzen diese Ports als “Driving Adapters”.
○ Also Logiken, die das System steuern und “benutzen” ○ Sie übersetzen alle “Befehle” die von außen hereinkommen auf die
interne Logik.● Genutzte Schichten benutzen äußere Ports als “Driven Adapters”
○ Also Logik, welche aus dem System genutzt wird.● Wichtig: Driving und Driven Adapters definieren nicht den Datenfluss, sondern
die Richtung der Befehle.
Ports And Adapter
≪ Aggregate ≫ShoppingCart
putProductInto
CommandService
≪ interface ≫ServiceInterface
≪ interface ≫Repository
≪ implements ≫
≪ implements ≫
≪ adapter ≫ShoppingCartController
≪ adapter ≫JPARepository
≪ port ≫SpringBootMVC
≪ port ≫Hibernate
●
Hatten wir das nicht schon mal?ShoppingCartController
CommandService
JPARepository
top down
ShoppingCart Product
0..n1
● Die Struktur auf Codeebene ähnelt sehr stark dem, was wir bereits hatten● Allerdings “denken” wir hier in anderen Strukturen:
○ Statt einer 1 dimensionale Struktur von oben nach unten spannen wir eine 2 dimensionale Ebene auf.
○ Die Concerns sind viel enger gefasst als in der alten Architektur.○ Die Elemente sind durch Ports und Adapter noch loser gekoppelt.
Hexagonale Architektur
Application
Ports
Product-Service
DB
REST
User Interface Infrastructure
Domain
ViewsController
HTTP
SQL
RESTController
HTTP Client
OR Mapper
Srv
Alles zusammen:● Sieht komplizierter, aber auch strukturierter aus.
Achso: Warum ein Hexagon: ● Alistair Cockburn zeichnete in seinen ersten Entwurf zur Ports-and-adapter
Architektur ein Secheck, weil es grafisch besser zu seiner Anzahl an Ports passte.
● Man kann auch alle anderen X-Ecke nehmen.
Klaus kommt neu ins TeamWas bauen wir denn hier?
Hier ist der Domainkern und hier die Tests dazu.
Wow! Das ist echt sauber und verständlich.
Die Umsysteme findest du unter Ports.
● Klaus kommt neu ins Team und ist begeistert!
Wie geht es weiter?
Product-Service
DB
Component
Ursprünge:● von Alistair Cockburn 2005 propagiert. Die Itention war die Ports-and-adapter
Architektur.● Hexagonal war zunächst nur der Arbeitstitel. Hat sich aber bis heute behalten.
Die hexagonale Architektur besteht grob aus drei Schichten. Daher auch Schichtenarchitektur.
● Modell: Gekapselte Fachlogik. NUR die Fachlogik und Fachdaten. Berechnungen, Validierungen, Pre- Post-Conditions, Invarianten. Keine Technik.
● Application: Komposition - Lade das Modell aus der DB. Sag ihm was du willst. Speicher es zurück in die DB.
● PortsTechnische Anhängsel - HTTP Controller, Queue Subscriber oder Publisher, etc. Reine Technik.
Wann sollte ich es einsetzen?● Man hat generell eine Fachlogik (Domain)
● Die Domain hat Invarianten
● Viele Umsysteme, bzw. APIs
● Verschiedene fachliche Sichten
● Kein Fachmodell
● Keine Invarianten
● simples CRUD
Pro:● Man hat eine Domain mit Invarianten. Selbst wenn es wenige sind: Wo die her
kommen, gibt es noch viel mehr.● Man hat viele Umsysteme, die man irgendwo in seiner Architektur verankern
muss.● Man hat mehr als eine fachliche Sicht auf die Domäne und arbeitet mir
verschiedenen Readmodels.
Contra:● Man arbeitet generell nicht mit einem Fachmodell● Man muss keine Invarianten beachten.● z.B. simples CRUD oder ein Duchlauferhitzer (Formatkonverter, Gateway,
Datenpumpe, etc.)
Vor- und Nachteile● Macht die Domainkomplexität handhabbarer,
da zentralisiert.
● Einzelne Schichten lassen sich besser testen.
● Einführung eines neuen Umsystems ist einfacher.
● Neue Kollegen sehen sich den Kern an und wissen, was passiert. Die Wahrheit steht im Code. Diesmal wirklich. ;-)
● Overhead - Domainobjekte müssen oft in neue Modelle umgewandelt werden.
● Manche Frameworks machen einem ein Strich durch die Rechnung. Aufwand von Sonderlockenhier: Hibernate und Jackson
Vorteile:● Zentralisierte Fachlichkeit im µService hat immer Vorteile● Domain, Applikation und Ports sind über Interfaces getrennt. Sie lassen sich
separat gut testen.● Durch die Interfaces ist es auch einfach z.B. einen Messagebus anzubinden. ● Wenn man DDD betreibt,
Nachteile:● Die Domain muss oft in andere DTOs umgewandelt werden um auf
Frameworks zu passen● Die Domain ist die ganze Wahrheit. Manche Abfragen wollen aber nicht die
ganze Wahrheit liefern. Hier muss man “ReadModels” einbauen. Das macht die Abfagen aber auch wiederum entkoppelter.
● https://fideloper.com/hexagonal-architecture
● https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexa
gonal-onion-clean-cqrs-how-i-put-it-all-together/
● https://marcus-biel.com/hexagonal-architecture/
● https://web.archive.org/web/20060711220612/http://alistair.cockburn.us:80/index.php/Main_Page
Literatur
Beispiel:
https://gitlab.com/Iwanzik/hexagonal-service
Vielen Dank!