Boehrsi.de - IT und Gaming Blog

Boehrsi.de Services - Modulares Kotlin Setup

Erstellt am event Uhr von account_circle Boehrsi in label Boehrsi
Boehrsi.de Services - Modulares Kotlin Setup Bild

Über die Git Struktur meiner Dienste schrieb ich bereits, heute geht es etwas tiefer in die technischen Interna und die Ideen dahinter. Wie erwähnt nutze ich einen Core und selbiger erhält eine beliebige Anzahl an Implementierungen. Dies kombiniert ergibt dann einen Service. Doch wie habe ich dies umgesetzt, wie erfolgt im Code die eigentliche Verbindung zwischen Core und Implementierungen und wie arbeitet man wirklich mit diesem simplen Modularisierungskonzept?
Wie bereits mehrfach erwähnt nutze ich den folgenden Ansatz für kleinere Projekte, an welchen ich meist alleine arbeite. Für mittlere Projekte, mit mehreren Entwicklern, könnte man den Ansatz mit simplem Scripting auch nutzbar machen, für größere Teams würde ich selbigen aber nicht empfehlen. Doch nun zum technischen.

Ich nutze Kotlin open_in_new als Sprache und Javalin open_in_new als Web-Framework. Das Ganze dürfte aber definitiv auch mit anderen Sprachen und Frameworks umsetzbar sein, aber der Vollständigkeit halber wollte ich es noch einmal erwähnen.

Core

Der Core hat eine Reihe von Aufgaben, welche grob zusammengefasst die folgenden Bereiche abdecken:

  • Konfiguration: Die globale Config, die genutzten Environments und andere Werte werden aus verschiedenen Dateien geladen und vom Core verwaltet.
  • Handler: Web-Requests werden bei mir von sogenannten Handler Klassen verwaltet. Diese werden in den Implementierungen erstellt. Der Core stellt allerdings ein Interface, einen Error Handler und die HandlerRegistry zur Verfügung. Letzteres ist die zentrale Instanz, die dem Web-Framework, basierend auf den gesammelten Informationen, mitteilt welche Endpoints wie bedient werden.
  • Health: Wie es dem Server geht ist wichtig. Dies und verschiedene andere Informationen stellt der Core als eigenständige Endpoints zur Verfügung.
  • Logging: Natürlich müssen Aktionen, Fehler und andere Dinge via Logging gespeichert werden und dafür stellt der Core die nötige Logik zur Verfügung.
  • Provider: Diverse Funktionalitäten ähneln oder wiederholen sich. Dazu gehört z.B. das Versenden von Mails, die Nutzung von SQL oder NoSQL Datenbanken und diverse weitere Dinge. Provider stellen für wiederkehrende Logik Basisimplementierungen zur Verfügung, welche dann von den eigentlichen Implementierungen genutzt werden können.
  • Security: Auch eine kleine Rest-API sollte halbwegs sicher sein. Hier stellt der Core einige Funktionen zur Verfügung, verwaltet das Rate Limiting und überprüft z.B. API-Keys.
  • Stats: Wie wird die API genutzt, gibt es viele fehlerhafte Calls, versucht jemand das System zu manipulieren? Bei der Beantwortung dieser Fragen helfen Statistiken. Diese sammelt der Core und stellt sie für die spätere Auswertung zur Verfügung.
  • Utilities: Egal ob JSON Handling oder wiederkehrende Operationen auf Strings, Util-Klassen werden immer wieder gebraucht. Entsprechend habe ich auch diese in den Core ausgelagert.
  • Application: Die eigentliche Anwendung, der Webservers und all die anderen grundlegenden Dinge werden natürlich auch im Core verwaltet.
Mapping

Mit diesem durchaus großen Set an Basisfunktionen müssen sich die Implementierungen wirklich nur noch um ihre eigentlichen Funktionen kümmern. Eine Implementierung muss dafür nur mein extrem schlankes Handler Interface Implementieren:

interface Handler {
    fun register(app: Javalin, config: Config)
}

Sofern dies erledigt ist, kann die Implementierung in die Mapping-Datei eingetragen werden. Selbige ist, ebenso wie die eigentlichen Implementierungen, nicht im Core Git Repository zu finden. Die Mapping-Datei definiert welche Implementierung dem aktuell lokal verfügbaren Core bekannt sind. Der Flow ist hier also eine Version X des Cores auschecken, eine Anzahl N Implementierungen in den /implementations Ordner auschecken und in die Mapping-Datei die verfügbaren Handler der Implementierungen eintragen. Die Mapping-Datei sieht dann z.B. wie folgt aus:

object Implementations {
    val implementationHandlers =
        listOf(SearchHandler(), CommentHandler())
}

Die oben erwähnte HandlerRegistry nutzt dann das Implementations Singelton und macht die bis dato lose Verbindung zwischen Core und Implementierungen stabil. Während der Core und die Implementierungen in jeweils eigenen Repositories liegen, wird die Mapping-Datei nur lokal beim Entwickler abgelegt.

RestTender Local Setup open_in_new
Implementation

Eine Implementierung selbst ist also nur dafür zuständig die register Methode zu implementieren, in welcher Endpoints hinzugefügt werden. Dahinter befindet sich dann beliebige Logik, welche die Funktionalität der Implementierung bereitstellt. Da sich die verschiedenen Implementierungen nicht kennen, ist es wichtig auf die Bezeichnung der Endpoints zu achten, sodass es keine Duplikate gibt. Ich arbeite hier mit einem Prefix pro Implementierung (z.B. search- oder comments-), sodass Überschneidungen nicht möglich sind.

Flow

Möchte man nun einen weiteren Service entwickeln, erstellt man einfach einen neuen lokalen Ordner, mit einem frischen Core Repository und schon geht alles von vorne los. Sofern am Core Dinge geändert wurden, werden diese einfach gepusht und gleiches gilt auch für die Implementierungen. Ob andere lokale Versionen der Repositories diese Änderungen dann nutzen, kann je nach Situation entschieden werden. Sollten parallel Stände gepflegt werden müssen, kann man mit Git Tags und Branches natürlich auch dies relativ einfach umsetzen.
Wie zu Anfang erwähnt macht es durchaus Sinn ein paar simple Skripte zu erstellen, sofern man in solche Situationen kommt. Ich habe bis dato das Glück, dass alles auf den jeweils aktuellen Master Versionen läuft.

Fazit

Ich hoffe ich konnte mit diesem Beitrag meine generelle Idee klar machen und aufzeigen wofür ein derartiger Ansatz nützlich ist und wo er an seine Grenzen stößt. Warum ich das Ganze so aufgezogen habe, habe ich in den anderen Beiträgen der Sammlung bereits mehrfach thematisiert, weswegen ich darauf an dieser Stelle verzichte. Solltet ihr Fragen, Anregungen oder Wünsche für weitere Beiträge haben, meldet euch gerne in den Kommentaren.

Kommentare  
Kommentar erstellen
Mit dem Abschicken des Kommentars erklären sie sich mit der in der Datenschutzerklärung dargelegten Datenerhebung für Kommentare einverstanden. Spam, unangebrachte Werbung und andere unerwünschte Inhalte werden entfernt. Das Abonnieren via E-Mail ist nur für E-Mail Adressen erlaubt die Sie rechtmäßig administrieren. Widerrechtliche Abonnements werden entfernt.