OOP in Kotlin: Interfaces, Abstract Classes & Composition
Design patterns and OOP principles that define senior Android architecture
Open interactive version (quiz + challenge)Real-world analogy
What is it?
OOP in Kotlin centers around composition over inheritance, explicit openness (classes are final by default), and powerful interface capabilities including default methods and delegation. Senior Android interviews test whether you know when to use abstract classes vs interfaces, how to design for testability through dependency injection, and how to apply SOLID principles in Android architecture.
Real-world relevance
In a fintech claims processing app, you'd define a ClaimProcessor interface with validate() and process() methods. A BaseClaimProcessor abstract class provides shared audit logging and error handling. Specific processors (AutoClaimProcessor, ManualClaimProcessor) compose specialized services (FraudDetector, DocumentValidator) rather than inheriting them. The 'by' delegation creates a LoggingClaimRepository that wraps the real repository without subclassing it.
Key points
- Primary vs secondary constructors — Primary constructor is in the class header: class User(val id: Int, val name: String). Secondary constructors use constructor keyword and must delegate to primary with this(). In Kotlin, prefer primary constructors + default parameter values over multiple secondary constructors.
- init block — init { } runs as part of the primary constructor. Used for validation: init { require(name.isNotBlank()) { 'Name cannot be blank' } }. Multiple init blocks run in declaration order interleaved with property initializers.
- open keyword — Kotlin classes are final by default (cannot be subclassed). You must explicitly mark a class open to allow inheritance. This is the opposite of Java and encourages composition over inheritance by making inheritance opt-in.
- abstract classes — abstract class defines a contract with partial implementation. abstract fun process() must be overridden. Non-abstract methods provide shared behavior. Cannot be instantiated directly. Use when subclasses share significant implementation.
- interfaces with default methods — Kotlin interfaces can have default method implementations (unlike Java 7 interfaces). interface Validator { fun validate(input: String): Boolean; fun isValid(input: String) = validate(input) }. Classes can implement multiple interfaces.
- Composition over inheritance — Instead of extending a class to reuse behavior, hold a reference to it. class OrderRepository(private val db: Database, private val api: ApiService). This avoids fragile base class problem, is easier to test (mock dependencies), and is the foundation of Clean Architecture.
- Interface delegation with by — class LoggingList(private val delegate: MutableList) : MutableList by delegate — delegates all MutableList methods to delegate, overriding only what you need. Kotlin's by keyword generates all the delegation boilerplate.
- Sealed classes for type hierarchies — sealed class restricts subclasses to the same file/package. When used in when expressions, the compiler verifies exhaustiveness. Perfect for Result types, navigation events, UI states — any bounded set of types.
- Override rules — override fun must be explicit in Kotlin. You can override a val with var (widening) but not var with val. Final override prevents further subclassing: final override fun toString().
- Abstract vs Interface: when to choose — Use abstract class when: subclasses share state (fields), you need constructor parameters, or there's significant shared implementation. Use interface when: defining a contract without state, multiple inheritance is needed, or you want maximum flexibility for testing/mocking.
- Data class inheritance restrictions — data classes cannot be extended by other data classes (they can extend abstract classes/interfaces). Trying to extend a data class with another data class is a compile error. Use sealed class hierarchies instead.
- Visibility modifiers — Kotlin defaults to public (opposite of Java). private: file/class scope. protected: class and subclasses. internal: same Gradle module. In Clean Architecture, use internal for implementation classes and expose only interfaces publicly.
Code example
// Interface with default methods
interface ClaimProcessor {
fun validate(claim: Claim): ValidationResult
fun process(claim: Claim): ProcessingResult
// Default method — shared across all implementations
fun canProcess(claim: Claim): Boolean = validate(claim).isValid
}
// Abstract class with shared implementation
abstract class BaseClaimProcessor(
private val auditLogger: AuditLogger
) : ClaimProcessor {
override fun process(claim: Claim): ProcessingResult {
auditLogger.log("Processing claim: ${claim.id}")
return doProcess(claim).also { result ->
auditLogger.log("Claim ${claim.id} result: ${result.status}")
}
}
// Template method pattern — subclasses fill in the logic
protected abstract fun doProcess(claim: Claim): ProcessingResult
}
// Composition — holds specialists, doesn't inherit them
class AutoClaimProcessor(
auditLogger: AuditLogger,
private val fraudDetector: FraudDetector, // composed
private val documentValidator: DocumentValidator // composed
) : BaseClaimProcessor(auditLogger) {
override fun validate(claim: Claim): ValidationResult {
val fraudCheck = fraudDetector.check(claim)
val docCheck = documentValidator.validate(claim.documents)
return ValidationResult(fraudCheck.passed && docCheck.passed)
}
override fun doProcess(claim: Claim): ProcessingResult =
ProcessingResult(status = Status.AUTO_APPROVED, claimId = claim.id)
}
// Interface delegation with 'by'
class LoggingClaimRepository(
private val delegate: ClaimRepository,
private val logger: Logger
) : ClaimRepository by delegate {
override fun save(claim: Claim): Claim {
logger.d("Saving claim ${claim.id}")
return delegate.save(claim) // intercept only save()
}
}Line-by-line walkthrough
- 1. interface ClaimProcessor with default method canProcess() — implementors get canProcess() for free; must implement validate() and process()
- 2. abstract class BaseClaimProcessor(private val auditLogger: AuditLogger) — constructor injection of a dependency; auditLogger is shared across all subclasses
- 3. override fun process() calls auditLogger before and after — template method pattern; subclasses override doProcess() not process()
- 4. protected abstract fun doProcess() — forces subclasses to provide the core logic; protected means subclass-visible only
- 5. class AutoClaimProcessor(...) : BaseClaimProcessor(auditLogger) — passes auditLogger up to parent via constructor delegation
- 6. private val fraudDetector: FraudDetector — COMPOSITION: AutoClaimProcessor HAS a FraudDetector, doesn't extend it
- 7. class LoggingClaimRepository(...) : ClaimRepository by delegate — compiler generates all ClaimRepository methods forwarding to delegate
- 8. override fun save() — only save() is intercepted; all other ClaimRepository methods pass through to delegate automatically
Spot the bug
interface Syncable {
fun sync(): Boolean
fun lastSyncTime(): Long = System.currentTimeMillis()
}
class DataRepository : Syncable {
fun sync() = true // missing override keyword
}
abstract class BaseViewModel {
abstract fun loadData()
fun refresh() = loadData()
}
class UserViewModel : BaseViewModel() {
fun loadData() { // missing override keyword
fetchUsers()
}
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Classes and Inheritance — Kotlin (kotlinlang.org)
- Interfaces in Kotlin (kotlinlang.org)
- Delegation Pattern in Kotlin (kotlinlang.org)
- Android Architecture: Repository Pattern (developer.android.com)