Kotlin / Reusable behavior and typed boundaries / Lesson 11
Compose behavior with interfaces, delegation, and objects
Define capability contracts, forward behavior with by, and choose explicit object lifetimes instead of hidden shared state.
On this page
Lesson goals
- Explain interface properties, implementation conflicts, and delegated calls.
- Distinguish an object declaration, object expression, and companion object.
Use an interface for a coherent behavior contract
An interface can declare functions, provide default implementations, and describe properties through abstract declarations or accessors. It does not own backing fields for interface properties. Implementations supply actual state where needed. A small LabelSource interface can separate a caller’s need for text from how text is produced. Avoid a universal interface that combines unrelated storage, mail, and rendering operations. Narrow capabilities are easier to substitute and test, but their error, blocking, and ownership semantics still require documentation beyond method signatures.
Make the implementation and caller responsibilities visible
The complete sample supplies one implementation and a function that accepts the interface. The caller asks only for label and does not construct a provider internally. This makes substitution straightforward without implying a framework or runtime discovery mechanism. If several interfaces provide the same inherited implementation, a class may need to override and resolve the conflict explicitly using qualified super calls. A shared method name does not prove the meanings are compatible; examine the behavioral promises before combining unrelated interfaces in one class.
interface LabelSource { fun label(): String }
class FixedLabel(private val value: String) : LabelSource {
override fun label(): String = value
}
fun render(source: LabelSource): String = "[${source.label()}]"
fun main() {
println(render(FixedLabel("workshop")))
val local = object : LabelSource {
override fun label(): String = "local"
}
println(render(local))
}
[workshop]
[local]
Expected output is [workshop] and [local]. The object expression constructs a local implementation for this call context. No external provider or Kotlin compiler is invoked.
Delegation forwards a contract to another object
A class implementing an interface by a delegate can let the compiler generate forwarding members. The wrapper may override selected members, but calls made internally by the delegate still operate on the delegate’s own implementation and state. Do not assume overriding a wrapper property transparently changes the delegate’s internal reads. Delegation is composition with forwarding, not inheritance of the delegate’s implementation identity. It is useful for a focused decorator, but ownership, resource closure, exception translation, and thread safety remain responsibilities the wrapper must define rather than properties created by by.
Override one operation while forwarding the rest
The sample interface has two independent operations. The wrapper changes label while forwarding code to its delegate. The result is easy to predict because neither delegated method calls another interface method internally. For a real decorator, include tests for interactions inside the delegate and for preserved failure behavior. Do not use delegation to conceal a different business meaning behind the same interface. If the wrapper narrows valid input or changes whether an operation is durable, callers need an explicit contract change rather than an invisible implementation trick.
interface WorkshopInfo {
fun label(): String
fun code(): String
}
class BasicInfo : WorkshopInfo {
override fun label(): String = "Basics"
override fun code(): String = "ws-1"
}
class DecoratedInfo(private val delegate: WorkshopInfo) : WorkshopInfo by delegate {
override fun label(): String = "Featured: ${delegate.label()}"
}
fun main() {
val info = DecoratedInfo(BasicInfo())
println(info.label())
println(info.code())
}
Featured: Basics
ws-1
Expected output is Featured: Basics and ws-1. code is forwarded and label is overridden. The source was not compiled here, and no resource or concurrency guarantee is implied.
Singleton identity does not make mutable state safe
An object declaration provides a named singleton, while an object expression creates an anonymous object at its evaluation site. A companion object belongs to a class and is often used for factories or related constants; its functions are not automatically ordinary Java static methods without applicable interop support. A singleton with a mutable counter is still shared mutable state and needs concurrency and reset semantics. Prefer passing an owned dependency when tests or separate application instances need independent state. Global accessibility is a lifetime decision with costs, not a substitute for clear composition.
Common errors and fixes
An interface property attempts to store a backing field.
- Why it happens
- An interface contract was used as a stateful class.
- How to fix it
- Supply abstract accessors and keep storage in an implementation.
- Prevent it next time
- Separate capability description from state ownership.
A wrapper override does not affect delegate-internal behavior.
- Why it happens
- Forwarding was mistaken for virtual inheritance.
- How to fix it
- Review the delegate’s own calls and override the full required behavior explicitly.
- Prevent it next time
- Test interactions rather than only direct wrapper calls.
Tests leak state through a mutable singleton.
- Why it happens
- Global lifetime was selected without reset or ownership rules.
- How to fix it
- Inject independent instances where isolation is required.
- Prevent it next time
- Keep singleton state minimal and document shared mutation.
Check your understanding
1. Does by automatically close a delegated file resource?
Show worked solution
No. It forwards the declared interface members. Resource ownership and closure must be represented and implemented explicitly, including whether the wrapper or caller owns the delegate. Avoid inventing lifecycle behavior from concise syntax. Verify cleanup at the actual resource boundary when a real implementation exists.
2. When is an object expression preferable to a named singleton test double?
Show worked solution
When one local scenario needs a small implementation with its own captured data and no globally shared state. A named class may be clearer for repeated or complex behavior. Choose a lifetime matching the test and avoid sharing mutable fixtures across unrelated cases merely to reduce declarations.
Key takeaways
- Interfaces express capability; implementations own state and lifecycle.
- Delegation forwards behavior without creating hidden safety guarantees.