Suche
Kotlin Tutorial

Kotlin: Ein Tutorial für Einsteiger

Alexander Hanschke
icon_Kotlin

Die statisch typisierte Sprache Kotlin erfreut sich zunehmender Beliebtheit. Kotlin läuft sowohl auf der JVM als auch in JavaScript-Umgebungen. Durch die Kompatibilität zu Java 6 findet Kotlin zudem im Android-Umfeld Anklang, da die Sprache prägnanteren Code ermöglicht und zusätzlich die funktionale Programmierung unterstützt. In diesem mehrteiligen Tutorial betrachten wir die wichtigsten Sprachfeatures anhand einer Beispielanwendung.

Janitor: Unsere Beispielanwendung

Als durchgehendes Beispiel in diesem Tutorial dient eine Anwendung, mit der gespeicherte Artikel in Pocket abgerufen und basierend auf einem Filter gelöscht oder archiviert werden können. Als ein Anwendungsfall können alle ungelesenen Artikel, die älter als 30 Tage sind, archiviert werden. Da die Anwendung sich somit um das Reinhalten der Artikel kümmert, bekommt sie den passenden Namen Janitor (Hausmeister). Janitor enthält ein Modul, das den Zugriff auf das Pocket API kapselt. Dieses wird im ersten Teil entwickelt. Anschließend nutzen wir das Spring Framework um das Arbeiten mit Janitor komfortabler zu gestalten und die Anwendung bei Bedarf in der Cloud zu deployen.

JVM-Sprache Kotlin

Man nehme …

Für die Entwicklung von Janitor verwenden wir Kotlin 1.0.4 und Java 8. Das Verwalten der Abhängigkeiten und Bauen der Anwendung erfolgt durch Gradle 3.1. Um auf Pocket zuzugreifen, ist ein Account erforderlich. Anschließend muss, wie hier beschrieben, eine neue Anwendung erstellt werden. Dabei benötigt der Consumer Key die Berechtigungen Modify und Retrieve. Access sowie Consumer Key werden dann direkt von Janitor für die Zugriffe verwendet.

Installation

Der Kotlin Compiler kann direkt von der Webseite heruntergeladen werden. Komfortabler ist aber die Verwendung in einer IDE. Mittlerweile werden IntelliJ IDEA, Eclipse sowie NetBeans unterstützt. Um schnell einfache Ausdrücke auszuwerten, bringt Kotlin einen REPL (Read-Evaluate-Print Loop) mit, der wie folgt aufgerufen werden kann:

$ kotlinc-jvm
Welcome to Kotlin version 1.0.4 (JRE 1.8.0_92-b14)
Type :help for help, :quit for quit
>>> val target = "repl"
>>> println("hello $target")
hello repl

Quickstart: Los geht’s!

Janitor ist via GitHub verfügbar und kann direkt bezogen werden. Der initiale Stand kann via git checkout tags/setup geprüft werden. Das Projekt verwendet Gradle und folgt der üblichen Verzeichnisstruktur. Unter src/main/kotlin liegen die Kotlin-Dateien. die unsere Anwendung ausmachen. Wollen wir parallel auch Java verwenden, werden die Dateien wie gewohnt unter src/main/java abgelegt. Tests werden analog unter src/test/kotlin bzw. src/test/java abgelegt. Ebenfalls existiert bereits das Modul pocket-api, das den Zugriff auf das Pocket API kapselt und von Janitor direkt verwendet wird. Der Einstiegspunkt des Programms ist die Datei Janitor.kt, die unter src/main/kotlin/de/techdev/janitor zu finden ist und bereits den folgenden Code beinhaltet:

src/main/kotlin/de/techdev/janitor/Janitor.kt
package de.techdev.janitor

import java.io.PrintStream as Printer

fun main(args: Array): Unit {
val name: String = "Janitor"
var version = "0.0.1"

fun Printer.echo() = "$name v$version"

println(System.out.echo())
}

Die Datei nutzt das Package de.techdev.janitor und folgt auch der entsprechenden Organisation im Dateisystem. In Kotlin ist dies aber nicht zwingend erforderlich.

Packages in Kotlin

Kotlin verwendet Packages, um Typen in Namespaces zu unterteilen – nicht aber, zur Kontrolle von Sichtbarkeiten (wie package-private in Java). Wir könnten das Package also auch de.techdev.janitor.foo nennen. Da dies aber potenziell zu Verwirrung führen kann, besonders dann, wenn man bestehende Java-Projekte auf Kotlin migriert, sollte man der Konvention folgen und Packages analog zu der Organisation im Dateisystem definieren. Import Statements funktionieren wie in Java auch. Zusätzlich zu Typen können in Kotlin aber auch einzelne Funktionen importiert werden. In Janitor.kt existiert bereits der Import import java.io.PrintStream as Printer. Dieser Ausdruck importiert den (Java-) Typen java.io.PrintStream und macht diesen unter dem Namen Printer innerhalb der Datei verfügbar.

Das Verwenden dieser Alias ist besonders dann hilfreich, wenn in einer Datei unterschiedliche Typen mit identischen Namen verwendet werden. Hier muss in Java jeweils der voll-qualifizierte Name inklusive Package angegeben werden. Zusätzlich enthält die Datei bereits eine Funktion mit dem Namen main. Hierbei handelt es sich um eine globale Funktion, da sie keinem Typen zugeordnet ist und auf oberster Ebene definiert ist. Die main-Funktion ist das Gegenstück zu der gleichnamigen Methode aus Java und beschreibt den Einstiegspunkt in unser Programm.

Funktionsdefinitionen: fun, fun, fun!

Mit dem Schlüsselwort fun werden Funktionsdefinitionen eingeleitet, gefolgt von dem jeweiligen Namen. Als einziger Parameter wird ein Array erwartet, das unter dem Namen args innerhalb der Funktion verfügbar ist. Anders als in Java wird zuerst der Name gefolgt von einem Doppelpunkt und dem Typen angegeben. Die Funktion gibt kein Ergebnis zurück. Das ist an dem Rückgabetyp Unit erkennbar, der vergleichbar mit void aus Java ist. Da Unit der Default ist, kann es auch weggelassen werden. Innerhalb der Funktion werden zwei Referenzen vom Typ String deklariert, name und version. Die erste Referenz verwendet das Schlüsselwort val (von Value). Values sind unveränderliche Referenzen, wie final in Java. Analog steht var (von Variable) für veränderliche Referenzen, die auch nach der Initialisierung noch verändert werden können.

Für den Namen wird der Typ mit String explizit angegeben, bei der Version wird er hingegen weggelassen. Da beide Referenzen durch ein Literal direkt initialisiert werden, erkennt der Compiler den Typen automatisch, sodass die Angabe wegfallen kann. In der Funktion main wird anschließend eine weitere Funktion echo deklariert. Hierbei handelt es sich um eine lokale Funktion, die Zugriff auf alle Referenzen der umgebenden Funktion hat. Die Funktion echo selbst kann nur innerhalb der Funktion main verwendet werden, da sie außerhalb dieser nicht sichtbar ist.

Der Inhalt der main-Funktion ist innerhalb der geschweiften Klammern definiert – diese Syntax wird Block Body genannt. Die Funktion echo hingegen besteht nur aus einem einzelnen Ausdruck, dem Stringliteral $name v$version. Daher kann die kürzere Expression Body Syntax verwendet werden. Anstelle des : wird ein = verwendet. Der Rückgabetyp ist optional und ein explizites return ist nicht nötig. Noch etwas ist an dieser Funktion besonders, es handelt sich um eine Erweiterungsfunktion. Die Funktion kann nur auf Instanzen des Typs PrintStream (aka. Printer) aufgerufen werden. Beim Aufruf verhält sich die Funktion so, als wäre sie eine Member-Funktion des entsprechenden Typs. Wir verwenden die Funktion im letzten Ausdruck println(System.out.info()) der den Text Janitor v0.0.1 auf der Konsole ausgibt. Die Funktion println ist Teil der Kotlin-Standardbibliothek und kann ohne zusätzlichen Import verwendet werden.

Der von echo zurückgegebene String verwendet die Referenzen name und version. Innerhalb von Strings können Variablen direkt verwendet werden, das ist sogar typsicher und der Compiler beschwert sich, falls der Zugriff nicht möglich ist. Ausdrücke in Strings werden mit der Syntax ${..} referenziert – bei einfachen Variablen können die geschweiften Klammern auch weggelassen werden.

API Binding: Aufrufe direkt aus Kotlin durchführen

Im nächsten Schritt beginnen wir mit der Implementierung der Pocket API Bindings, um Aufrufe direkt aus Kotlin durchzuführen. Dafür existiert bereits das Modul pocket-api. Innerhalb des Moduls trennen wir die öffentlichen Typen von der jeweiligen Implementierung, die von Clients des API verwendet werden. Implementierungen und Dateien, die für die Verwendung außerhalb des Moduls nicht vorgesehen sind, behalten wir im Package de.techdev.pocket. Alle öffentlichen Dateien werden dagegen im Package de.techdev.pocket.api angelegt.

Kotlin verwendet Packages nicht für die Kontrolle von Sichtbarkeiten. Dafür gibt es die Sichtbarkeiten public, protected, internal und private. Mit internal lässt sich die Sichtbarkeit eines Typen oder einer Funktion auf Dateien innerhalb desselben Moduls begrenzen – in unserem Fall auf das Gradle-Modul pocket-api. Da es internal in Java nicht gibt, werden die Typen und Funktionen im Bytecode als public markiert. Sie sind also von Java heraus nach wie vor aufrufbar.

Der Zugriffsmodifikator public ist der Default und verhält sich analog zu Java, dasselbe gilt für private. Auch wenn protected vom Namen her identisch mit dem Java-Gegenstück ist, ist die Semantik eine andere. Der Modifikator kann nur für Member-Funktionen verwendet werden, also Funktionen, die zu einem Typen (bspw. einer Klasse) gehören. Die gekennzeichneten Funktionen können nur innerhalb des zugehörigen Typen und allen erbenden Klassen aufgerufen werden. Ein Zugriff von außerhalb ist jedoch nicht möglich.

Janitor soll in der Lage sein, bestehende Artikel abzurufen und anschließend zu archivieren oder zu löschen. Daher beginnen wir damit, die benötigten Operationen zu entwerfen. Das Pocket API bietet drei dedizierte Endpoints an: Retrieve für das Laden von Artikeln, Modify zum Bearbeiten existierender Artikel und Add für das Hinzufügen neuer Artikel. Für unseren Anwendungsfall reichen die ersten beiden Operationen aus, die wir als Interfaces in Kotlin definieren und später implementieren.

pocket-api/src/main/kotlin/de/techdev/pocket/api
interface RetrieveOperations

interface ModifyOperations

Interfaces werden in Kotlin genauso definiert wie in Java: mit dem Schlüsselwort interface. Da beide Interfaces initial leer sind, können die geschweiften Klammern hinter dem Namen weggelassen werden. Um bequem auf die Interfaces zugreifen zu können, definieren wir noch zusätzlich das Interface Pocket, das Zugriff auf die jeweiligen Operationen gewährt.

pocket-api/src/main/kotlin/de/techdev/pocket/api/Pocket.kt
interface Pocket {
fun modifyOperations(): ModifyOperations

fun retrieveOperations(): RetrieveOperations
}

Die Deklaration der Funktionen modifyOperations und retrieveOperations erfolgt, wie oben beschrieben, unter Angabe des Namen und Rückgabetyps. Die in Pocket gespeicherten Artikel modellieren wir als Klasse wie folgt:

pocket-api/src/main/kotlin/de/techdev/pocket/api/Item.kt
class Item(val id: Long, val added: Long, val title: String)

Auch ohne explizite Angabe ist die Klasse Item als public und final gekennzeichnet. Das ist der Default in Kotlin und bedeutet in der Konsequenz, dass Klassen nicht offen für Vererbung sind, es sei denn, das ist explizit definiert. Neben dem Namen der Klasse folgt direkt der primäre Konstruktor. In diesem definieren wir drei Properties id, added und title, die später mit den Werten des jeweiligen Artikel befüllt werden. Aufrufe sehen bspw. so aus:

val item = Item(1, Date().time, "My Item")
println(item.title)

Der oben gezeigte Code gibt den String My Item auf der Konsole aus. Anders als in Java, werden Instanzen nicht mittels des Schlüsselwortes new erzeugt. Dieses fällt in Kotlin weg. Auf Properties kann mit der Punktnotation zugegriffen werden, sowohl lesend als auch schreibend. Da die Properties mit val gekennzeichnet sind, kann man auf diese nur lesend zugreifen. Der folgende Aufruf führt somit zu einem Fehler.

val item = Item(1, Date().time, "My Item")
item.title = "New Item"

Würden wir mit dem Item aus Java heraus arbeiten, so würde es nur einen Getter für den Zugriff auf den Titel geben. Wären die Properties mit var gekennzeichnet, ist eine neue Zuweisung möglich. Zusätzlich würde ein Setter erzeugt werden, um aus Java heraus den Wert setzen zu können. Um die Artikel nun auch laden zu können, definieren wir die Funktion items in den RetrieveOperations, die eine Collection zurückliefert.

pocket-api/src/main/kotlin/de/techdev/pocket/api/RetrieveOperations.kt
interface RetrieveOperations {
fun items(): Collection = emptyList()
}

Interface-Funktionen können eine Defaultimplementierung bereitstellen, was wir hier auch nutzen. So wird beim Aufruf der Funktion eine leere Liste zurückgeliefert. Für die Suche nach bestehenden Artikeln kann eine Reihe von Parametern angegeben werden. Darunter den Status der Artikel (ungelesen, archiviert oder alle), die Sortierreihenfolge (neueste zuerst, älteste zuerst, alphabetisch nach Titel oder URL sortiert) sowie den Detailgrad (einfach oder komplett). Diese Parameter wollen wir ebenfalls verwenden und können diese mit ihren erlaubten Ausprägungen als Enums implementieren.

pocket-api/src/main/kotlin/de/techdev/pocket/api/State.kt
enum class State(internal val value: String) {
UNREAD("unread"), ARCHIVE("archive"), ALL("all")
}

Enums werden in Kotlin mit dem Schlüsselwort enum definiert. Zusätzlich muss ebenfalls das Schlüsselwort class angegeben werden, was etwas umständlicher als die Java-Variante ist.

Wir definieren die drei erlaubten Werte und speichern jeweils in einem String den Wert (value), den das Pocket API erwartet. Dieser Wert ist ein Implementierungsdetail und kann sich gegebenenfalls ändern. Daher deklarieren wir ihn als internal, wodurch er nur innerhalb des Moduls pocket-api zugreifbar ist. Nutzer des APIs haben aber keinen Zugriff darauf. Um die Suchparameter auch zu verwenden, fügen wir sie als Parameter der items-Funktion zu.

pocket-api/src/main/kotlin/de/techdev/pocket/api/RetrieveOperations.kt
interface RetrieveOperations {
fun items(state: State = State.UNREAD, sort: Sort = Sort.OLDEST, details: Details = Details.SIMPLE): Collection = emptyList()
}

Kotlin ermöglicht es, Defaultwerte für Funktionsparameter zu definieren. So sollen per Default alle ungelesenen Artikel zurückgeliefert werden. Diese sollen sortiert sein: nach den ältesten zuerst und begrenzt auf Titel und URL (Details.SIMPLE), um nicht unnötig viele Daten zu transferieren. Da jeder Parameter mit einem Wert vorbelegt ist, kann die Funktion ohne Angabe der Parameter aufgerufen werden. Wollen wir hingegen nur einzelne Parameter ändern, so können wir diese beim Aufruf der Funktion anhand ihres Namens explizit setzen

pocket.retrieveOperations().items(state = State.ALL)

Parameternamen können nur beim Aufruf von Kotlin-Funktionen verwendet werden. Bei Aufrufen von Java-Methoden funktioniert dies noch nicht, da Kotlin die Kompatibilität zu Java 6 behält – aufgrund von Android. Das Speichern von Parameternamen im Bytecode ist aber erst ab Java 8 möglich.

Nun wird es Zeit unsere Interfaces zu implementieren. Wir beginnen mit dem zentralen Interface Pocket und der Implementierung PocketTemplate:

pocket-api/src/main/kotlin/de/techdev/pocket/PocketTemplate.kt
internal class PocketTemplate : Pocket {

private val modifyOperations: ModifyOperations
private val retrieveOperations: RetrieveOperations

init {
modifyOperations = ModifyTemplate()
retrieveOperations = RetrieveTemplate()
}

override fun modifyOperations() = modifyOperations

override fun retrieveOperations() = retrieveOperations
}

Die Klasse ist als internal gekennzeichnet und kann somit nur innerhalb des Moduls verwendet werden. Um von einer Klasse zu erben oder ein Interface zu implementieren wird der Doppelpunkt : verwendet. Innerhalb der Klasse erzeugen wir drei private Properties für die jeweiligen Operationen und initialisieren diese in einem init-Block. Dieser Block wird für den primären Konstruktor aufgerufen. Daher können wir hier die als val gekennzeichneten Properties initialisieren.

Wir implementieren auch die benötigten Funktionen addOperations, modifyOperations und retrieveOperations. Das Implementieren und überschreiben von Funktionen erfolgt mittels des Schlüsselwortes override. Die einzelnen Operationen implementieren wir ebenfalls als interne Klassen.

pocket-api/src/main/kotlin/de/techdev/pocket/RetrieveTemplate.kt
internal class RetrieveTemplate : RetrieveOperations
pocket-api/src/main/kotlin/de/techdev/pocket/ModifyTemplate.kt
internal class ModifyTemplate : ModifyOperations

Da das Interface RetrieveOperations nur eine Funktion enthält, die eine Defaultimplementierung aufweist, müssen wir die Funktion an dieser Stelle noch nicht implementieren. Da wir die Artikel in einer Collection verwenden wollen, empfiehlt es sich, die Funktionen equals und hashCode zu implementieren um keine unerwarteten Ergebnisse zu bekommen.

class Item(val id: Long, val added: Long, val title: String) {
override fun equals(other: Any?): Boolean {

}
override fun hashCode(): Int {

}
}

Bevor wir uns die Implementierung anschauen, lohnt sich noch ein Blick auf die von Kotlin verwendete Typenhierarchie. Wo in Java der Typ Object die Superklasse aller Klassen ist, ist das Gegenstück in Kotlin der Typ Any. Die Funktion equals erwartet aber eine Referenz vom Typen Any?Any? steht in der Hierarchie noch über Any und ist die Superklasse für alle Typen, die potenziell null sein können. Any ist dagegen die Superklasse für alle Typen, die nicht null sein können.

Somit unterscheidet Kotlin streng zwischen beiden Typen und versucht damit die zur Laufzeit auftretenden NullPointerException zu vermeiden. Mit den zusätzlichen Typinformationen ist es dem Compiler möglich, bereits vor der Ausführung des Programms zu verifizieren, dass es keine Zugriffe auf Referenzen gibt, die zur Laufzeit null sind. Der folgende Ausschnitt zeigt dies an einem Beispiel.

var name: String = null

Anders als String?, darf String nicht null sein. Der Versuch, das zu erzwingen, wird vom Compiler abgefangen. Ändern wir den Typen hingegen auf String?, compiliert das Programm ohne Fehler.

var name: String? = null

Wie wir Referenzen verwenden können, die nicht null sind, haben wir bereits gesehen. Doch wie verhält es sich mit Referenzen, die potenziell null sind? Zum Einen können wir, wie aus Java gewohnt, einen expliziten Check durchführen und den Typen dann entsprechend casten.

var name: String? = null

if (name != null) {
(name as String).reversed()
}

Wir prüfen, ob name nicht null ist und casten die Referenz anschließend von String? auf String, bevor wir die Funktion reversed aus der Kotlin-Standardbibliothek aufrufen. Der Aufruf kann aber noch kürzer gehalten werden.

var name: String? = null

if (name != null) {
name.reversed()
}

Der Compiler führt nach dem Check einen Smart Cast durch. Das heißt, der Cast wird implizit durch den Compiler erledigt, ohne dass wir dies erneut angeben müssen. Wie sieht nun also die Implementierung der equals-Funktion aus?

override fun equals(other: Any?): Boolean {
if (other is Item) {
return id.equals(other.id) and added.equals(other.added) and title.equals(other.title)
}

return false
}

Basierend auf allen Parametern des primären Konstruktors haben wir die Funktion equals implementiert, welche die Aufrufe an die einzelnen Properties delegiert. Oftmals folgen die Implementierungen von equals, hashCode oder auch toString festen Konventionen basierend auf Best Practices. Daher kann mittlerweile jede IDE diese Methoden generieren. Zusätzlich gibt es das Projekt Lombok, das basierend auf Annotations arbeitet. In Kotlin gibt es dafür ein Sprachkonstrukt, mit dessen Hilfe die Funktionen automatisch generiert werden:

pocket-api/src/main/kotlin/de/techdev/pocket/api/Item.kt
data class Item(val id: Long, val added: Long, val title: String)

Das Schlüsselwort data sorgt dafür, dass die Klasse Item-Implementierungen für die eben genannten Funktionen bekommt. Zusätzlich wurde eine weitere Funktion generiert: copy. Diese ermöglicht es, Objekte ganz einfach zu kopieren. Die Funktion belegt alle Parameter mit den aktuellen Werten der Instanz, auf der die Funktion aufgerufen wird. Wollen wir nur einzelne Properties überschreiben, können wir das bspw. wie folgt tun.

val item = Item(1, Date().time, "My Item")
val other = item.copy(id = 2)

Nachdem wir die Interfaces und die ersten Implementierungen entwickelt haben, müssen nun Instanzen der Typen erzeugt werden, damit diese verwendet werden können. Die einzelnen Operation werden in der internen Klasse PocketTemplate manuell erzeugt – aber wie kommen Nutzer an die Implementierung des Pocket Interfaces?

Neben Klassen und Interfaces kennt Kotlin zusätzlich Objekte, gekennzeichnet mit dem Schlüsselwort object. Objekte definieren einen Typen und erzeugen gleichzeitig die einzig mögliche Instanz dieses Typen. Hierbei handelt es sich als um Singletons. Zusätzlich können Objekte als Companions für bestehende Klassen oder Interfaces benutzt werden. Dies ist dann hilfreich, wenn sich die Objekte um die Erzeugung der jeweiligen Klassen kümmern.

pocket-api/src/main/kotlin/de/techdev/pocket/api/Pocket.kt
interface Pocket {

companion object {
fun connect(): Pocket = PocketTemplate()
}

}

Bei Bedarf können wir dem Objekt einen Namen geben, beispielsweise companion object Builder. Das ist aber nicht nötig, da Aufrufe des Objektes nach außen wie Aufrufe auf den umgebenden Typen aussehen. Das Companion-Objekt ermöglicht uns nun den folgenden Aufruf:

src/main/kotlin/de/techdev/janitor/Janitor.kt
val pocket = Pocket.connect()

Wir bekommen eine Implementierung von Pocket, nämlich PocketTemplate, beim Aufruf an das Objekt zurück, mit dem wir nun die verfügbaren Operationen aufrufen können. Damit haben wir die Basisstruktur der Anwendung erstellt und können im nächsten Schritt die Aufrufe des Pocket API implementieren. Der vollständige Code für den ersten Teil kann via git checkout tags/part-one ausgecheckt werden.

Fazit

Im ersten Teil dieses Tutorials haben wir bereits eine Menge der Features behandelt, die Kotlin ausmachen. Anhand der Beispielanwendung haben wir gesehen, dass die Unterteilung in Packages sich nicht in der Verzeichnisstruktur widerspiegeln muss und Packages nicht zur Kontrolle von Sichtbarkeiten verwendet werden. Wir haben die Schlüsselwörter var und val behandelt, welche für veränderliche bzw. unveränderliche Referenzen verwendet werden.

Funktionen spielen in Kotlin eine zentrale Rolle und können global oder lokal definiert werden, als Member einem bestimmten Typen zugeordnet sein oder bestehende Typen erweitern. Janitor nutzt den Zugriffsmodifikator internal für Typen, die außerhalb des API-Moduls nicht verwendet werden sollen. Daneben existieren die drei anderen Modifikatoren public, private und protected. Wir haben die ersten Klassen, Interfaces und Enums definiert und ebenfalls das Schlüsselwort data verwendet, das dafür sorgt, dass die entsprechenden Klassen Implementierungen für toString, hashCode, equals sowie copy erhalten.

Bezeichnend für Kotlin ist die Handhabung von null und nicht-null Typen, die im Typsystem voneinander getrennt sind. Wir haben die Verwendung von nullable-Typen gezeigt und auch, wie der Compiler uns mittels Smart Casts einiges an Arbeit abnimmt. Zuletzt haben wir den ersten Anwendungsfall für Objekte kennengelernt, zu dem in den folgenden Teilen noch weitere hinzukommen werden.

Geschrieben von
Alexander Hanschke
Alexander Hanschke
Alexander Hanschke arbeitet seit 2013 bei der techdev Solutions GmbH in Karlsruhe, wo er Kunden bei der erfolgreichen Umsetzung von IT-Projekten unterstützt.
Kommentare

Hinterlasse eine Antwort

Deine E-Mail-Adresse wird nicht veröffentlicht.