How do I use composition instead of inheritance in EK9?

← Classes and OOP · Ref: Q109

EK9 supports composition over inheritance through trait delegation with the 'by' keyword. A class delegates trait methods to a field, avoiding tight inheritance coupling.

WHY COMPOSITION

Inheritance creates tight coupling between parent and child. Composition lets you combine behaviours flexibly through delegation. EK9's closed-by-default types encourage this approach.

DELEGATION WITH BY KEYWORD

Delegate trait methods to a field:

  Worker with trait of Task by delegate
    delegate as Task?
    Worker()
      -> delegate as Task
      this.delegate: delegate

All Task methods are automatically forwarded to the delegate field.

TRAIT DELEGATION PATTERN

Combine multiple trait delegations:

  Employee with trait of Role by role, Payroll by payroll
    role as Role?
    payroll as Payroll?

COMPOSITION VS INHERITANCE DECISION

Use composition when: you need to combine multiple behaviours, swap implementations at runtime, or the relationship is 'has-a' not 'is-a'.
Use inheritance when: there is a genuine 'is-a' relationship and you control the base class.

See Q101 for closed by default. See Q106 for traits. See Q108 for implementing traits. See Q128 for applying composition to wrap closed collection types. See Q210 for trait delegation pattern. See Q212 for composition over inheritance. See Q264 for adapter pattern using composition. See Q315 for inheritance depth limits.

Example

defines module qa.oop.composition

  defines trait

    Logger
      log()
        -> message as String
      override operator ? as pure
        <- rtn as Boolean: true

    Formatter
      format() as pure
        -> content as String
        <- rtn as String?
      override operator ? as pure
        <- rtn as Boolean: true

  defines class

    ConsoleLogger with trait of Logger
      stdout as Stdout: Stdout()

      override log()
        -> message as String
        stdout.println(message)

      default operator ?

    BracketFormatter with trait of Formatter
      override format() as pure
        -> content as String
        <- rtn as String: `[${content}]`

      default operator ?

    Service with trait of Logger by logger, Formatter by formatter
      logger as Logger?
      formatter as Formatter?

      default private Service()

      Service()
        ->
          logger as Logger
          formatter as Formatter
        this.logger: logger
        this.formatter: formatter

      process()
        -> content as String
        formatted <- format(content)
        log(formatted)

      override operator ? as pure
        <- rtn as Boolean: logger? and formatter?

  defines program

    CompositionDemo()
      stdout <- Stdout()

      // === COMPOSITION VIA DELEGATION ===

      logger <- ConsoleLogger()
      formatter <- BracketFormatter()

      service <- Service(logger, formatter)
      service.process("Hello composition")
      service.process("Delegation works")

      // === DELEGATED METHODS ALSO CALLABLE DIRECTLY ===

      service.log("Direct log call")
      stdout.println(service.format("Direct format"))

Common mistakes

E05120 — When implementing an abstract or default trait method in a concrete class, the 'override' keyword is required. ConsoleLogger must use 'override log()' to implement Logger's log method. See ek9 -h E05120 for details.

Incorrect:

log()

Correct:

override log()

E05030 — ConsoleLogger is closed by default (no 'as open'). Use trait delegation with 'by' instead of inheritance to compose behaviours. See ek9 -h E05030 for details.

Incorrect:

Service extends ConsoleLogger

Correct:

Service with trait of Logger by logger, Formatter by formatter

E07090 — Changing class name to 'CompositeTrait' makes the existing constructor named 'Service()' no longer match the class name. It becomes a regular method, and 'default' modifier on a regular method triggers E07090 — 'default' is only valid for constructors. See ek9 -h E07090 for details.

Incorrect:

    CompositeTrait with trait of Logger by logger
      logger as Logger?

Correct:

    Service with trait of Logger by logger, Formatter by formatter
      logger as Logger?
Other ways to ask this
  • How does delegation work in EK9?
  • What is the 'by' keyword for trait delegation in EK9?
  • How do I avoid inheritance with composition in EK9?

Coming from another language?

Java: no delegation syntax, must manually write forwarding methods or use IDE generation. Python: no delegation syntax, use __getattr__ for dynamic delegation. Rust: no delegation, manual forwarding or Deref trait abuse. Go: struct embedding provides automatic method forwarding, closest to EK9 delegation. Kotlin: 'by' keyword for interface delegation, identical concept to EK9. EK9: 'with trait of T by field' delegates all trait methods to the field automatically, combinable with multiple traits.

Keywords: flexible, combine, inheritance, composition, decouple, delegation, forward, object-oriented, field, by, trait, migrate