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