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