How does trait delegation with 'by' work in EK9?
← Design Patterns and Idioms · Ref: Q210
EK9 supports trait delegation with the 'by' keyword. A class can implement a trait and delegate unoverridden methods to a field.
TRAIT DELEGATION SYNTAX
Declare a class that delegates trait methods:
FilteredLogger with trait of Logger by delegate
Here 'delegate' is a field of type Logger. All Logger methods are forwarded to it unless overridden.
SELECTIVE OVERRIDE
Override only the methods you want to customize:
override log() -> msg as String delegate.log("[FILTERED] " + msg)
The logLevel() method is automatically delegated.
WHY DELEGATION
Delegation avoids tight coupling from inheritance. You can swap the delegate at runtime and only customize the methods that need different behavior.
COMPOSITION BENEFIT
The class wraps and decorates the delegate without extending it. This follows the principle of favoring composition over inheritance.
See Q106 for trait basics. See Q109 for composition. See Q115 for dynamic classes. See Q212 for composition over inheritance. See Q264 for adapter pattern using delegation. See Q266 for cross-cutting concerns via delegation.
See Q334 for transaction decorator pattern.
Example
defines module qa.patterns.delegation defines trait Logger log() as abstract -> msg as String logLevel() as abstract <- rtn as String? defines class ConsoleLogger with trait of Logger override log() -> msg as String Stdout().println("[LOG] " + msg) override logLevel() <- rtn as String: "INFO" default operator ? // Delegation: FilteredLogger delegates to ConsoleLogger // Only overrides log(), logLevel() is auto-delegated FilteredLogger with trait of Logger by delegate delegate as Logger: ConsoleLogger() FilteredLogger() -> logger as Logger this.delegate: logger override log() -> msg as String delegate.log("[FILTERED] " + msg) default operator ? defines program TraitDelegationDemo() stdout <- Stdout() // === BASIC LOGGER === logger <- ConsoleLogger() logger.log("Hello from console") stdout.println(`Level: ${logger.logLevel()}`) // === DELEGATING LOGGER === filtered <- FilteredLogger(logger) filtered.log("Hello through filter") // logLevel() is delegated to ConsoleLogger stdout.println(`Filtered level: ${filtered.logLevel()}`)
Common mistakes
E05120 — When overriding a trait method in a delegating class, the 'override' keyword is required. FilteredLogger must use 'override log()' to customize the delegated Logger method. See ek9 -h E05120 for details.
Incorrect:
log()
Correct:
override log()
E05030 — ConsoleLogger is closed by default and cannot be extended. EK9 types are closed unless declared 'as open'. Use trait delegation with 'by' instead of inheritance. See ek9 -h E05030 for details.
Incorrect:
FilteredLogger extends ConsoleLogger
Correct:
FilteredLogger with trait of Logger by delegate
Other ways to ask this
- How do I delegate trait methods to a field in EK9?
- What is the 'by' keyword for traits in EK9?
- How do I use delegation instead of inheritance in EK9?
Coming from another language?
Java: no built-in delegation, manual forwarding methods. Kotlin: 'by' delegation on interfaces. Rust: no delegation, manual impl forwarding. Go: embedded structs provide forwarding. Python: __getattr__ for delegation. EK9: 'with trait of X by field' delegates all unoverridden methods to the field.
Keywords: pattern, override, idiom, design, compose, delegate, open, virtual, proxy, abstract, delegation, trait, by