ProgrammingBackend Developer

Talk about the features of composition and inheritance in Kotlin. How does the language recommend building class hierarchies and why is composition often preferred over inheritance? How to implement the delegation pattern in Kotlin?

Pass interviews with Hintsage AI assistant

Answer

Kotlin supports inheritance parameters through the open keyword, but the main recommendation of the language is to favor composition over inheritance. This allows for the creation of more flexible, extensible systems, avoiding the fragility of hierarchies and problems associated with deep inheritance.

Composition involves including an object of the desired type as a class field and delegating work to it instead of inheriting its implementation. Kotlin simplifies the delegation pattern using the by keyword, allowing automatic delegation of interface implementations to objects.

Example of delegation pattern:

interface Logger { fun log(message: String) } class ConsoleLogger : Logger { override fun log(message: String) = println(message) } class Service(private val logger: Logger) : Logger by logger { fun doAction() { log("Action done") } } fun main() { val service = Service(ConsoleLogger()) service.doAction() // Output: Action done }

This approach simplifies code reuse and makes logic more modular.

Trick question

"Can a data class inherit from another class, for example from an abstract class?"

  • Answer: data class in Kotlin cannot inherit from another class (except interfaces) because data class is always final. The exception is interfaces, which can be implemented.

Example:

abstract class Base(val name: String) data class Derived(val age: Int, val name: String) : Base(name) // Compilation error: data class cannot extend Base class

But it is possible:

interface User data class Admin(val name: String, val rights: List<String>) : User

Examples of real mistakes due to lack of knowledge of the topic


Story

In the project, it was decided to inherit several services from a common abstract class to implement repeating logic. As a result, there were multiple levels of inheritance, debugging became complicated, and testing issues arose. After switching to composition and delegation (through interfaces and dependency injection), the code was simplified, made more modular, and test coverage increased.


Story

A beginner developer tried to extend a data class using another class to add common functionality. The code did not compile, but the programmer could not understand the reason for a long time (the restrictions of data class in Kotlin).


Story

In a project with extensive logging logic, it was decided to extract logging functionality into a base class. However, as the system grew, some services required different logging implementations. Refactoring was necessary using the Logger interface and composition through delegation, which significantly simplified the architecture.