SOLID Principles in Kotlin: Theory and Practice from A to Z

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

SOLID Principles in Kotlin: From Theory to Practice

\n\n

SOLID is an acronym for five fundamental principles of object-oriented programming formulated by Robert Martin. These principles help create flexible, maintainable, and scalable code. In this article, we will thoroughly examine each SOLID principle with examples from real-world Kotlin development.

\n\n

Understanding SOLID is critically important for any Kotlin developer, especially when working with frameworks like Spring Boot or Ktor. These principles form the foundation of clean architecture and design patterns.

\n\n

1. Single Responsibility Principle (SRP)

\n\n

Essence: Each class should have only one reason to change. In other words, a class should be responsible for a single functionality.

\n\n

In Kotlin, this principle is especially important due to support for functional style. Consider a bad example:

\n\n
// ❌ SRP violation: the class handles both logging and database operations\nclass UserService {\n    fun saveUser(user: User) {\n        // Logging\n        println("Saving user: ${user.name}")\n        // Business logic\n        validateUser(user)\n        // Database operations\n        database.insert(user)\n    }\n}\n
\n\n

Let's fix the situation by separating responsibilities:

\n\n
// ✅ Correct approach: each responsibility in a separate class\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) { /* saving to 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\n

2. Open-Closed Principle (OCP)

\n\n

Essence: Classes should be open for extension but closed for modification. New functionality should be added through inheritance or composition without changing existing code.

\n\n

In Kotlin, sealed classes and interfaces are excellent for implementing OCP:

\n\n
// ❌ OCP violation: adding a new shape type requires modifying the class\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\n

Correct implementation using polymorphism:

\n\n
// ✅ Correct approach: new shapes are added without modifying existing code\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// Now you can add a triangle without changing AreaCalculator\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\n

3. Liskov Substitution Principle (LSP)

\n\n

Essence: Objects of a base class should be replaceable with objects of derived classes without breaking the program. A subclass should not weaken preconditions or strengthen postconditions.

\n\n

Consider the classic example with a rectangle and a square:

\n\n
// ❌ LSP violation: a square cannot replace a rectangle\nopen class Rectangle(\n    open var width: Int,\n    open var height: Int\n) {\n    fun area() = width * height\n}\n\nclass Square(sid                

Blogs

Book Recommendations