Why must injected components be abstract types?

← DI Validation · Ref: Q667

EK9 requires injected component types to be abstract (E08150). This enforces the Dependency Inversion Principle: depend on abstractions, not implementations.

WHY ABSTRACT ONLY

Concrete injection creates tight coupling. Abstract injection allows:

  1. Different applications wire different implementations
  2. Test applications inject mocks
  3. Production applications inject real services

REGISTRATION MUST USE 'AS'

Concrete components must be registered with 'as AbstractType':

  register ConsoleLogger() as Logger    // Correct
  register ConsoleLogger()              // ERROR: E50020

Without 'as', the registration genus is incompatible.

INJECTION MUST USE ABSTRACT TYPE

  logger as Logger!                     // Correct: abstract

Injecting the concrete type instead is wrong and fails with E08150:

  defines component
    Logger as abstract
      log() as abstract
        -> message as String
    ConsoleLogger extends Logger
      override log()
        -> message as String
        stdout <- Stdout()
        stdout.println(message)
  defines application
    ProdApp
      register ConsoleLogger() as Logger
  defines program
    MyProgram with application of ProdApp
      logger as ConsoleLogger!       // ERROR: E08150
      logger.log("hello")

PATTERN

  defines component
    Logger as abstract
      log() as abstract
        -> message as String
    ConsoleLogger extends Logger
      override log()
        -> message as String
        stdout <- Stdout()
        stdout.println(message)
  defines application
    ProdApp
      register ConsoleLogger() as Logger
  defines program
    MyProgram with application of ProdApp
      logger as Logger!       // Inject abstract

See Q111 for component basics. See Q666 for pure context. See Q668 for injectable contexts. See Q324 for Autowired equivalent. See Q325 for Component equivalent.

Example

#!ek9
defines module qa.divalidation.abstractonly

  defines component

    <?-
      Abstract logger contract.
      Concrete implementations are registered in applications.
    -?>
    Logger as abstract

      logMessage() as abstract
        -> logEntry as String

      default operator ?

    <?-
      Console logger implementation.
    -?>
    ConsoleLogger extends Logger

      override logMessage()
        -> logEntry as String
        stdout <- Stdout()
        stdout.println(logEntry)

      default operator ?

    <?-
      Abstract repository contract.
    -?>
    UserRepository as abstract

      findByName() as abstract
        -> userName as String
        <- result as String?

      default operator ?

    <?-
      In-memory repository implementation.
    -?>
    InMemoryUserRepo extends UserRepository

      override findByName()
        -> userName as String
        <- result as String: ""

        result: "Found: " + userName

      default operator ?

  defines application

    ProductionApp
      register ConsoleLogger() as Logger
      register InMemoryUserRepo() as UserRepository

  defines program

    AbstractInjectionDemo() with application of ProductionApp
      stdout <- Stdout()

      logger as Logger!
      userRepo as UserRepository!

      logger.logMessage("Starting application")

      foundUser <- userRepo.findByName("Alice")
      stdout.println(foundUser)

      logger.logMessage("Application complete")

Common mistakes

E08150 — Only abstract component types can be injected. Injecting a concrete type like ConsoleLogger creates tight coupling and defeats the purpose of dependency injection. Depend on the abstract type Logger and let the application wiring choose the implementation. See ek9 -h E08150 for details.

Incorrect:

logger as ConsoleLogger!

Correct:

logger as Logger!
Other ways to ask this
  • What is E08150 injecting concrete component?
  • Why can't I inject a concrete component class?
  • How does abstract injection enable testing?

Coming from another language?

Java: Spring injects concrete types (bad practice). Python: duck typing, no enforcement. Go: interfaces encouraged but not required. Rust: trait objects for abstraction. EK9: compiler enforces abstract injection, concrete types cannot be injected.

Keywords: DI, E08150, abstract, component, concrete, inject, injection, interface, registration, validate