E11022: class has component characteristics - use 'defines component'

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

What went wrong

Your class has 4+ service (abstract/trait) fields and 0 data fields. This is a pure service coordinator - architecturally, it's a component.

EK9 has distinct constructs for distinct architectural roles:

  • Classes: Data + behavior (state + methods operating on that state)
  • Components: Service coordination with dependency injection

A class with only service dependencies and no data is acting as a component - it coordinates other services rather than managing state.

Type classification

SERVICE fields (behavior contracts - you program to the interface):

  • Traits: 'validator as Validator?' where Validator is a trait
  • Abstract classes: 'processor as Processor?' where Processor is abstract
  • Abstract functions: 'handler as Handler?' where Handler is a function signature

DATA fields (concrete values - you have the actual implementation):

  • Records: 'order as OrderRecord?'
  • Text constructs: 'messages as Translations?'
  • Concrete classes: 'name as String?', 'count as Integer?', 'config as MyConfig?'

THE KEY DISTINCTION - Programming to interface vs instantiating concrete: a field typed as a trait ('validator as Validator?' where Validator is a trait) is a SERVICE; a field typed as a concrete class and given an instance of it ('productivitySystem' declared as a ProductivitySystem and initialised with 'ProductivitySystem()') is DATA.

The second pattern creates and holds a concrete object. Even though ProductivitySystem might have methods, it's a concrete class you instantiate directly - making it DATA, not a service abstraction.

See: https://ek9.io/composition.html The "EmployeeDatabase" example holds "productivitySystem", "payrollSystem", and "addresses" - all concrete class instances (DATA fields). This is valid composition where concrete helpers are used, not service coordination.

Parameterized types - based on what's INSIDE:

  • 'validators as List of Validator?' -> SERVICE (Validator is a trait)
  • 'names as List of String?' -> DATA (String is concrete)
  • 'users as Dict of (String, UserRecord)?' -> DATA (value type is concrete)

EXCLUSION: Generic classes are excluded from this check. Generic classes cannot be components in EK9, so this error doesn't apply to them.

Worked examples

Compilable examples demonstrating this error and the correct fix:

  • ek9 -q 856 (When should I use a component instead of a class in EK9?)

How to fix it

  1. CONVERT TO COMPONENT (recommended for application code)
  • Declare it in a component block instead of a class block
  • Mark fields for injection with '!' suffix
  • Register in your application's component section
  1. ADD A DATA FIELD (if component is inappropriate)
  • Adding even one data field removes this error
  • Use for: library code, test utilities, or intentional facades
  1. REDUCE SERVICE COUNT (if over-designed)
  • Combine related services into a single service
  • Question whether all 4+ services are truly needed

Transform

The [EXAMPLE] section below shows a class holding four trait-typed services and no data, and the coordinator declared as a component instead. Converted to a component, its service fields can be injected ('serviceA as ServiceA!') rather than constructed. The other way out is a class that gains a real data field (a 'config' it holds, say), which makes it a legitimate class.

When to keep as class

  • Library code that shouldn't depend on Application registration
  • Test utilities and mocks
  • Intentional facade pattern with legitimate reason

Why this matters

  • Components enable EK9's dependency injection system
  • Components can be registered and wired automatically
  • Using the correct construct makes architectural intent clear
  • EK9's compile-time verification is more effective with proper typing

THRESHOLD: >= 4 service fields AND 0 data fields

Research basis

  • Martin, R.C.: Single Responsibility Principle - "A class should have one reason to change"
  • Riel, A.J.: 'OO Design Heuristics' (1996) - "Top-level classes share work uniformly"
  • Marinescu, R.: IEEE ICSM 2004 - Detection strategies for design flaws
  • Brown et al.: 'AntiPatterns' (1998) - The Blob pattern definition

Examples

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

Fails with E11022: class has component characteristics - use 'defines component'

#!ek9
defines module error.e11022.bad

  defines trait
    ServiceA
      doA() as abstract
    ServiceB
      doB() as abstract
    ServiceC
      doC() as abstract
    ServiceD
      doD() as abstract

  defines class
    //Concrete implementations of the traits
    ServiceAImpl with trait of ServiceA
      override doA()
        require true
    ServiceBImpl with trait of ServiceB
      override doB()
        require true
    ServiceCImpl with trait of ServiceC
      override doC()
        require true
    ServiceDImpl with trait of ServiceD
      override doD()
        require true

  defines class

    PureCoordinator
      //4 service (trait) fields initialized to implementations
      //0 data fields = should be component
      serviceA as ServiceA: ServiceAImpl()
      serviceB as ServiceB: ServiceBImpl()
      serviceC as ServiceC: ServiceCImpl()
      serviceD as ServiceD: ServiceDImpl()

      orchestrate()
        serviceA.doA()
        serviceB.doB()
        serviceC.doC()
        serviceD.doD()

      default operator ?

Fixed: A type that only coordinates services is a component

#!ek9
defines module error.e11022.good

  defines trait

    ServiceA
      doA() as abstract

    ServiceB
      doB() as abstract

  defines class

    ServiceAImpl with trait of ServiceA
      override doA()
        stdout <- Stdout()
        stdout.println("A")

    ServiceBImpl with trait of ServiceB
      override doB()
        stdout <- Stdout()
        stdout.println("B")

  defines component

    Coordinator
      serviceA as ServiceA: ServiceAImpl()
      serviceB as ServiceB: ServiceBImpl()

      orchestrate()
        serviceA.doA()
        serviceB.doB()

      default operator ?

Questions and answers

These Q&A entries show E11022 and its fix: