Why are types closed by default in EK9?

← Classes and OOP · Ref: Q101

EK9 types are closed by default, meaning they cannot be extended unless explicitly marked with 'as open' or 'as abstract'. This prevents the fragile base class problem and enforces composition over inheritance.

THREE LEVELS OF TYPE OPENNESS

EK9 provides three levels of control over type extension:

  1. CLOSED (default) — cannot be extended at all
  2. OPEN ('as open') — can be extended by any class
  3. SEALED ('allow only') — can only be extended by explicitly named classes

This gives precise control: most types stay closed, some are selectively opened, and sealed types restrict extension to a known set.

WHICH CONSTRUCTS SUPPORT OPEN AND ABSTRACT?

Five constructs support 'as open' and 'as abstract': functions, records, traits, classes, and components. Traits are inherently open (designed to be implemented) so 'as open' on a trait is accepted for syntax consistency but has no additional effect. Services and text blocks are always closed — they cannot be extended.

CLOSED BY DEFAULT PRINCIPLE

When you define a class without modifiers, it is final:

  defines class
    Config
      host <- String()

Attempting to extend Config produces compile error E05030.

SEALED TYPES WITH 'ALLOW ONLY'

For types that need limited extensibility, EK9 provides 'allow only' — the equivalent of Java 17's sealed classes and Kotlin's sealed classes. Both traits and classes support this:

  Shape allow only Circle, Square, Triangle
    area() as abstract
      <- rtn as Float?

Only the named classes can implement Shape. Any other class attempting 'with trait of Shape' triggers E05240. Classes with 'allow only' must also be declared 'as open' since classes are closed by default.

THE PROBLEM WITH OPEN TYPES

Open types create maintenance risks: subclasses depend on implementation details, overriding methods can break invariants, and changes to the base class can silently break subclasses (fragile base class problem).

HISTORICAL EVIDENCE

Java's open-by-default ArrayList, HashMap, and other collection classes have been a source of bugs for decades. Subclassing collections mixes collection behaviour with application logic.

MODERN TREND

Swift structs are closed. Rust has no inheritance. Kotlin classes are final by default. Java added sealed classes in Java 17. EK9 follows this modern consensus and goes further by making closed the default.

WHAT THIS MEANS IN PRACTICE

Use composition and delegation instead of inheritance:

  Employee
    role as Role?

This approach is more flexible and avoids tight coupling.

See Q102 for making types extensible with 'as open'. See Q109 for composition patterns. See Q93 for class basics. See Q106 for traits as an alternative. See Q128 for why built-in collection types (List, Dict) are closed and the delegation pattern. See Q212 for composition over inheritance pattern. See Q268 for how closed types prevent OWASP access control vulnerabilities. See Q298 for sealed traits with 'allow only'. See Q301 for sealed classes with 'allow only'. See Q299 for sealed types with dispatchers.

Example

defines module qa.oop.closedbydefault

  defines trait

    SealedTrait allow only AllowedPrinter

      describe() as abstract
        <- rtn as String?

  defines record

    //Closed record - cannot be extended (no 'as open')
    ClosedRecord
      value <- 0
      default operator

    //Open record - can be extended
    OpenRecord as open
      name <- String()
      default operator

    ExtendedRecord extends OpenRecord
      extra <- String()
      default operator

  defines class

    Config
      host <- "localhost"
      port <- 8080

      host() as pure
        <- rtn as String: host

      port() as pure
        <- rtn as Integer: port

      default operator

    //Open class - can be extended
    OpenClass as open
      label <- String()

      label() as pure
        <- rtn as String: label

      default operator

    ExtendedClass extends OpenClass
      detail <- String()
      default operator

    //Sealed trait implementation - allowed
    AllowedPrinter with trait of SealedTrait
      override describe()
        <- rtn as String: "allowed"

      default operator

    //Not in the allow only list
    UnlistedPrinter

      unlisted()
        <- rtn as String: "not allowed"

      default operator

    ServerRunner
      config <- Config()

      ServerRunner()
        -> config as Config
        this.config: config

      describe()
        <- rtn as String: `Server at ${config.host()}:${config.port()}`

      default operator ?

  defines program

    ClosedByDefaultDemo()
      stdout <- Stdout()

      // === CLOSED BY DEFAULT ===

      config <- Config()
      stdout.println(`Config: ${config}`)

      // === OPEN CLASS CAN BE EXTENDED ===

      ext <- ExtendedClass()
      stdout.println(`Extended: ${ext}`)

      // === OPEN RECORD CAN BE EXTENDED ===

      rec <- ExtendedRecord()
      stdout.println(`Extended record: ${rec}`)

      // === SEALED TRAIT — ONLY ALLOWED TYPES ===

      allowed <- AllowedPrinter()
      stdout.println(`Sealed: ${allowed.describe()}`)

      // === COMPOSITION INSTEAD OF INHERITANCE ===

      server <- ServerRunner(config)
      stdout.println(server.describe())

Common mistakes

E05030 — Config is closed by default (no 'as open' modifier). Attempting to extend it triggers E05030 — not open to be extended. EK9 types are closed by default to prevent the fragile base class problem. Use composition instead. See ek9 -h E05030 for details.

Incorrect:

ExtendedClass extends Config

Correct:

ExtendedClass extends OpenClass

E05030 — ClosedRecord has no 'as open' modifier, so it cannot be extended. Records are also closed by default, just like classes. See ek9 -h E05030 for details.

Incorrect:

ExtendedRecord extends ClosedRecord

Correct:

ExtendedRecord extends OpenRecord

E06180 — Class properties are private by default in EK9. Accessing 'host' directly from outside the class fails; use the accessor method 'host()' instead. See ek9 -h E06180 for details.

Incorrect:

config.host

Correct:

config.host()

E05240 — When a trait uses 'allow only' to restrict implementations, only the named classes can implement it. A class not in the list triggers E05240 — type is not permitted to extend/implement this sealed type. This is EK9's equivalent of Java's sealed interfaces. See ek9 -h E05240 for details.

Incorrect:

UnlistedPrinter with trait of SealedTrait

      override describe()

Correct:

UnlistedPrinter

      unlisted()
Other ways to ask this
  • Why can I not extend a class in EK9?
  • What does closed by default mean in EK9?
  • How does EK9 prevent fragile base class problems?
  • What are the three levels of type openness in EK9?

Coming from another language?

Java: classes are open by default, must use 'final' to prevent extension (most developers forget); sealed classes added in Java 17 with 'permits' clause. Python: all classes are open, no way to prevent extension. Rust: no inheritance, uses traits and composition exclusively. Go: no inheritance, only interface embedding and struct composition. Kotlin: classes are final by default, must use 'open' keyword; sealed classes restrict subclasses to same file. Swift: classes are final by default with 'final' keyword, structs cannot be inherited. EK9: three levels — closed (default), open ('as open'), sealed ('allow only' with named permitted types). Closed by default like Kotlin, with sealed types equivalent to Java 17 sealed classes.

Keywords: component, allow, only, swift, final, class, restrict, open, exhaustive, abstract, closed, fragile, base, permit, function, object-oriented, sealed, composition, trait, inheritance, record, default, migrate