E11024: class mixes data and service coordination - split into class + component

Category: E11xxx Code quality and IR analysis · Reported in phase: PRE_IR_CHECKS · Classification: HYBRID_CLASS_COMPONENT · All error codes

What went wrong

Your class uses 'by' delegation but also has many service fields (3+). This mixes data-holding and service-coordination responsibilities.

This pattern indicates the class is trying to do too much:

  • It delegates trait implementation (class pattern with 'by')
  • It also coordinates multiple services (component responsibility)

This violates the Single Responsibility Principle and makes the code harder to test, maintain, and understand.

How services are counted

Fields used in 'by' delegation are NOT counted toward the threshold. Only ADDITIONAL service fields beyond the 'by' delegation trigger this error.

So a class 'with trait of Persistable by repo' holding 'repo' plus a validator, a notifier and an auditor has 3 additional services and triggers this error; a class delegating 'A by a, B by b' and holding one other helper has 1 and is fine.

Worked examples

Compilable examples demonstrating this error and the correct fix:

  • ek9 -q 860 (Why can't I mix 'by' delegation with many service fields?)

How to fix it

  1. SIMPLEST: REMOVE THE 'by' DELEGATION If the delegation isn't essential, remove it and implement the trait directly. This makes the class a coordinator (may then trigger E11022 if 0 data).
  2. SPLIT INTO SEPARATE CONCERNS
  • Data class: holds state
  • Component: coordinates services
  • Thin class: just delegation
  1. EXTRACT SERVICES TO A COMPONENT Keep the delegating class, move service coordination elsewhere.

Transform

The [EXAMPLE] section below shows a class that delegates Persistable 'by repo' while also holding three services, then the split: a thin 'Record' that delegates and holds its own data, and a 'Workflow' component that coordinates the service and works on a Record. Removing the 'by' and implementing the trait directly is the other option, at the cost of making the class a coordinator (which may then be E11022 if it has no data).

Why this matters

  • Separation of concerns improves testability
  • Each piece has a single, clear responsibility
  • Components can be injected and configured
  • Data classes are simple and predictable

See: https://ek9.io/components.html "EK9 reduces the opportunities to 'abuse' Dependency Injection" by constraining components and not allowing classes to be injected. Components are for major subsystems (Factories, Strategies, HRSystem). Classes with 'by' delegation should stay focused on that delegation role.

THRESHOLD: 1+ 'by' delegations AND >= 3 additional service fields

Research basis

  • Martin, R.C.: Single Responsibility Principle
  • Fowler, M.: 'Refactoring' (1999) - God Class code smell
  • Chidamber & Kemerer: CK Metrics Suite - LCOM

Examples

Every example is compiled by the EK9 compiler: the first fails exactly as captioned, the others fix it.

Fails with E11024: class mixes data and service coordination - split into class + component

#!ek9
defines module error.e11024.bad

  defines trait
    Persistable
      load() as abstract

      save() as abstract

  defines trait
    NotificationService
      notify() as abstract
    AuditService
      audit() as abstract
    ValidationService
      validate() as abstract

  defines class
    Repository with trait of Persistable
      override load()
        require true

      override save()
        require true

  defines class
    NotifierImpl with trait of NotificationService
      override notify()
        require true
    AuditorImpl with trait of AuditService
      override audit()
        require true
    ValidatorImpl with trait of ValidationService
      override validate()
        require true

  defines class

    MixedResponsibilities with trait of Persistable by repo
      //Uses 'by' delegation - good so far
      repo as Persistable: Repository()

      //But also has 3+ additional service fields - wants to be a component!
      notifier as NotificationService: NotifierImpl()
      auditor as AuditService: AuditorImpl()
      validator as ValidationService: ValidatorImpl()

      //Has data fields too - so it's NOT pure component (doesn't trigger E11022)
      //But the mix of 'by' + 3 services + data = hybrid (triggers E11024)
      currentData as String: "initial"
      counter as Integer: 0

      //This class is trying to do too much:
      //- It delegates persistence (class pattern with 'by')
      //- It coordinates multiple services (component pattern)
      //- It holds data (class pattern)
      doWork()
        validator.validate()
        repo.save()
        notifier.notify()
        auditor.audit()
        counter: counter + 1

      default operator ?

Fixed: Split it: a class for the data, a component for the coordination

#!ek9
defines module error.e11024.good

  defines trait

    Persistable
      save() as abstract

    NotificationService
      notify() as abstract

  defines class

    Repository with trait of Persistable
      override save()
        stdout <- Stdout()
        stdout.println("saved")

    NotifierImpl with trait of NotificationService
      override notify()
        stdout <- Stdout()
        stdout.println("notified")

    Record with trait of Persistable by repo
      repo as Persistable: Repository()
      currentData as String: "initial"
      counter as Integer: 0

      touch()
        counter: counter + 1

      default operator ?

  defines component

    Workflow
      notifier as NotificationService: NotifierImpl()

      run()
        -> item as Record
        item.save()
        item.touch()
        notifier.notify()

      default operator ?

Questions and answers

These Q&A entries show E11024 and its fix: