Kotlin Interfaces, Composition, and Delegation
An interface names the small capability a caller needs. This is stronger than depending on a concrete class because production code and tests can supply different implementations without changing the caller. Keep the interface focused, inject it at construction, and use delegation only when the exposed capability still makes sense for the wrapper.
Work through the programs in order. Each example changes one idea at a time, includes the expected result, and asks you to make a small change. That practice turns object-oriented vocabulary into code you can confidently write, review, and test.
What You Will Build
This is a code lab, not a compressed definition. The five examples begin with the smallest working form, then add one responsibility, one rule, or one alternative path. Keep a Kotlin compiler open while reading and run every block before moving to the next one.
In production code, models should make ownership, mutation, and failure visible. Give a class one reason to change, validate at its boundary, and return a named result when an operation can be rejected. Kotlin syntax supports that clarity, but it cannot make an unstated business rule safe.
Example 1: Define one focused capability
This interface promises only the current time. A caller does not need to know whether the answer came from the system clock, a test clock, or a remote service.
Kotlin
interface Clock {
fun now(): String
}
class FixedClock : Clock {
override fun now(): String = "09:30"
}
fun main() {
println(FixedClock().now())
}
Expected Result
09:30
Try next: Add a SystemClock later without changing the function that consumes Clock.
Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.
Example 2: Inject the capability
Constructor injection makes a class's dependency visible. The report can use any Clock implementation that honors the same small contract.
Kotlin
interface Clock {
fun now(): String
}
class FixedClock : Clock {
override fun now(): String = "09:30"
}
class Report(private val clock: Clock) {
fun label(): String = "Generated ${clock.now()}"
}
fun main() {
println(Report(FixedClock()).label())
}
Expected Result
Generated 09:30
Try next: Pass another Clock to Report. The Report code should not need an if statement for each implementation.
Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.
Example 3: Use an interface default method
A default method shares a small derived behavior while implementations still provide the primitive operation. Do not hide complex state or external effects in defaults.
Kotlin
interface Greeter {
fun name(): String
fun message(): String = "Hello, ${name()}"
}
class Student : Greeter {
override fun name(): String = "Mina"
}
fun main() {
println(Student().message())
}
Expected Result
Hello, Mina
Try next: Override message only if a Student really has a different greeting contract.
Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.
Example 4: Delegate a small interface
by forwards an interface implementation to another object. The wrapper keeps its own responsibility while avoiding repetitive pass-through methods.
Kotlin
interface Reader {
fun read(): String
}
class MemoryReader : Reader {
override fun read(): String = "cached note"
}
class AuditedReader(private val source: Reader) : Reader by source {
fun auditLabel(): String = "read requested"
}
fun main() {
val reader = AuditedReader(MemoryReader())
println("${reader.auditLabel()}: ${reader.read()}")
}
Expected Result
read requested: cached note
Try next: Keep delegation narrow. If the interface exposes twenty unrelated methods, redesign its boundary first.
Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.
Example 5: Use a fake in a small test-style check
A fake implementation lets you test the caller without network or time dependencies. This is the practical value of coding to an interface.
Kotlin
interface TitleSource {
fun load(): String
}
class FakeTitleSource : TitleSource {
override fun load(): String = "Kotlin lesson"
}
fun headline(source: TitleSource): String = source.load().uppercase()
fun main() {
println(headline(FakeTitleSource()))
}
Expected Result
KOTLIN LESSON
Try next: Make FakeTitleSource return an empty title and decide whether headline should reject, replace, or display that value.
Trace the program from construction through its printed result. Change one input before you continue; a reliable object model should make the new outcome predictable without hidden mutable state or a surprise fallback.
Example 6: Make the input visible
Start by naming the value the next line depends on. This keeps the lesson input explicit before we apply a transformation or call an external boundary. We are applying that habit while revisiting Kotlin Interfaces, Composition, and Delegation.
Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.
Kotlin
fun review(value: String?): String =
value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
?: "needs a value"
fun main() {
println(review("Kotlin Interfaces Composition and Delegation"))
}Expected Result
KOTLIN INTERFACES COMPOSITION AND DELEGATION
Try next: Replace the sample value with one from your own notes, predict the result, and then run it.
Example 7: Guard an empty boundary
A small guard makes the allowed and rejected path visible. We want a caller to receive an intentional result instead of discovering an unexplained failure later. We are applying that habit while revisiting Kotlin Interfaces, Composition, and Delegation.
Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.
Kotlin
fun review(value: String?): String =
value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
?: "needs a value"
fun main() {
println(review("Kotlin Interfaces Composition and Delegation"))
}Expected Result
KOTLIN INTERFACES COMPOSITION AND DELEGATION
Try next: Try an empty value as well as the normal value and describe why the two outcomes differ.
Example 8: Keep the transformation pure
This step returns a value instead of mixing the rule with display or storage work. That separation gives us a small unit we can run, test, and reuse. We are applying that habit while revisiting Kotlin Interfaces, Composition, and Delegation.
Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.
Kotlin
fun review(value: String?): String =
value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
?: "needs a value"
fun main() {
println(review("Kotlin Interfaces Composition and Delegation"))
}Expected Result
KOTLIN INTERFACES COMPOSITION AND DELEGATION
Try next: Call the same rule twice with different inputs and confirm that each result depends only on its input.
Example 9: Make a branch deliberate
Use a named branch where the business rule changes. A future reader should be able to point at the condition and state the result for both sides. We are applying that habit while revisiting Kotlin Interfaces, Composition, and Delegation.
Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.
Kotlin
fun review(value: String?): String =
value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
?: "needs a value"
fun main() {
println(review("Kotlin Interfaces Composition and Delegation"))
}Expected Result
KOTLIN INTERFACES COMPOSITION AND DELEGATION
Try next: Change the boundary value by one and explain exactly which branch becomes active.
Example 10: Use a collection without hidden mutation
Collections become safer when we can see which value is read, which value is derived, and whether the original collection remains owned by the caller. We are applying that habit while revisiting Kotlin Interfaces, Composition, and Delegation.
Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.
Kotlin
fun review(value: String?): String =
value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
?: "needs a value"
fun main() {
println(review("Kotlin Interfaces Composition and Delegation"))
}Expected Result
KOTLIN INTERFACES COMPOSITION AND DELEGATION
Try next: Add one more item and predict the derived result before executing the code.
Example 11: Return a named outcome
A named outcome is easier to test than an implied side effect. We can now distinguish a successful path from a rejected path without guessing from console noise. We are applying that habit while revisiting Kotlin Interfaces, Composition, and Delegation.
Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.
Kotlin
fun review(value: String?): String =
value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
?: "needs a value"
fun main() {
println(review("Kotlin Interfaces Composition and Delegation"))
}Expected Result
KOTLIN INTERFACES COMPOSITION AND DELEGATION
Try next: Rename the outcome in your own words while preserving the same success and failure rule.
Example 12: Trace a small loop
Work through one value at a time. The point is not to memorize syntax; it is to see how the accumulator or output changes after each repeatable step. We are applying that habit while revisiting Kotlin Interfaces, Composition, and Delegation.
Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.
Kotlin
fun review(value: String?): String =
value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
?: "needs a value"
fun main() {
println(review("Kotlin Interfaces Composition and Delegation"))
}Expected Result
KOTLIN INTERFACES COMPOSITION AND DELEGATION
Try next: Use a shorter collection first, then add one value and trace the extra iteration aloud.
Example 13: Keep display at the edge
The core rule prepares a value, while the outer line decides how to show it. That keeps the result usable by a UI, test, API, or command-line caller. We are applying that habit while revisiting Kotlin Interfaces, Composition, and Delegation.
Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.
Kotlin
fun review(value: String?): String =
value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
?: "needs a value"
fun main() {
println(review("Kotlin Interfaces Composition and Delegation"))
}Expected Result
KOTLIN INTERFACES COMPOSITION AND DELEGATION
Try next: Change only the display text and verify that the underlying rule and result stay unchanged.
Example 14: Test the closest boundary
A useful example includes the nearest boundary, not only the happy path. We are making the behavior inspectable before a real caller relies on it. We are applying that habit while revisiting Kotlin Interfaces, Composition, and Delegation.
Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.
Kotlin
fun review(value: String?): String =
value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
?: "needs a value"
fun main() {
println(review("Kotlin Interfaces Composition and Delegation"))
}Expected Result
KOTLIN INTERFACES COMPOSITION AND DELEGATION
Try next: Try the lowest supported value and then one value just outside the accepted range.
Example 15: Write a tiny regression check
A compact assertion turns the lesson into a repeatable promise. It helps us catch a changed rule before a larger program hides the source of the mistake. We are applying that habit while revisiting Kotlin Interfaces, Composition, and Delegation.
Keep the example small enough to trace. First predict the normal result; then change one input and explain the boundary result in your own words.
Kotlin
fun review(value: String?): String =
value?.trim()?.takeIf { it.isNotEmpty() }?.uppercase()
?: "needs a value"
fun main() {
println(review("Kotlin Interfaces Composition and Delegation"))
}Expected Result
KOTLIN INTERFACES COMPOSITION AND DELEGATION
Try next: Deliberately change the expected value once, observe the failed check, and then restore the correct contract.
Practice Checklist
Reproduce all five programs from memory before moving forward. Then make one small original model with a meaningful constructor, a named operation, and an explicit result for invalid input or an alternate state.
- Practice 1: recreate “Define one focused capability” without copying, change one value, and explain the result before running it.
- Practice 2: recreate “Inject the capability” without copying, change one value, and explain the result before running it.
- Practice 3: recreate “Use an interface default method” without copying, change one value, and explain the result before running it.
- Practice 4: recreate “Delegate a small interface” without copying, change one value, and explain the result before running it.
- Practice 5: recreate “Use a fake in a small test-style check” without copying, change one value, and explain the result before running it.
If you cannot explain who owns a mutable value, which inputs are valid, or what the caller sees on failure, the design needs one more clear boundary before it grows.
Common Mistakes
- Making every property public and mutable before defining the class operation that should own the change.
- Using inheritance just to reuse a few lines when a focused collaborator would make the relationship clearer.
- Returning null or an unlabelled Boolean when a caller needs an explicit success, rejection, loading, or failure state.
- Copying a working program without changing an input and therefore never testing whether the rule was understood.
