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