SOLID Principles in Kotlin: Theory and Practice at an Advanced Level
SOLID is a fundamental set of object-oriented design principles introduced by Robert Martin. In the context of Kotlin, which combines the best features of OOP and functional programming, these principles gain new implementation possibilities. In this article, we will examine each principle at an advanced level, using unique language features: inline functions, delegation, sealed classes, and extension functions.
1. Single Responsibility Principle (SRP) and Delegation
The Single Responsibility Principle states: a class should have only one reason to change. In Kotlin, the Delegation pattern is often used to comply with SRP. The by keyword allows automatically delegating interface implementation to another object.
Problem: The UserService class is responsible for business logic, logging, and sending emails.
// ❌ SRP Violationclass UserService { fun registerUser(email: String, password: String) { // Business logic println("User registered") // Logging log("User registered: $email") // Sending email sendEmail(email, "Welcome!") }
private fun log(message: String) { /* ... */ } private fun sendEmail(to: String, body: String) { /* ... */ }}Solution using delegation:
// ✅ SRP Compliance via Delegationinterface Logger { fun log(message: String)}
interface EmailSender { fun send(to: String, body: String)}
class ConsoleLogger : Logger { override fun log(message: String) = println("LOG: $message")}
class SmtpEmailSender : EmailSender { override fun send(to: String, body: String) = println("Sending email to $to: $body")}
class UserService( private val logger: Logger, private val emailSender: EmailSender) : Logger by logger, EmailSender by emailSender { // 👈 Delegation
fun registerUser(email: String, password: String) { println("User registered") log("User registered: $email") send(email, "Welcome!") }}
// Usagefun main() { val service = UserService(ConsoleLogger(), SmtpEmailSender()) service.registerUser("user@example.com", "pass123")}Using by avoids boilerplate code and clearly separates responsibilities. The UserService class now only handles registration, while logging and sending are delegated to separate classes.
2. Open-Closed Principle (OCP) and Sealed Classes
The Open-Closed Principle: classes should be open for extension but closed for modification. In Kotlin, sealed classes and extension functions are ideal for this.
Problem: The processPayment function uses when with type checking, and adding a new payment method requires modifying it.
// ❌ OCP Violationfun processPayment(type: String, amount: Double) { when (type) { "card" -> println("Processing card payment: $amount") "paypal" -> println("Processing PayPal payment: $amount") else -> throw IllegalArgumentException("Unknown payment type") }}Solution with sealed classes:
// ✅ OCP Compliancesealed class PaymentMethod { data class Card(val number: String, val cvv: String) : PaymentMethod() data class PayPal(val email: String) : PaymentMethod() object Cash : PaymentMethod()}
// Function is closed for modificationfun processPayment(method: PaymentMethod, amount: Double) { // When new subclasses appear, the compiler will require handling them