SOLID-Prinzipien in Kotlin: Von der Theorie zur Praxis
\n\nSOLID ist ein Akronym für fünf fundamentale Prinzipien der objektorientierten Programmierung, formuliert von Robert Martin. Diese Prinzipien helfen dabei, flexiblen, wartbaren und skalierbaren Code zu erstellen. In diesem Artikel werden wir jedes SOLID-Prinzip anhand von Beispielen aus der realen Entwicklung mit Kotlin im Detail analysieren.
\n\nDas Verständnis von SOLID ist für jeden Kotlin-Entwickler von entscheidender Bedeutung, insbesondere bei der Arbeit mit Frameworks wie Spring Boot oder Ktor. Diese Prinzipien bilden die Grundlage für saubere Architektur und Entwurfsmuster.
\n\n1. Single Responsibility Principle (SRP) — Prinzip der einzigen Verantwortung
\n\nKernaussage: Jede Klasse sollte nur einen Grund für eine Änderung haben. Mit anderen Worten, die Klasse sollte für eine einzige Funktionalität verantwortlich sein.
\n\nIn Kotlin ist dieses Prinzip aufgrund der Unterstützung des funktionalen Stils besonders wichtig. Betrachten wir ein schlechtes Beispiel:
\n\n// ❌ Verletzung des SRP: Die Klasse kümmert sich sowohl um das Logging als auch um die Datenbankarbeit\nclass UserService {\n fun saveUser(user: User) {\n // Logging\n println("Saving user: ${user.name}")\n // Geschäftslogik\n validateUser(user)\n // Datenbankarbeit\n database.insert(user)\n }\n}\n\n\nBeheben wir die Situation, indem wir die Verantwortlichkeiten trennen:
\n\n// ✅ Korrekter Ansatz: Jede Verantwortung in einer eigenen Klasse\nclass Logger {\n fun log(message: String) = println("[LOG] $message")\n}\n\nclass UserValidator {\n fun validate(user: User): Boolean = user.name.isNotBlank()\n}\n\nclass UserRepository {\n fun save(user: User) { /* Speichern in der DB */ }\n}\n\nclass UserService(\n private val logger: Logger,\n private val validator: UserValidator,\n private val repository: UserRepository\n) {\n fun saveUser(user: User) {\n logger.log("Saving user: ${user.name}")\n if (validator.validate(user)) {\n repository.save(user)\n }\n }\n}\n\n\n2. Open-Closed Principle (OCP) — Prinzip der Offenheit/Geschlossenheit
\n\nKernaussage: Klassen sollten für Erweiterungen offen, aber für Modifikationen geschlossen sein. Neue Funktionalität sollte durch Vererbung oder Komposition hinzugefügt werden, ohne vorhandenen Code zu ändern.
\n\nIn Kotlin eignen sich versiegelte Klassen (sealed classes) und Interfaces hervorragend zur Umsetzung des OCP:
\n\n// ❌ Verletzung des OCP: Beim Hinzufügen eines neuen Figurentyps muss die Klasse geändert werden\nclass AreaCalculator {\n fun calculate(shape: Any): Double = when (shape) {\n is Circle -> 3.14 * shape.radius * shape.radius\n is Rectangle -> shape.width * shape.height\n else -> throw IllegalArgumentException()\n }\n}\n\n\nKorrekte Implementierung unter Verwendung von Polymorphismus:
\n\n// ✅ Korrekter Ansatz: Neue Figuren werden hinzugefügt, ohne vorhandenen Code zu ändern\ninterface Shape {\n fun area(): Double\n}\n\nclass Circle(val radius: Double) : Shape {\n override fun area() = Math.PI * radius * radius\n}\n\nclass Rectangle(val width: Double, val height: Double) : Shape {\n override fun area() = width * height\n}\n\n// Jetzt kann ein Dreieck hinzugefügt werden, ohne AreaCalculator zu ändern\nclass Triangle(val base: Double, val height: Double) : Shape {\n override fun area() = 0.5 * base * height\n}\n\nclass AreaCalculator {\n fun calculate(shape: Shape) = shape.area()\n}\n\n\n3. Liskov Substitution Principle (LSP) — Prinzip der Substitution von Barbara Liskov
\n\nKernaussage: Objekte einer Basisklasse sollten durch Objekte abgeleiteter Klassen ersetzt werden können, ohne die Funktionsweise des Programms zu beeinträchtigen. Eine Unterklasse sollte Vorbedingungen nicht abschwächen oder Nachbedingungen verstärken.
\n\nBetrachten wir das klassische Beispiel mit Rechteck und Quadrat:
\n\n// ❌ Verletzung des LSP: Ein Quadrat kann ein Rechteck nicht ersetzen\nopen class Rectangle(\n open var width: Int,\n open var height: Int\n) {\n fun area() = width * height\n}\n\nclass Square(sid