How do I apply the composition-over-inheritance pattern in EK9?
← Design Patterns and Idioms · Ref: Q212
EK9 types are closed by default, encouraging composition over inheritance. Wrap a collection or type as a private field and expose only the operations you need.
COMPOSITION PATTERN
Wrap a collection as a private field:
TaskQueue
items as List of String
Expose only the methods that make sense:
addTask(), pendingCount(), hasWork()
MULTI-CONCERN COMPOSITION
Composition shines when wrapping multiple concerns. ValidatedConfig wraps a Dict and adds validation:
ValidatedConfig
entries as Dict of (String, String)
get() with default, set() with validation, has() for existence
The Dict is hidden behind a controlled API that enforces business rules.
WHY COMPOSITION
Built-in types like List and Dict are closed (cannot be extended). This prevents mixing collection behavior with application logic. Composition gives you control over the public API.
WHEN INHERITANCE IS APPROPRIATE
Inheritance is still correct for genuine IS-A relationships. Use 'as open' on the base class and extend when the subtype truly specializes the base.
CONTROLLED INTERFACE
With composition, clients see only:
config.get(key), config.set(key, value), config.has(key)
Not the full Dict API (merge, replace, iterate, etc.).
See Q101 for closed by default. See Q128 for closed collections. See Q109 for composition. See Q210 for trait delegation. See Q264 for adapter pattern using composition. See Q266 for cross-cutting concerns via delegation. See Q278 for why AI extends closed types. See Q315 for inheritance depth limits that encourage composition.
Example
defines module qa.patterns.composition defines class TaskQueue items as List of String: List() of String default TaskQueue() addTask() -> task as String items += task pendingCount() as pure <- rtn as Integer: length items hasWork() as pure <- rtn as Boolean: length items > 0 default operator ? // Multi-concern composition: Dict + validation ValidatedConfig entries as Dict of (String, String): Dict() of (String, String) default ValidatedConfig() get() -> key as String <- rtn as String: entries.getOrDefault(key, "") set() -> key as String setting as String <- rtn as Boolean: false if key? and setting? entries += DictEntry(key, setting) rtn: true has() as pure -> key as String <- rtn as Boolean: entries contains key entryCount() as pure <- rtn as Integer: length entries default operator ? defines program CompositionDemo() stdout <- Stdout() // === TASK QUEUE: LIST COMPOSITION === queue <- TaskQueue() queue.addTask("Build feature") queue.addTask("Write tests") queue.addTask("Review code") stdout.println(`Pending tasks: ${queue.pendingCount()}`) stdout.println(`Has work: ${queue.hasWork()}`) // === VALIDATED CONFIG: DICT COMPOSITION === config <- ValidatedConfig() hostName <- "host" config.set(hostName, "localhost") config.set("port", "8080") stdout.println(`Has host: ${config.has(hostName)}`) stdout.println(`Host: ${config.get(hostName)}`) stdout.println(`Entries: ${config.entryCount()}`) // Clients use get/set/has, not the full Dict API stdout.println("Composition controls the public API")
Common mistakes
E50060 — Dict does not have a get() method. Use getOrDefault() with a fallback value. Calling a non-existent method triggers E50060 — method not resolved. See ek9 -h E50060 for details.
Incorrect:
rtn as String: entries.get(key)
Correct:
rtn as String: entries.getOrDefault(key, "")
Other ways to ask this
- Why does EK9 prefer composition over inheritance?
- How do I wrap a collection in EK9?
- How do I encapsulate a List in EK9?
Coming from another language?
Java: ArrayList<T> is open, commonly extended (bad practice). Python: list is open. Rust: no inheritance, composition by default. Go: embedding for delegation. Kotlin: classes final by default like EK9. EK9: types closed by default, composition is the natural and preferred pattern.
Keywords: closed, migrate, delegate, wrap, inheritance, encapsulate, idiom, design, composition, pattern