E11025: class has too many service and data fields - split into component + data class

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

What went wrong

Your class has 3+ service fields AND 3+ data fields. This is the classic "Blob" or "God Class" anti-pattern - the class is trying to do too much.

This pattern indicates MIXED RESPONSIBILITIES:

  • Holding significant state (data responsibility)
  • Coordinating multiple services (component responsibility)

The Single Responsibility Principle states: "A class should have only one reason to change." Your class has at least two distinct reasons to change.

SEPARATION GUIDANCE - Ask these questions for each field:

DATA FIELDS (should go to a data class):

  • Does it represent state that persists between method calls?
  • Is it passed to/from external systems?
  • Would it make sense in a record?

SERVICE FIELDS (should go to a component):

  • Does it perform operations (validate, transform, notify)?
  • Is it stateless (same input -> same output)?
  • Would it benefit from dependency injection?

Worked examples

Compilable examples demonstrating this error and the correct fix:

  • ek9 -q 861 (Why does EK9 reject classes with too many service and dat...)

How to fix it

  1. SPLIT (recommended)
  • Extract data fields into a separate data class
  • Create a component for service coordination
  • Pass data to component methods instead of storing it
  1. REDUCE SERVICES (if over-designed)
  • Combine related services into a single service
  • Question whether all services are truly needed
  • Example: validator + sanitizer -> inputProcessor
  1. REDUCE DATA (if data should be parameters)
  • Pass data as method parameters instead of storing
  • Keep only truly persistent state as fields

Transform

The [EXAMPLE] section below shows an OrderProcessor class holding three services and three data fields, then the split: the order's data in an 'Order' record, and an 'OrderProcessor' component holding the services and taking the Order as a parameter. Reducing either side below three also clears the error - combining two services into one, or passing a value that was stored (the shipping address, say) as a parameter.

When not to split

  • If the 6 fields are genuinely cohesive (all needed for one operation)
  • If splitting would require passing many parameters between pieces
  • If the "component" would have only one method

In these cases, the threshold may be highlighting over-engineering. Consider REMOVING services rather than adding more structure.

See: https://ek9.io/composition.html "Breaking up classes into smaller implementations creates many classes to be composed." The key is finding the RIGHT blend. Small, well-defined pieces are more reliable, testable, and reusable. But don't split just to meet a threshold - split when it improves clarity.

Why this matters

  • Each class has a single, clear responsibility
  • Data class is simple, predictable, easy to test
  • Component can be injected and configured
  • Changes to data don't affect service coordination
  • Changes to services don't affect data structure

THRESHOLD: >= 3 service fields AND >= 3 data fields

Why this threshold

  • 2 services + 4 data = OK (data class with helpers)
  • 4 services + 2 data = OK (stateful coordinator)
  • 3 services + 3 data = ERROR (neither dominates, mixed)

Research basis

  • Brown et al.: 'AntiPatterns' (1998) - The Blob pattern
  • Marinescu, R.: IEEE ICSM 2004 - God Class detection
  • Riel, A.J.: 'OO Design Heuristics' (1996)
  • Chidamber & Kemerer: CK Metrics Suite - LCOM
  • Basili, Briand, Melo: IEEE TSE 1996 - OO metrics validation

Examples

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

Fails with E11025: class has too many service and data fields - split into component + data class

#!ek9
defines module error.e11025.bad

  defines record
    Order
      id as String: String()
      item as String: String()

      default operator ?

    Amount
      amount as Float: 0.0
      currency as String: "USD"

      default operator ?

    Address
      street as String: String()
      city as String: String()

      default operator ?

  defines trait
    ValidatorService
      validate() as abstract
    PricingService
      calculatePrice() as abstract
    ShippingService
      ship() as abstract
    NotificationService
      notify() as abstract

  defines class
    ValidatorImpl with trait of ValidatorService
      override validate()
        require true
    PricerImpl with trait of PricingService
      override calculatePrice()
        require true
    ShipperImpl with trait of ShippingService
      override ship()
        require true
    NotifierImpl with trait of NotificationService
      override notify()
        require true

  defines class

    OrderProcessor
      //3 service fields
      validator as ValidatorService: ValidatorImpl()
      pricer as PricingService: PricerImpl()
      shipper as ShippingService: ShipperImpl()
      //3 data fields
      currentOrder as Order: Order()
      totalAmount as Amount: Amount()
      shippingAddress as Address: Address()

      process()
        validator.validate()

      default operator ?

  defines class

    MassiveBlob
      //4 service fields
      validator as ValidatorService: ValidatorImpl()
      pricer as PricingService: PricerImpl()
      shipper as ShippingService: ShipperImpl()
      notifier as NotificationService: NotifierImpl()
      //4 data fields
      order1 as Order: Order()
      order2 as Order: Order()
      amount as Amount: Amount()
      destination as Address: Address()

      doWork()
        validator.validate()

      default operator ?

  defines class
    //OK: 2 services + 4 data = under threshold
    DataClassWithHelpers
      validator as ValidatorService: ValidatorImpl()
      pricer as PricingService: PricerImpl()
      order1 as Order: Order()
      order2 as Order: Order()
      amount as Amount: Amount()
      destination as Address: Address()

      doWork()
        validator.validate()

      default operator ?

  defines class
    //OK: 4 services + 2 data = under threshold
    StatefulCoordinator
      validator as ValidatorService: ValidatorImpl()
      pricer as PricingService: PricerImpl()
      shipper as ShippingService: ShipperImpl()
      notifier as NotificationService: NotifierImpl()
      currentOrder as Order: Order()
      amount as Amount: Amount()

      doWork()
        validator.validate()

      default operator ?

  defines class
    //OK: 3 services + 2 data = under threshold
    JustUnderThreshold
      validator as ValidatorService: ValidatorImpl()
      pricer as PricingService: PricerImpl()
      shipper as ShippingService: ShipperImpl()
      order as Order: Order()
      amount as Amount: Amount()

      doWork()
        validator.validate()

      default operator ?

  defines class
    //OK: 2 services + 3 data = under threshold
    OtherBoundary
      validator as ValidatorService: ValidatorImpl()
      pricer as PricingService: PricerImpl()
      order as Order: Order()
      amount as Amount: Amount()
      destination as Address: Address()

      doWork()
        validator.validate()

      default operator ?

Fixed: Keep the services in a component and the data in a record

#!ek9
defines module error.e11025.good

  defines record

    Order
      id as String: String()
      item as String: String()

      default operator ?

  defines trait

    ValidatorService
      validate() as abstract

    PricingService
      calculatePrice() as abstract

  defines class

    ValidatorImpl with trait of ValidatorService
      override validate()
        stdout <- Stdout()
        stdout.println("valid")

    PricerImpl with trait of PricingService
      override calculatePrice()
        stdout <- Stdout()
        stdout.println("priced")

  defines component

    OrderProcessor
      validator as ValidatorService: ValidatorImpl()
      pricer as PricingService: PricerImpl()

      process()
        -> order as Order
        require order?
        validator.validate()
        pricer.calculatePrice()

      default operator ?

Questions and answers

These Q&A entries show E11025 and its fix: