How do I refactor circular dependencies using composition?
← Type Hierarchy Constraints · Ref: Q610
When you encounter E05020 (circular hierarchy), the root cause is usually two types that each depend on the other's features. The solution is composition: instead of A extending B and B extending A, extract the shared behavior and have both types delegate to it.
EXTRACT SHARED BEHAVIOR
Identify what A needs from B and what B needs from A. Create a separate type (trait or class) for the shared behavior. Both A and B depend on the extracted type instead of on each other.
USE TRAITS FOR CONTRACTS
If A and B need to call methods on each other, define traits that describe the required interface. Each class implements the trait the other needs, breaking the direct dependency.
DELEGATE INSTEAD OF INHERIT
Instead of inheriting methods, hold a reference to the other object and call methods through delegation. This is the classic 'composition over inheritance' principle.
PRACTICAL EXAMPLE
If Engine needs Car features and Car needs Engine features, extract a Powertrain trait. Engine implements its contract, Car implements its contract, and they communicate through the trait interface rather than inheritance.
See Q602 for circular hierarchy basics. See Q109 for composition patterns. See Q212 for composition over inheritance. See Q210 for trait delegation.
Example
defines module qa.typehierarchy.composition defines trait //Shared behavior extracted into traits Reportable report() as abstract <- rtn as String? Trackable tracking() as abstract <- rtn as String? defines class //Instead of Order extending Shipment extending Order (circular), //both implement shared traits and use composition OrderItem with trait of Reportable itemName <- "item" shipmentRef <- String() OrderItem() -> itemName as String shipmentRef as String this.itemName: itemName this.shipmentRef: shipmentRef override report() <- rtn as String: `Order: ${itemName}` shipmentReference() as pure <- rtn as String: shipmentRef default operator ? Shipment with trait of Trackable shipmentId <- "shipment" orderRef <- String() Shipment() -> shipmentId as String orderRef as String this.shipmentId: shipmentId this.orderRef: orderRef override tracking() <- rtn as String: `Shipment: ${shipmentId}` orderReference() as pure <- rtn as String: orderRef default operator ? defines function testCompositionOverCycles() //OrderItem and Shipment reference each other by ID, not by inheritance item <- OrderItem("Widget", "SHIP-001") shipment <- Shipment("SHIP-001", "ORD-100") require item.report() == "Order: Widget" require shipment.tracking() == "Shipment: SHIP-001" require item.shipmentReference() == "SHIP-001" require shipment.orderReference() == "ORD-100"
Common mistakes
E05030 — If OrderItem extended Shipment and Shipment extended OrderItem, a circular hierarchy would be created. Using composition with traits avoids this cycle. See ek9 -h E05030 for details.
Incorrect:
OrderItem extends Shipment
Correct:
OrderItem with trait of Reportable
E05120 — When implementing the abstract report method from the Reportable trait, the override keyword is required. Omitting it triggers shadowing detection. See ek9 -h E05120 for details.
Incorrect:
report()
Correct:
override report()
Other ways to ask this
- How to fix circular hierarchy errors in EK9?
- What is the composition pattern for breaking cycles?
- How do I restructure code that has circular inheritance?
Coming from another language?
Java: same refactoring pattern applies (extract interface, use composition). Python: mixins or composition to break cycles. Rust: traits for shared behavior, no inheritance. Go: interfaces and embedding for composition. Kotlin: delegation pattern with 'by' keyword. EK9: use traits and composition to replace circular inheritance.
Keywords: refactor, trait, hierarchy, dependency, delegate, composition, circular, inheritance, type, E05020, extract