Hexagonale Architektur in Kotlin: Wenn Code frei atmet
\n\nStellen Sie sich vor, Sie bauen ein Haus. Sie können sofort das Fundament gießen, Wände hochziehen und Fenster einsetzen. Oder Sie können zuerst den Rahmen entwerfen, die Leitungen verlegen und sich erst dann um die Innenausstattung kümmern. In der Programmierwelt wird der zweite Ansatz als hexagonale Architektur (auch Ports & Adapters) bezeichnet. Und Kotlin mit seiner Prägnanz und seinem leistungsstarken Typsystem ist die ideale Sprache, um dieses Konzept in die Tat umzusetzen.
\n\nDieser Artikel ist kein langweiliges Lehrbuch. Es ist ein praktischer Leitfaden, der zeigt, wie die hexagonale Architektur in Kotlin Ihren Code in ein flexibles, testbares und von externen Diensten unabhängiges System verwandelt. Wir werden ein Beispiel von Grund auf durchgehen, echten Code schreiben und sehen, wie das in der Praxis funktioniert.
\n\nWas ist hexagonale Architektur und warum brauchen Sie sie?
\n\nAlistair Cockburn, der diesen Begriff geprägt hat, verglich die Anwendung mit einem Sechseck (Hexagon). Im Zentrum befindet sich Ihre Geschäftslogik. An den Rändern befinden sich Ports (Schnittstellen), über die sie mit der Außenwelt kommuniziert. Adapter sind Implementierungen dieser Schnittstellen für bestimmte Technologien: Datenbank, Web-Framework, Message Queue.
\n\nDie Kernidee: Die Geschäftslogik weiß nichts darüber, wie Daten gespeichert werden oder welches Framework verwendet wird. Sie ruft einfach Methoden der Ports auf. Möchten Sie PostgreSQL durch MongoDB ersetzen? Schreiben Sie einfach einen neuen Adapter. Die Geschäftslogik wird den Austausch nicht einmal bemerken.
\n\nIn Kotlin ist dies besonders praktisch dank Schnittstellen mit Default-Implementierungen, sealed-Klassen und funktionalem Stil. Kommen wir zur Sache.
\n\nSystementwurf: Beispiel eines Online-Shops
\n\nAngenommen, wir schreiben einen Microservice zur Verwaltung von Bestellungen. Im Zentrum steht OrderService, der Bestellungen erstellt und die Verfügbarkeit von Artikeln prüft. Die Außenwelt besteht aus einem HTTP-Controller und einer Datenbank.
So sieht die Projektstruktur aus:
\n\nsrc/\n├── domain/ # Kern (Geschäftslogik)\n│ ├── model/\n│ ├── port/\n│ └── service/\n├── adapter/\n│ ├── input/ # Eingehende Adapter (Controller)\n│ └── output/ # Ausgehende Adapter (Repositories)\n└── Application.kt # Einstiegspunkt\n\n\nSchritt 1. Kern: Modelle und Ports
\n\nBeginnen wir mit dem Bestellmodell. In Kotlin wird dies eine data class sein:
// domain/model/Order.kt\ndata class Order(\n val id: String,\n val customerId: String,\n val items: List<OrderItem>,\n val status: OrderStatus\n)\n\nenum class OrderStatus {\n PENDING, CONFIRMED, SHIPPED, CANCELLED\n}\n\ndata class OrderItem(\n val productId: String,\n val quantity: Int\n)\n\n\nJetzt definieren wir die Ports – Schnittstellen, über die der Kern mit der Außenwelt kommuniziert. Wir werden zwei Ports haben: einen zum Abrufen von Produktdaten und einen zum Speichern von Bestellungen.
\n\n// domain/port/ProductRepository.kt\ninterface ProductRepository {\n fun checkAvailability(productId: String, quantity: Int): Boolean\n}\n\n// domain/port/OrderRepository.kt\ninterface OrderRepository {\n fun save(order: Order): Order\n fun findById(id: String): Order?\n}\n\n\nBeachten Sie: Es gibt keine Erwähnung von Datenbanken, HTTP oder Frameworks. Reine Geschäftslogik.
\n\nSchritt 2. Business-Service
\n\nJetzt schreiben wir den OrderService, der diese Ports verwendet. In Kotlin übergeben wir Abhängigkeiten über den Konstruktor – dies wird als Abhängigkeitsinjektion (DI) bezeichnet.
// domain/service/OrderService.kt\nclass OrderService(\n private val productRepository: ProductRepository,\n private val orderRepository: OrderRepository\n) {\n fun createOrder(customerId: String, items: List<OrderItem>): Order {\n // Prüfen der Verfügbarkeit jedes Artikels\n for (item in items) {\n if (!productRepository.checkA